Plugins handle the transformation between external file formats (JSON, i18next, XLIFF) and inlang's internal data model. They only do import/export — they don't touch the database directly.

┌─────────────────┐       ┌─────────┐       ┌──────────────────┐
│  .inlang file   │◄─────►│ Plugins │◄─────►│ Translation files│
│    (SQLite)     │       │         │       │  (JSON, XLIFF)   │
└─────────────────┘       └─────────┘       └──────────────────┘

Plugin interface

type InlangPlugin<ExternalSettings = unknown> = {
  key: string;
  settingsSchema?: TObject;
  toBeImportedFiles?: (args) => Promise<Array<{ path, locale, metadata? }>>;
  importFiles?: (args) => Promise<{ bundles, messages, variants }>;
  exportFiles?: (args) => Promise<Array<ExportFile>>;
  meta?: Record<string, Record<string, unknown>>;
};

Minimal example

import type { InlangPlugin } from "@inlang/sdk";

export const plugin: InlangPlugin = {
  key: "plugin.my.json",

  toBeImportedFiles: async ({ settings }) => {
    return settings.locales.map((locale) => ({
      path: `./messages/${locale}.json`,
      locale,
    }));
  },

  importFiles: async ({ files, settings }) => {
    const bundles = [];
    const messages = [];
    const variants = [];

    for (const file of files) {
      const json = JSON.parse(new TextDecoder().decode(file.content));

      for (const [key, value] of Object.entries(json)) {
        bundles.push({ id: key, declarations: [] });
        messages.push({
          bundleId: key,
          locale: file.locale,
          selectors: [],
        });
        variants.push({
          messageBundleId: key,
          messageLocale: file.locale,
          matches: [],
          pattern: [{ type: "text", value: value as string }],
        });
      }
    }

    return { bundles, messages, variants };
  },

  exportFiles: async ({ bundles, messages, variants, settings }) => {
    const files: Record<string, Record<string, string>> = {};

    for (const message of messages) {
      const variant = variants.find((v) => v.messageId === message.id);
      const text = variant?.pattern
        .filter((p) => p.type === "text")
        .map((p) => p.value)
        .join("");

      if (!files[message.locale]) files[message.locale] = {};
      files[message.locale][message.bundleId] = text ?? "";
    }

    return Object.entries(files).map(([locale, content]) => ({
      locale,
      name: `${locale}.json`,
      content: new TextEncoder().encode(JSON.stringify(content, null, 2)),
    }));
  },
};

Methods

toBeImportedFiles

Discovers which files should be imported from the filesystem.

toBeImportedFiles: async ({ settings }) => {
  return [
    { path: "./messages/en.json", locale: "en" },
    { path: "./messages/de.json", locale: "de" },
  ];
}

Parameters:

  • settings — Project settings including plugin-specific config

Returns: Array of file descriptors:

  • path — Path to the file
  • locale — Locale this file contains
  • metadata — Optional, passed to importFiles

importFiles

Parses file content and converts to inlang's data model.

importFiles: async ({ files, settings }) => {
  return {
    bundles: [...],
    messages: [...],
    variants: [...],
  };
}

Parameters:

  • files — Array of files to import:
    • locale — The locale
    • content — Binary file content (Uint8Array)
    • toBeImportedFilesMetadata — Metadata from toBeImportedFiles
  • settings — Project settings

Returns:

  • bundles — Array of BundleImport
  • messages — Array of MessageImport
  • variants — Array of VariantImport

exportFiles

Converts inlang's data model back to files.

exportFiles: async ({ bundles, messages, variants, settings }) => {
  return [
    {
      locale: "en",
      name: "en.json",
      content: new TextEncoder().encode(JSON.stringify(data)),
    },
  ];
}

Parameters:

  • bundles — All bundles
  • messages — All messages
  • variants — All variants
  • settings — Project settings

Returns: Array of files to write:

  • locale — The locale
  • name — Filename (e.g., "en.json")
  • content — Binary content (Uint8Array)

Settings schema

Define plugin settings using TypeBox:

import { Type } from "@sinclair/typebox";

export const PluginSettings = Type.Object({
  pathPattern: Type.String({
    pattern: ".*\\{locale\\}.*\\.json$",
    description: "Path to translation files",
    examples: ["./messages/{locale}.json"],
  }),
  sort: Type.Optional(
    Type.Union([Type.Literal("asc"), Type.Literal("desc")])
  ),
});

Then reference it in your plugin:

export const plugin: InlangPlugin<{
  "plugin.my.json": typeof PluginSettings;
}> = {
  key: "plugin.my.json",
  settingsSchema: PluginSettings,
  // ...
};

Users configure your plugin in settings.json:

{
  "baseLocale": "en",
  "locales": ["en", "de"],
  "modules": ["./plugins/my-plugin.js"],
  "plugin.my.json": {
    "pathPattern": "./messages/{locale}.json",
    "sort": "asc"
  }
}

Import types

BundleImport

type BundleImport = {
  id: string;
  declarations: Declaration[];
};

MessageImport

type MessageImport = {
  id?: string;  // auto-generated if omitted
  bundleId: string;
  locale: string;
  selectors: VariableReference[];
};

VariantImport

Variants can reference messages by ID or by bundle/locale:

// By message ID
type VariantImport = {
  id?: string;
  messageId: string;
  matches: Match[];
  pattern: Pattern;
};

// By bundle/locale (recommended)
type VariantImport = {
  messageBundleId: string;
  messageLocale: string;
  matches: Match[];
  pattern: Pattern;
};

Meta

Use meta to expose plugin-specific APIs to other tools:

export const plugin: InlangPlugin = {
  key: "plugin.my.json",
  meta: {
    "app.inlang.ideExtension": {
      documentSelectors: [{ language: "json" }],
    },
  },
};

Next steps