Author:
    Creation:2025-03-13Last update:2026-06-21

    Sync JSON (i18n bridges) - Sync JSON with ICU / i18next support

    www.youtube.com

    Use Intlayer as an add‑on to your existing i18n stack. This plugin keeps your JSON messages in sync with Intlayer dictionaries so you can:

    • Keep i18next, next‑intl, react‑intl, vue‑i18n, next‑translate, nuxt‑i18n, Solid‑i18next, svelte‑i18n, etc.
    • Manage and translate your messages with Intlayer (CLI, CI, providers, CMS), without refactoring your app.
    • Deliver tutorials and SEO content targeting each ecosystem, whilst recommending Intlayer as the JSON management layer.

    Notes and current scope:

    • Externalisation to the CMS works for translations and classic text.
    • No support yet for insertions, plurals/ICU, or advanced runtime features of other libraries.
    • The visual editor is not supported yet for third‑party i18n outputs.

    When to use this plugin

    • You already use an i18n library and store messages in JSON files.
    • You want AI-assisted fill, test in CI, and content operations without changing your rendering runtime.

    Installation

    bash
    pnpm add -D @intlayer/sync-json-plugin# ornpm i -D @intlayer/sync-json-plugin

    Plugins

    This package provides two plugins:

    • loadJSON: Load JSON files into Intlayer dictionaries.

      • This plugin is used to load JSON files from a source and will be loaded into Intlayer dictionaries. It can scan all the codebase and search for specific JSON files. This plugin can be used
        • if you use an i18n library that imposes a specific location for your JSON to be loaded (ex: next-intl, i18next, react-intl, vue-i18n, etc.), but you want to place your content declaration where you want in your code base.
        • It can also be used if you want to fetch your messages from a remote source (ex: a CMS, an API, etc.) and store your messages in JSON files.

      Under the hood, this plugin will scan all the codebase and search for specific JSON files and load them into Intlayer dictionaries. Note that this plugin will not write the output and translations back to the JSON files.

    • syncJSON: Synchronise JSON files with Intlayer dictionaries.

      • This plugin is used to synchronise JSON files with Intlayer dictionaries. It can scan the given location and load the JSON that match the pattern for specific JSON files. This plugin is useful if you want to get the benefits of Intlayer whilst using another i18n library.

    Using both plugins

    intlayer.config.ts
    import { Locales, type IntlayerConfig } from "intlayer";import { loadJSON, syncJSON } from "@intlayer/sync-json-plugin";const config: IntlayerConfig = {  internationalization: {    locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],    defaultLocale: Locales.ENGLISH,  },  // Keep your current JSON files in sync with Intlayer dictionaries  plugins: [    /**     * Will load all the JSON files in the src that match the pattern {key}.i18n json     */    loadJSON({      source: ({ key }) => `./src/**/${key}.i18n.json`,      locale: Locales.ENGLISH,      priority: 1, // Ensures these JSON files take precedence over files at `./locales/en/${key}.json`      format: "intlayer", // Format of the JSON content    }),    /**     * Will load, and write the output and translations back to the JSON files in the locales directory     */    syncJSON({      source: ({ key, locale }) => `./locales/${locale}/${key}.json`,      priority: 0,      format: "i18next",    }),  ],};export default config;

    syncJSON plugin

    Quick start

    Add the plugin to your intlayer.config.ts and point it at your existing JSON structure.

    intlayer.config.ts
    import { Locales, type IntlayerConfig } from "intlayer";import { syncJSON } from "@intlayer/sync-json-plugin";const config: IntlayerConfig = {  internationalization: {    locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],    defaultLocale: Locales.ENGLISH,  },  // Keep your current JSON files in sync with Intlayer dictionaries  plugins: [    syncJSON({      // Per-locale, per-namespace layout (e.g., next-intl, i18next with namespaces)      source: ({ key, locale }) => `./locales/${locale}/${key}.json`,      format: "icu",    }),  ],};export default config;

    Alternative: single file per locale (common with i18next/react-intl setups):

    intlayer.config.ts
    import { Locales, type IntlayerConfig } from "intlayer";import { syncJSON } from "@intlayer/sync-json-plugin";const config: IntlayerConfig = {  internationalization: {    locales: [Locales.ENGLISH, Locales.FRENCH],    defaultLocale: Locales.ENGLISH,  },  plugins: [    syncJSON({      format: "i18next",      source: ({ locale }) => `./locales/${locale}.json`,      format: "i18next",    }),  ],};export default config;

    How it works

    • Read: the plugin discovers JSON files from your source builder and loads them as Intlayer dictionaries.
    • Write: after builds and fills, it writes localised JSON back to the same paths (with a final newline to avoid formatting issues).
    • Auto‑fill: the plugin declares an autoFill path for each dictionary. Running intlayer fill updates only missing translations in your JSON files by default.

    API:

    ts
    syncJSON({  source: ({ key, locale }) => string, // required  location?: string, // optional label, default: "plugin"  priority?: number, // optional priority for conflict resolution, default: 0  format?: 'intlayer' | 'icu' | 'i18next', // optional formatter, used for intlayer runtime compatibility  splitKeys?: boolean, // optional, split a single file into one dictionary per top-level key (auto-detected)});

    format ('intlayer' | 'icu' | 'i18next')

    Specifies the formatter to use for the dictionary content when synchronising JSON files. This allows using different message formatting syntaxes compatible with intlayer runtime.

    • undefined: No formatter will be used, the JSON content will be used as is.
    • 'intlayer': The default Intlayer formatter (default).
    • 'icu': Uses ICU message formatting (compatible with libraries like react-intl, vue-i18n).
    • 'i18next': Uses i18next message formatting (compatible with i18next, next-i18next, Solid-i18next).

    Note that using a formatter will transform your JSON content in input, and output. For complex json rules as ICU plurals, the parsing may not ensure a 1 to 1 mapping between the input and output. If you do not use Intlayer runtime, you may prefer to not set a formatter.

    Example:

    ts
    syncJSON({  source: ({ key, locale }) => `./locales/${locale}/${key}.json`,  format: "i18next", // Use i18next formatting for compatibility}),

    splitKeys (boolean)

    Controls whether a single JSON file whose first-level keys are namespaces should become one dictionary per top-level key, instead of a single dictionary holding the whole file.

    This matches the namespace model of libraries like next-intl and react-intl, where one messages/{locale}.json file groups several namespaces by its first-level keys, each addressed independently (e.g. useTranslations('Hero') resolves to the Hero dictionary).

    • undefined (default): auto-detected — the file is split when the source pattern has no {key} segment (one file holds every namespace), and kept as a single dictionary otherwise (one file per key).
    • true: always split each top-level key into its own dictionary.
    • false: never split; the whole file becomes a single dictionary.

    Given a single messages/{locale}.json file:

    messages/en.json
    {  "Hero": { "title": "Full-stack developer" },  "Nav": { "work": "Work", "about": "About" },  "About": { "lead": "I build apps end to end." }}
    intlayer.config.ts
    syncJSON({  format: "icu",  source: ({ locale }) => `./messages/${locale}.json`,  // splitKeys: true, // implied because the pattern has no `{key}` segment}),

    This produces three dictionaries — Hero, Nav, and About — so useTranslations('Hero') (next-intl) resolves correctly. On write-back, all namespaces are re-assembled into the same per-locale file.

    When you keep the explicit {key} segment in your source (e.g. ./locales/${locale}/${key}.json), each file is already one namespace, so splitting is disabled by default.

    Multiple JSON sources and priority

    You can add multiple syncJSON plugins to synchronise different JSON sources. This is useful when you have multiple i18n libraries or different JSON structures in your project.

    Priority system

    When multiple plugins target the same dictionary key, the priority parameter determines which plugin takes precedence:

    • Higher priority numbers win over lower ones
    • Default priority of .content files is 0
    • Default priority of plugins is 0
    • Plugins with the same priority are processed in the order they appear in the configuration
    intlayer.config.ts
    import { Locales, type IntlayerConfig } from "intlayer";import { syncJSON } from "@intlayer/sync-json-plugin";const config: IntlayerConfig = {  internationalization: {    locales: [Locales.ENGLISH, Locales.FRENCH],    defaultLocale: Locales.ENGLISH,  },  plugins: [    // Primary JSON source (highest priority)    syncJSON({      format: "i18next",      source: ({ key, locale }) => `./locales/${locale}/${key}.json`,      location: "main-translations",      priority: 10,    }),    // Fallback JSON source (lower priority)    syncJSON({      format: "i18next",      source: ({ locale }) => `./fallback-locales/${locale}.json`,      location: "fallback-translations",      priority: 5,    }),    // Legacy JSON source (lowest priority)    syncJSON({      format: "i18next",      source: ({ locale }) => `/my/other/app/legacy/${locale}/messages.json`,      location: "legacy-translations",      priority: 1,    }),  ],};export default config;

    Load JSON plugin

    Quick start

    Add the plugin to your intlayer.config.ts to ingest existing JSON files as Intlayer dictionaries. This plugin is read‑only (no writes to disk):

    intlayer.config.ts
    import { Locales, type IntlayerConfig } from "intlayer";import { loadJSON } from "@intlayer/sync-json-plugin";const config: IntlayerConfig = {  internationalization: {    locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],    defaultLocale: Locales.ENGLISH,  },  plugins: [    // Ingest JSON messages located anywhere in your source tree    loadJSON({      source: ({ key }) => `./src/**/${key}.i18n.json`,      // Load a single locale per plugin instance (defaults to the config defaultLocale)      locale: Locales.ENGLISH,      priority: 0,    }),  ],};export default config;

    Alternative: per‑locale layout, still read‑only (only the selected locale is loaded):

    intlayer.config.ts
    import { Locales, type IntlayerConfig } from "intlayer";import { loadJSON } from "@intlayer/sync-json-plugin";const config: IntlayerConfig = {  internationalization: {    locales: [Locales.ENGLISH, Locales.FRENCH],    defaultLocale: Locales.ENGLISH,  },  plugins: [    loadJSON({      // Only files for Locales.FRENCH will be loaded from this pattern      source: ({ key, locale }) => `./locales/${locale}/${key}.json`,      locale: Locales.FRENCH,    }),  ],};export default config;

    How it works

    • Discover: builds a glob from your source builder and collects matching JSON files.
    • Ingest: loads each JSON file as an Intlayer dictionary with the provided locale.
    • Read‑only: does not write or format output files; use syncJSON if you need round‑trip sync.
    • Auto‑fill ready: defines a fill pattern so intlayer content fill can populate missing keys.

    API

    ts
    loadJSON({  // Build paths to your JSON. `locale` is optional if your structure has no locale segment  source: ({ key, locale }) => string,  // Target locale for the dictionaries loaded by this plugin instance  // Defaults to configuration.internationalization.defaultLocale  locale?: Locale,  // Optional label to identify the source  location?: string, // default: "plugin"  // Priority used for conflict resolution against other sources  priority?: number, // default: 0  // Optional formatter for the JSON content  format?: 'intlayer' | 'icu' | 'i18next', // default: 'intlayer'  // Split a single file into one dictionary per top-level key (auto-detected)  splitKeys?: boolean,});

    format ('intlayer' | 'icu' | 'i18next')

    Specifies the formatter to use for the dictionary content when loading JSON files. This allows using different message formatting syntaxes compatible with various i18n libraries.

    • 'intlayer': The default Intlayer formatter (default).
    • 'icu': Uses ICU message formatting (compatible with libraries like react-intl, vue-i18n).
    • 'i18next': Uses i18next message formatting (compatible with i18next, next-i18next, Solid-i18next).

    Example:

    ts
    loadJSON({  source: ({ key }) => `./src/**/${key}.i18n.json`,  locale: Locales.ENGLISH,  format: "icu", // Use ICU formatting for compatibility}),

    splitKeys (boolean)

    Same behaviour as in syncJSON: when a single JSON file groups several namespaces by its first-level keys, each top-level key becomes its own dictionary.

    • undefined (default): auto-detected — split when the source pattern has no {key} segment, single dictionary otherwise.
    • true / false: force or disable splitting.
    ts
    loadJSON({  source: ({ locale }) => `./messages/${locale}.json`,  format: "icu",  // splitKeys auto-enabled: `Hero`, `Nav`, `About`, … each become a dictionary}),

    Behavior and conventions

    • If your source mask includes a locale placeholder, only files for the selected locale are ingested.
    • If there is no {key} segment in your mask, each top-level key of the file becomes its own dictionary by default (see splitKeys). Set splitKeys: false to instead load the whole file as a single index dictionary.
    • Keys are derived from file paths by substituting the {key} placeholder in your source builder.
    • The plugin only uses discovered files and does not fabricate missing locales or keys.
    • The fill path is inferred from your source and used to update missing values via CLI when you opt‑in.

    Conflict resolution

    When the same translation key exists in multiple JSON sources:

    1. The plugin with the highest priority determines the final value
    2. Lower priority sources are used as fallbacks for missing keys
    3. This allows you to maintain legacy translations whilst gradually migrating to new structures

    CLI

    The synchronised JSON files will be considered as other .content files. That means, all intlayer commands will be available for the synchronised JSON files. Including:

    • intlayer content test to test if there are missing translations
    • intlayer content list to list the synchronised JSON files
    • intlayer content fill to fill the missing translations
    • intlayer content push to push the synchronised JSON files
    • intlayer content pull to pull the synchronised JSON files

    See Intlayer CLI for more details.

    Limitations (current)

    • No insertions or plurals/ICU support when targeting third‑party libraries.
    • Visual editor is not available for non‑Intlayer runtimes yet.
    • JSON synchronisation only; non‑JSON catalogue formats are not supported.

    Why this matters

    • We can recommend established i18n solutions and position Intlayer as an add‑on.
    • We leverage their SEO/keywords with tutorials that end by suggesting Intlayer to manage JSON.
    • Expands the addressable audience from “new projects” to “any team already using i18n”.