@intlayer/sync-json-plugin 9.0.0-canary.2 → 9.0.0-canary.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,16 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_syncJSON = require('./syncJSON.cjs');
2
+ const require_jsonCodec = require('./jsonCodec.cjs');
3
3
  const require_loadJSON = require('./loadJSON.cjs');
4
+ const require_syncJSON = require('./syncJSON.cjs');
5
+ let _intlayer_engine_syncPluginKit = require("@intlayer/engine/syncPluginKit");
4
6
 
5
- exports.extractKeyAndLocaleFromPath = require_syncJSON.extractKeyAndLocaleFromPath;
7
+ Object.defineProperty(exports, 'extractKeyAndLocaleFromPath', {
8
+ enumerable: true,
9
+ get: function () {
10
+ return _intlayer_engine_syncPluginKit.extractKeyAndLocaleFromPath;
11
+ }
12
+ });
13
+ exports.jsonCodec = require_jsonCodec.jsonCodec;
6
14
  exports.loadJSON = require_loadJSON.loadJSON;
15
+ exports.readJSONEntry = require_jsonCodec.readJSONEntry;
7
16
  exports.syncJSON = require_syncJSON.syncJSON;
@@ -0,0 +1,23 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ let _intlayer_config_file = require("@intlayer/config/file");
3
+
4
+ //#region src/jsonCodec.ts
5
+ /**
6
+ * JSON payload codec: standard JSON parsing, pretty-printed serialization
7
+ * (2-space indentation, trailing newline).
8
+ */
9
+ const jsonCodec = {
10
+ parse: (raw) => JSON.parse(raw),
11
+ serialize: (content) => `${JSON.stringify(content, null, 2)}\n`
12
+ };
13
+ /**
14
+ * Read one JSON source through `loadExternalFile`, which also supports JSON5,
15
+ * JSONC and transpiled JS/TS message files. Returns `undefined` for missing or
16
+ * unreadable files so callers fall back to empty content.
17
+ */
18
+ const readJSONEntry = async (absoluteFilePath) => await (0, _intlayer_config_file.loadExternalFile)(absoluteFilePath, { logError: false });
19
+
20
+ //#endregion
21
+ exports.jsonCodec = jsonCodec;
22
+ exports.readJSONEntry = readJSONEntry;
23
+ //# sourceMappingURL=jsonCodec.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonCodec.cjs","names":[],"sources":["../../src/jsonCodec.ts"],"sourcesContent":["import { loadExternalFile } from '@intlayer/config/file';\nimport type { FormatCodec, SyncContent } from '@intlayer/engine/syncPluginKit';\n\n/**\n * JSON payload codec: standard JSON parsing, pretty-printed serialization\n * (2-space indentation, trailing newline).\n */\nexport const jsonCodec: FormatCodec = {\n parse: (raw) => JSON.parse(raw),\n serialize: (content) => `${JSON.stringify(content, null, 2)}\\n`,\n};\n\n/**\n * Read one JSON source through `loadExternalFile`, which also supports JSON5,\n * JSONC and transpiled JS/TS message files. Returns `undefined` for missing or\n * unreadable files so callers fall back to empty content.\n */\nexport const readJSONEntry = async (\n absoluteFilePath: string\n): Promise<SyncContent | undefined> =>\n await loadExternalFile(absoluteFilePath, { logError: false });\n"],"mappings":";;;;;;;;AAOA,MAAa,YAAyB;CACpC,QAAQ,QAAQ,KAAK,MAAM,GAAG;CAC9B,YAAY,YAAY,GAAG,KAAK,UAAU,SAAS,MAAM,CAAC,EAAE;AAC9D;;;;;;AAOA,MAAa,gBAAgB,OAC3B,qBAEA,kDAAuB,kBAAkB,EAAE,UAAU,MAAM,CAAC"}
@@ -1,100 +1,27 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
3
- const require_syncJSON = require('./syncJSON.cjs');
4
- let node_path = require("node:path");
5
- let _intlayer_config_file = require("@intlayer/config/file");
6
- let _intlayer_config_logger = require("@intlayer/config/logger");
7
- let _intlayer_config_utils = require("@intlayer/config/utils");
8
- let fast_glob = require("fast-glob");
9
- fast_glob = require_runtime.__toESM(fast_glob);
2
+ const require_jsonCodec = require('./jsonCodec.cjs');
3
+ let _intlayer_engine_syncPluginKit = require("@intlayer/engine/syncPluginKit");
10
4
 
11
5
  //#region src/loadJSON.ts
12
- const listMessages = async (source, configuration) => {
13
- const { system, internationalization } = configuration;
14
- const { baseDir } = system;
15
- const { locales } = internationalization;
16
- const result = {};
17
- for (const locale of locales) {
18
- const globPatternLocale = await (0, _intlayer_config_utils.parseFilePathPattern)(source, {
19
- key: "**",
20
- locale
21
- });
22
- const maskPatternLocale = await (0, _intlayer_config_utils.parseFilePathPattern)(source, {
23
- key: "{{__KEY__}}",
24
- locale
25
- });
26
- if (!globPatternLocale || !maskPatternLocale) continue;
27
- const files = await (0, fast_glob.default)(globPatternLocale.startsWith("./") ? globPatternLocale.slice(2) : globPatternLocale, { cwd: baseDir });
28
- for (const file of files) {
29
- const hasLocaleInMask = maskPatternLocale.includes("{{__LOCALE__}}");
30
- const hasKeyInMask = maskPatternLocale.includes("{{__KEY__}}");
31
- let key;
32
- let extractedLocale;
33
- if (hasLocaleInMask || hasKeyInMask) {
34
- const extraction = require_syncJSON.extractKeyAndLocaleFromPath(file, maskPatternLocale, locales, locale);
35
- if (!extraction) continue;
36
- key = extraction.key;
37
- extractedLocale = extraction.locale;
38
- } else {
39
- key = "index";
40
- extractedLocale = locale;
41
- }
42
- const absolutePath = (0, node_path.isAbsolute)(file) ? file : (0, node_path.resolve)(baseDir, file);
43
- const usedLocale = extractedLocale;
44
- if (!result[usedLocale]) result[usedLocale] = {};
45
- result[usedLocale][key] = absolutePath;
46
- }
47
- }
48
- return result;
49
- };
50
- const loadMessagePathMap = async (source, configuration) => {
51
- const messages = await listMessages(source, configuration);
52
- return Object.entries(messages).flatMap(([locale, keysRecord]) => Object.entries(keysRecord).map(([key, path]) => {
53
- return {
54
- path: (0, node_path.isAbsolute)(path) ? path : (0, node_path.resolve)(configuration.system.baseDir, path),
55
- locale,
56
- key
57
- };
58
- }));
59
- };
60
- const loadJSON = (options) => {
61
- const { location, priority, locale, format } = {
62
- location: "plugin",
63
- priority: 0,
64
- ...options
65
- };
66
- return {
67
- name: "load-json",
68
- loadDictionaries: async ({ configuration }) => {
69
- const appLogger = (0, _intlayer_config_logger.getAppLogger)(configuration);
70
- const dictionariesMap = await loadMessagePathMap(options.source, configuration);
71
- if (dictionariesMap.length === 0) appLogger(`No dictionaries found at locations matching source pattern: ${(0, _intlayer_config_logger.colorizePath)(await (0, _intlayer_config_utils.parseFilePathPattern)(options.source, {
72
- key: "{{key}}",
73
- locale: "{{locale}}"
74
- }))}`, { level: "warn" });
75
- const dictionaries = [];
76
- for (const { path, key, locale: entryLocale } of dictionariesMap) {
77
- const json = await (0, _intlayer_config_file.loadExternalFile)(path, { logError: false }) ?? {};
78
- const filePath = (0, node_path.relative)(configuration.system.baseDir, path);
79
- const entryUsedLocale = locale ?? entryLocale;
80
- const dictionary = {
81
- key,
82
- locale: entryUsedLocale,
83
- fill: filePath,
84
- format,
85
- localId: `${key}::${location}::${filePath}`,
86
- location,
87
- filled: entryUsedLocale !== configuration.internationalization.defaultLocale ? true : void 0,
88
- content: json,
89
- filePath,
90
- priority
91
- };
92
- dictionaries.push(dictionary);
93
- }
94
- return dictionaries;
95
- }
96
- };
97
- };
6
+ /**
7
+ * Read-only JSON ingestion plugin: loads JSON message files as dictionaries
8
+ * without ever writing back to them.
9
+ */
10
+ const loadJSON = (options) => (0, _intlayer_engine_syncPluginKit.createSyncPlugin)({
11
+ name: "load-json",
12
+ adapter: (0, _intlayer_engine_syncPluginKit.createFileAdapter)({
13
+ source: options.source,
14
+ codec: require_jsonCodec.jsonCodec,
15
+ readEntry: require_jsonCodec.readJSONEntry,
16
+ discovery: "inclusive"
17
+ }),
18
+ direction: "pull",
19
+ location: options.location ?? "plugin",
20
+ priority: options.priority ?? 0,
21
+ format: options.format,
22
+ splitKeys: options.splitKeys,
23
+ localeOverride: options.locale
24
+ });
98
25
 
99
26
  //#endregion
100
27
  exports.loadJSON = loadJSON;
@@ -1 +1 @@
1
- {"version":3,"file":"loadJSON.cjs","names":["extractKeyAndLocaleFromPath","sourcePattern"],"sources":["../../src/loadJSON.ts"],"sourcesContent":["import { isAbsolute, relative, resolve } from 'node:path';\nimport { loadExternalFile } from '@intlayer/config/file';\nimport { colorizePath, getAppLogger } from '@intlayer/config/logger';\nimport { parseFilePathPattern } from '@intlayer/config/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type {\n Dictionary,\n DictionaryFormat,\n LocalDictionaryId,\n} from '@intlayer/types/dictionary';\nimport type {\n FilePathPattern,\n FilePathPatternContext,\n} from '@intlayer/types/filePathPattern';\nimport type { Plugin } from '@intlayer/types/plugin';\nimport fg from 'fast-glob';\nimport { extractKeyAndLocaleFromPath } from './syncJSON';\n\ntype JSONContent = Record<string, any>;\n\ntype FilePath = string;\n\ntype MessagesRecord = Record<Locale, Record<Dictionary['key'], FilePath>>;\n\nconst listMessages = async (\n source: FilePathPattern,\n configuration: IntlayerConfig\n): Promise<MessagesRecord> => {\n const { system, internationalization } = configuration;\n\n const { baseDir } = system;\n const { locales } = internationalization;\n\n const result: MessagesRecord = {} as MessagesRecord;\n\n for (const locale of locales) {\n const globPatternLocale = await parseFilePathPattern(source, {\n key: '**',\n locale,\n } as any as FilePathPatternContext);\n\n const maskPatternLocale = await parseFilePathPattern(source, {\n key: '{{__KEY__}}',\n locale,\n } as any as FilePathPatternContext);\n\n if (!globPatternLocale || !maskPatternLocale) {\n continue;\n }\n\n const normalizedGlobPattern = globPatternLocale.startsWith('./')\n ? globPatternLocale.slice(2)\n : globPatternLocale;\n\n const files = await fg(normalizedGlobPattern, {\n cwd: baseDir,\n });\n\n for (const file of files) {\n // extractKeyAndLocaleFromPath requires at least one named capture group\n // ({{__LOCALE__}} or {{__KEY__}}) in the mask to return a non-null result.\n // When the mask is fully concrete (e.g. `messages_ICU/en.json` — the source\n // has {{locale}} but no {{key}}), no groups exist and it returns null.\n // In that case, fall back directly to the loop locale and key = 'index'.\n const hasLocaleInMask = maskPatternLocale.includes('{{__LOCALE__}}');\n const hasKeyInMask = maskPatternLocale.includes('{{__KEY__}}');\n\n let key: string;\n let extractedLocale: Locale;\n\n if (hasLocaleInMask || hasKeyInMask) {\n const extraction = extractKeyAndLocaleFromPath(\n file,\n maskPatternLocale,\n locales,\n locale\n );\n\n if (!extraction) {\n continue;\n }\n\n key = extraction.key;\n extractedLocale = extraction.locale;\n } else {\n // Mask has no placeholders — the file was found via a concrete locale\n // glob. Attribute it directly to the current loop locale.\n key = 'index';\n extractedLocale = locale;\n }\n\n const absolutePath = isAbsolute(file) ? file : resolve(baseDir, file);\n\n const usedLocale = extractedLocale as Locale;\n if (!result[usedLocale]) {\n result[usedLocale] = {};\n }\n\n result[usedLocale][key as Dictionary['key']] = absolutePath;\n }\n }\n\n // For the load plugin we only use actual discovered files; do not fabricate\n // missing locales or keys, since we don't write outputs.\n return result;\n};\n\ntype DictionariesMap = { path: string; locale: Locale; key: string }[];\n\nconst loadMessagePathMap = async (\n source: MessagesRecord | FilePathPattern,\n configuration: IntlayerConfig\n) => {\n const sourcePattern = source as FilePathPattern;\n const messages: MessagesRecord = await listMessages(\n sourcePattern,\n configuration\n );\n\n // Always include all discovered locales — loadJSON is read-only and should\n // ingest every locale file that exists, just like syncJSON does.\n const entries = Object.entries(messages) as [\n Locale,\n Record<Dictionary['key'], FilePath>,\n ][];\n\n const dictionariesPathMap: DictionariesMap = entries.flatMap(\n ([locale, keysRecord]) =>\n Object.entries(keysRecord).map(([key, path]) => {\n const absolutePath = isAbsolute(path)\n ? path\n : resolve(configuration.system.baseDir, path);\n\n return {\n path: absolutePath,\n locale,\n key,\n } as DictionariesMap[number];\n })\n );\n\n return dictionariesPathMap;\n};\n\ntype LoadJSONPluginOptions = {\n /**\n * The source of the plugin.\n * Is a function to build the source from the key and locale.\n *\n * @example\n * ```ts\n * loadJSON({\n * source: ({ key }) => `blog/${'**'}/${key}.i18n.json`,\n * })\n * ```\n */\n source: FilePathPattern;\n\n /**\n * Locale\n *\n * If not provided, the plugin will consider the default locale.\n *\n * @example\n * ```ts\n * loadJSON({\n * source: ({ key }) => `blog/${'**'}/${key}.i18n.json`,\n * locale: Locales.ENGLISH,\n * })\n * ```\n */\n locale?: Locale;\n\n /**\n * Because Intlayer transform the JSON files into Dictionary, we need to identify the plugin in the dictionary.\n * Used to identify the plugin in the dictionary.\n *\n * In the case you have multiple plugins, you can use this to identify the plugin in the dictionary.\n *\n * @example\n * ```ts\n * const config = {\n * plugins: [\n * loadJSON({\n * source: ({ key }) => `./resources/${key}.json`,\n * location: 'plugin-i18next',\n * }),\n * loadJSON({\n * source: ({ key }) => `./messages/${key}.json`,\n * location: 'plugin-next-intl',\n * }),\n * ]\n * }\n * ```\n */\n location?: string;\n\n /**\n * The priority of the dictionaries created by the plugin.\n *\n * In the case of conflicts with remote dictionaries, or .content files, the dictionary with the highest priority will override the other dictionaries.\n *\n * Default is -1. (.content file priority is 0)\n *\n */\n priority?: number;\n\n /**\n * The format of the dictionary content.\n *\n * @example\n * ```ts\n * loadJSON({\n * format: 'icu',\n * })\n * ```\n */\n format?: DictionaryFormat;\n};\n\nexport const loadJSON = (options: LoadJSONPluginOptions): Plugin => {\n const { location, priority, locale, format } = {\n location: 'plugin',\n priority: 0,\n ...options,\n } as const;\n\n return {\n name: 'load-json',\n\n loadDictionaries: async ({ configuration }) => {\n const appLogger = getAppLogger(configuration);\n const dictionariesMap: DictionariesMap = await loadMessagePathMap(\n options.source,\n configuration\n );\n\n if (dictionariesMap.length === 0) {\n const pattern = await parseFilePathPattern(options.source, {\n key: '{{key}}',\n locale: '{{locale}}',\n } as any as FilePathPatternContext);\n\n appLogger(\n `No dictionaries found at locations matching source pattern: ${colorizePath(pattern)}`,\n { level: 'warn' }\n );\n }\n\n const dictionaries: Dictionary[] = [];\n\n for (const { path, key, locale: entryLocale } of dictionariesMap) {\n // loadExternalFile swallows errors and returns undefined for missing files;\n // use ?? {} to guarantee a plain object regardless.\n const json: JSONContent =\n (await loadExternalFile(path, { logError: false })) ?? {};\n\n const filePath = relative(configuration.system.baseDir, path);\n\n // Use the per-entry locale discovered from the file path. If a fixed\n // locale override was provided, use it only as a fallback.\n const entryUsedLocale = (locale ?? entryLocale) as Locale;\n\n const dictionary: Dictionary = {\n key,\n locale: entryUsedLocale,\n fill: filePath,\n format,\n localId: `${key}::${location}::${filePath}` as LocalDictionaryId,\n location: location as Dictionary['location'],\n filled:\n entryUsedLocale !== configuration.internationalization.defaultLocale\n ? true\n : undefined,\n content: json,\n filePath,\n priority,\n };\n\n dictionaries.push(dictionary);\n }\n\n return dictionaries;\n },\n };\n};\n"],"mappings":";;;;;;;;;;;AAyBA,MAAM,eAAe,OACnB,QACA,kBAC4B;CAC5B,MAAM,EAAE,QAAQ,yBAAyB;CAEzC,MAAM,EAAE,YAAY;CACpB,MAAM,EAAE,YAAY;CAEpB,MAAM,SAAyB,EAAE;AAEjC,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,oBAAoB,uDAA2B,QAAQ;GAC3D,KAAK;GACL;GACD,CAAkC;EAEnC,MAAM,oBAAoB,uDAA2B,QAAQ;GAC3D,KAAK;GACL;GACD,CAAkC;AAEnC,MAAI,CAAC,qBAAqB,CAAC,kBACzB;EAOF,MAAM,QAAQ,6BAJgB,kBAAkB,WAAW,KAAK,GAC5D,kBAAkB,MAAM,EAAE,GAC1B,mBAE0C,EAC5C,KAAK,SACN,CAAC;AAEF,OAAK,MAAM,QAAQ,OAAO;GAMxB,MAAM,kBAAkB,kBAAkB,SAAS,iBAAiB;GACpE,MAAM,eAAe,kBAAkB,SAAS,cAAc;GAE9D,IAAI;GACJ,IAAI;AAEJ,OAAI,mBAAmB,cAAc;IACnC,MAAM,aAAaA,6CACjB,MACA,mBACA,SACA,OACD;AAED,QAAI,CAAC,WACH;AAGF,UAAM,WAAW;AACjB,sBAAkB,WAAW;UACxB;AAGL,UAAM;AACN,sBAAkB;;GAGpB,MAAM,yCAA0B,KAAK,GAAG,8BAAe,SAAS,KAAK;GAErE,MAAM,aAAa;AACnB,OAAI,CAAC,OAAO,YACV,QAAO,cAAc,EAAE;AAGzB,UAAO,YAAY,OAA4B;;;AAMnD,QAAO;;AAKT,MAAM,qBAAqB,OACzB,QACA,kBACG;CAEH,MAAM,WAA2B,MAAM,aACrCC,QACA,cACD;AAwBD,QApBgB,OAAO,QAAQ,SAKqB,CAAC,SAClD,CAAC,QAAQ,gBACR,OAAO,QAAQ,WAAW,CAAC,KAAK,CAAC,KAAK,UAAU;AAK9C,SAAO;GACL,gCAL8B,KAAK,GACjC,8BACQ,cAAc,OAAO,SAAS,KAAK;GAI7C;GACA;GACD;GACD,CAGoB;;AA+E5B,MAAa,YAAY,YAA2C;CAClE,MAAM,EAAE,UAAU,UAAU,QAAQ,WAAW;EAC7C,UAAU;EACV,UAAU;EACV,GAAG;EACJ;AAED,QAAO;EACL,MAAM;EAEN,kBAAkB,OAAO,EAAE,oBAAoB;GAC7C,MAAM,sDAAyB,cAAc;GAC7C,MAAM,kBAAmC,MAAM,mBAC7C,QAAQ,QACR,cACD;AAED,OAAI,gBAAgB,WAAW,EAM7B,WACE,yGAA4E,uDANnC,QAAQ,QAAQ;IACzD,KAAK;IACL,QAAQ;IACT,CAAkC,CAGmD,IACpF,EAAE,OAAO,QAAQ,CAClB;GAGH,MAAM,eAA6B,EAAE;AAErC,QAAK,MAAM,EAAE,MAAM,KAAK,QAAQ,iBAAiB,iBAAiB;IAGhE,MAAM,OACH,kDAAuB,MAAM,EAAE,UAAU,OAAO,CAAC,IAAK,EAAE;IAE3D,MAAM,mCAAoB,cAAc,OAAO,SAAS,KAAK;IAI7D,MAAM,kBAAmB,UAAU;IAEnC,MAAM,aAAyB;KAC7B;KACA,QAAQ;KACR,MAAM;KACN;KACA,SAAS,GAAG,IAAI,IAAI,SAAS,IAAI;KACvB;KACV,QACE,oBAAoB,cAAc,qBAAqB,gBACnD,OACA;KACN,SAAS;KACT;KACA;KACD;AAED,iBAAa,KAAK,WAAW;;AAG/B,UAAO;;EAEV"}
1
+ {"version":3,"file":"loadJSON.cjs","names":["jsonCodec","readJSONEntry"],"sources":["../../src/loadJSON.ts"],"sourcesContent":["import {\n createFileAdapter,\n createSyncPlugin,\n} from '@intlayer/engine/syncPluginKit';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { DictionaryFormat } from '@intlayer/types/dictionary';\nimport type { FilePathPattern } from '@intlayer/types/filePathPattern';\nimport type { Plugin } from '@intlayer/types/plugin';\nimport { jsonCodec, readJSONEntry } from './jsonCodec';\n\nexport type LoadJSONPluginOptions = {\n /**\n * The source of the plugin.\n * Is a function to build the source from the key and locale.\n *\n * @example\n * ```ts\n * loadJSON({\n * source: ({ key }) => `blog/${'**'}/${key}.i18n.json`,\n * })\n * ```\n */\n source: FilePathPattern;\n\n /**\n * Locale\n *\n * If not provided, the plugin will consider the default locale.\n *\n * @example\n * ```ts\n * loadJSON({\n * source: ({ key }) => `blog/${'**'}/${key}.i18n.json`,\n * locale: Locales.ENGLISH,\n * })\n * ```\n */\n locale?: Locale;\n\n /**\n * Because Intlayer transform the JSON files into Dictionary, we need to identify the plugin in the dictionary.\n * Used to identify the plugin in the dictionary.\n *\n * In the case you have multiple plugins, you can use this to identify the plugin in the dictionary.\n *\n * @example\n * ```ts\n * const config = {\n * plugins: [\n * loadJSON({\n * source: ({ key }) => `./resources/${key}.json`,\n * location: 'plugin-i18next',\n * }),\n * loadJSON({\n * source: ({ key }) => `./messages/${key}.json`,\n * location: 'plugin-next-intl',\n * }),\n * ]\n * }\n * ```\n */\n location?: string;\n\n /**\n * The priority of the dictionaries created by the plugin.\n *\n * In the case of conflicts with remote dictionaries, or .content files, the dictionary with the highest priority will override the other dictionaries.\n *\n * Default is -1. (.content file priority is 0)\n *\n */\n priority?: number;\n\n /**\n * The format of the dictionary content.\n *\n * @example\n * ```ts\n * loadJSON({\n * format: 'icu',\n * })\n * ```\n */\n format?: DictionaryFormat;\n\n /**\n * Whether each top-level key of the JSON file should become its own\n * dictionary (keyed by that top-level key) instead of a single dictionary\n * holding the whole file.\n *\n * This matches the namespace model of libraries such as `next-intl` /\n * `react-intl`, where a single `messages/{locale}.json` file groups several\n * namespaces by its first-level keys.\n *\n * When omitted, it is auto-detected: the file is split when the `source`\n * pattern has no `{{key}}` segment, and kept as a single dictionary otherwise.\n */\n splitKeys?: boolean;\n};\n\n/**\n * Read-only JSON ingestion plugin: loads JSON message files as dictionaries\n * without ever writing back to them.\n */\nexport const loadJSON = (options: LoadJSONPluginOptions): Plugin =>\n createSyncPlugin({\n name: 'load-json',\n adapter: createFileAdapter({\n source: options.source,\n codec: jsonCodec,\n readEntry: readJSONEntry,\n discovery: 'inclusive',\n }),\n direction: 'pull',\n location: options.location ?? 'plugin',\n priority: options.priority ?? 0,\n format: options.format,\n splitKeys: options.splitKeys,\n localeOverride: options.locale,\n });\n"],"mappings":";;;;;;;;;AAwGA,MAAa,YAAY,iEACN;CACf,MAAM;CACN,+DAA2B;EACzB,QAAQ,QAAQ;EAChB,OAAOA;EACP,WAAWC;EACX,WAAW;CACb,CAAC;CACD,WAAW;CACX,UAAU,QAAQ,YAAY;CAC9B,UAAU,QAAQ,YAAY;CAC9B,QAAQ,QAAQ;CAChB,WAAW,QAAQ;CACnB,gBAAgB,QAAQ;AAC1B,CAAC"}
@@ -1,179 +1,37 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
3
- let node_path = require("node:path");
4
- let _intlayer_config_file = require("@intlayer/config/file");
5
- let _intlayer_config_logger = require("@intlayer/config/logger");
2
+ const require_jsonCodec = require('./jsonCodec.cjs');
3
+ let _intlayer_engine_syncPluginKit = require("@intlayer/engine/syncPluginKit");
6
4
  let _intlayer_config_utils = require("@intlayer/config/utils");
7
- let fast_glob = require("fast-glob");
8
- fast_glob = require_runtime.__toESM(fast_glob);
9
- let node_fs_promises = require("node:fs/promises");
10
5
 
11
6
  //#region src/syncJSON.ts
12
- const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13
- const extractKeyAndLocaleFromPath = (filePath, maskPattern, locales, defaultLocale) => {
14
- const keyPlaceholder = "{{__KEY__}}";
15
- const localePlaceholder = "{{__LOCALE__}}";
16
- const normalize = (path) => path.startsWith("./") ? path.slice(2) : path;
17
- const normalizedFilePath = normalize(filePath);
18
- const normalizedMask = normalize(maskPattern);
19
- const localesAlternation = locales.join("|");
20
- let regexStr = `^${escapeRegex(normalizedMask)}$`;
21
- regexStr = regexStr.replace(/\\\*\\\*/g, ".*");
22
- regexStr = regexStr.replace(/\\\*/g, "[^/]*");
23
- regexStr = regexStr.replace(escapeRegex(localePlaceholder), `(?<locale>${localesAlternation})`);
24
- if (normalizedMask.includes(keyPlaceholder)) regexStr = regexStr.replace(escapeRegex(keyPlaceholder), "(?<key>.+)");
25
- const match = new RegExp(regexStr).exec(normalizedFilePath);
26
- if (!match?.groups) return null;
27
- let locale = match.groups.locale;
28
- let key = match.groups.key ?? "index";
29
- if (typeof key === "undefined") key = "index";
30
- if (typeof locale === "undefined") locale = defaultLocale;
31
- return {
32
- key,
33
- locale
34
- };
35
- };
36
- const listMessages = async (source, configuration) => {
37
- const { system, internationalization } = configuration;
38
- const { baseDir } = system;
39
- const { locales } = internationalization;
40
- const result = {};
41
- for (const locale of locales) {
42
- const globPatternLocale = await (0, _intlayer_config_utils.parseFilePathPattern)(source, {
43
- key: "**",
44
- locale
45
- });
46
- const maskPatternLocale = await (0, _intlayer_config_utils.parseFilePathPattern)(source, {
47
- key: "{{__KEY__}}",
48
- locale
49
- });
50
- if (!globPatternLocale || !maskPatternLocale) continue;
51
- const files = await (0, fast_glob.default)(globPatternLocale.startsWith("./") ? globPatternLocale.slice(2) : globPatternLocale, { cwd: baseDir });
52
- for (const file of files) {
53
- const extraction = extractKeyAndLocaleFromPath(file, maskPatternLocale, locales, locale);
54
- if (!extraction) continue;
55
- const { key, locale: extractedLocale } = extraction;
56
- const expectedPath = await (0, _intlayer_config_utils.parseFilePathPattern)(source, {
57
- key,
58
- locale: extractedLocale
59
- });
60
- const absoluteFoundPath = (0, node_path.isAbsolute)(file) ? file : (0, node_path.resolve)(baseDir, file);
61
- if (absoluteFoundPath !== ((0, node_path.isAbsolute)(expectedPath) ? expectedPath : (0, node_path.resolve)(baseDir, expectedPath))) continue;
62
- const usedLocale = extractedLocale;
63
- if (!result[usedLocale]) result[usedLocale] = {};
64
- result[usedLocale][key] = absoluteFoundPath;
65
- }
66
- }
67
- const hasKeyInMask = (await (0, _intlayer_config_utils.parseFilePathPattern)(source, {
68
- key: "{{__KEY__}}",
69
- locale: locales[0]
70
- })).includes("{{__KEY__}}");
71
- const discoveredKeys = /* @__PURE__ */ new Set();
72
- for (const locale of Object.keys(result)) for (const key of Object.keys(result[locale] ?? {})) discoveredKeys.add(key);
73
- if (!hasKeyInMask) discoveredKeys.add("index");
74
- const keysToEnsure = discoveredKeys.size > 0 ? Array.from(discoveredKeys) : [];
75
- for (const locale of locales) {
76
- if (!result[locale]) result[locale] = {};
77
- for (const key of keysToEnsure) if (!result[locale][key]) {
78
- const builtPath = await (0, _intlayer_config_utils.parseFilePathPattern)(source, {
79
- key,
80
- locale
81
- });
82
- const absoluteBuiltPath = (0, node_path.isAbsolute)(builtPath) ? builtPath : (0, node_path.resolve)(baseDir, builtPath);
83
- result[locale][key] = absoluteBuiltPath;
84
- }
85
- }
86
- return result;
87
- };
88
- const loadMessagePathMap = async (source, configuration) => {
89
- const messages = await listMessages(source, configuration);
90
- return Object.entries(messages).flatMap(([locale, keysRecord]) => Object.entries(keysRecord).map(([key, path]) => {
91
- return {
92
- path: (0, node_path.isAbsolute)(path) ? path : (0, node_path.resolve)(configuration.system.baseDir, path),
93
- locale,
94
- key
95
- };
96
- }));
97
- };
7
+ /**
8
+ * Two-way JSON synchronization plugin: ingests JSON message files as
9
+ * dictionaries and writes the merged build output back to the same files.
10
+ */
98
11
  const syncJSON = async (options) => {
99
- const { location, priority, format } = {
100
- location: `sync-json::${await (0, _intlayer_config_utils.parseFilePathPattern)(options.source, {
101
- key: "{{key}}",
102
- locale: "{{locale}}"
103
- })}`,
104
- priority: 0,
105
- ...options
106
- };
107
- return {
12
+ const defaultLocation = `sync-json::${await (0, _intlayer_config_utils.parseFilePathPattern)(options.source, (0, _intlayer_engine_syncPluginKit.buildFilePathPatternContext)("{{key}}", "{{locale}}"))}`;
13
+ return (0, _intlayer_engine_syncPluginKit.createSyncPlugin)({
108
14
  name: "sync-json",
109
- loadDictionaries: async ({ configuration }) => {
110
- const appLogger = (0, _intlayer_config_logger.getAppLogger)(configuration);
111
- const dictionariesMap = await loadMessagePathMap(options.source, configuration);
112
- if (dictionariesMap.length === 0) appLogger(`[sync-json] No dictionaries found at locations matching source pattern: ${(0, _intlayer_config_logger.colorizePath)(await (0, _intlayer_config_utils.parseFilePathPattern)(options.source, {
113
- key: "{{key}}",
114
- locale: "{{locale}}"
115
- }))}`, { level: "warn" });
116
- let fill = await (0, _intlayer_config_utils.parseFilePathPattern)(options.source, {
117
- key: "{{key}}",
118
- locale: "{{locale}}"
119
- });
120
- if (fill) fill = (0, node_path.relative)(configuration.system.baseDir, (0, node_path.resolve)(configuration.system.baseDir, fill));
121
- const dictionaries = [];
122
- for (const { locale, path, key } of dictionariesMap) {
123
- const json = await (0, _intlayer_config_file.loadExternalFile)(path, { logError: false }) ?? {};
124
- const filePath = (0, node_path.relative)(configuration.system.baseDir, path);
125
- const dictionary = {
126
- key,
127
- locale,
128
- fill,
129
- format,
130
- localId: `${key}::${location}::${filePath}`,
131
- location,
132
- filled: locale !== configuration.internationalization.defaultLocale ? true : void 0,
133
- content: json,
134
- filePath,
135
- priority
136
- };
137
- dictionaries.push(dictionary);
138
- }
139
- return dictionaries;
140
- },
141
- formatOutput: async ({ dictionary, configuration }) => {
142
- const { formatDictionaryOutput } = await import("@intlayer/chokidar/build");
143
- if (!dictionary.filePath || !dictionary.locale) return dictionary;
144
- const builderPath = await (0, _intlayer_config_utils.parseFilePathPattern)(options.source, {
145
- key: dictionary.key,
146
- locale: dictionary.locale
147
- });
148
- if ((0, node_path.resolve)(configuration.system.baseDir, builderPath) !== (0, node_path.resolve)(configuration.system.baseDir, dictionary.filePath)) return dictionary;
149
- return formatDictionaryOutput(dictionary, format).content;
150
- },
151
- afterBuild: async ({ dictionaries, configuration }) => {
152
- const { getPerLocaleDictionary } = await import("@intlayer/core/plugins");
153
- const { parallelize } = await import("@intlayer/chokidar/utils");
154
- const { formatDictionaryOutput } = await import("@intlayer/chokidar/build");
155
- const { locales } = configuration.internationalization;
156
- await parallelize(Object.entries(dictionaries.mergedDictionaries).flatMap(([key, dictionary]) => locales.map((locale) => ({
157
- key,
158
- dictionary: dictionary.dictionary,
159
- locale
160
- }))), async ({ key, dictionary, locale }) => {
161
- if (dictionary.location !== location) return;
162
- const builderPath = await (0, _intlayer_config_utils.parseFilePathPattern)(options.source, {
163
- key,
164
- locale
165
- });
166
- const formattedOutput = formatDictionaryOutput(getPerLocaleDictionary(dictionary, locale), format);
167
- const content = JSON.parse(JSON.stringify(formattedOutput.content));
168
- if (typeof content === "undefined" || typeof content === "object" && Object.keys(content).length === 0) return;
169
- await (0, node_fs_promises.mkdir)((0, node_path.dirname)(builderPath), { recursive: true });
170
- await (0, node_fs_promises.writeFile)(builderPath, `${JSON.stringify(content, null, 2)}\n`, "utf-8");
171
- });
172
- }
173
- };
15
+ adapter: (0, _intlayer_engine_syncPluginKit.createFileAdapter)({
16
+ source: options.source,
17
+ codec: require_jsonCodec.jsonCodec,
18
+ readEntry: require_jsonCodec.readJSONEntry,
19
+ discovery: "strict"
20
+ }),
21
+ direction: "both",
22
+ location: options.location ?? defaultLocation,
23
+ priority: options.priority ?? 0,
24
+ format: options.format,
25
+ splitKeys: options.splitKeys
26
+ });
174
27
  };
175
28
 
176
29
  //#endregion
177
- exports.extractKeyAndLocaleFromPath = extractKeyAndLocaleFromPath;
30
+ Object.defineProperty(exports, 'extractKeyAndLocaleFromPath', {
31
+ enumerable: true,
32
+ get: function () {
33
+ return _intlayer_engine_syncPluginKit.extractKeyAndLocaleFromPath;
34
+ }
35
+ });
178
36
  exports.syncJSON = syncJSON;
179
37
  //# sourceMappingURL=syncJSON.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"syncJSON.cjs","names":[],"sources":["../../src/syncJSON.ts"],"sourcesContent":["import { mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, relative, resolve } from 'node:path';\nimport { loadExternalFile } from '@intlayer/config/file';\nimport { colorizePath, getAppLogger } from '@intlayer/config/logger';\nimport { parseFilePathPattern } from '@intlayer/config/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type {\n Dictionary,\n DictionaryFormat,\n DictionaryLocation,\n LocalDictionaryId,\n} from '@intlayer/types/dictionary';\nimport type {\n FilePathPattern,\n FilePathPatternContext,\n} from '@intlayer/types/filePathPattern';\nimport type { Plugin } from '@intlayer/types/plugin';\nimport fg from 'fast-glob';\n\ntype JSONContent = Record<string, any>;\n\ntype FilePath = string;\n\ntype MessagesRecord = Record<Locale, Record<Dictionary['key'], FilePath>>;\n\nconst escapeRegex = (str: string) => str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\nexport const extractKeyAndLocaleFromPath = (\n filePath: string,\n maskPattern: string,\n locales: Locale[],\n defaultLocale: Locale\n): { key: string; locale: Locale } | null => {\n const keyPlaceholder = '{{__KEY__}}';\n const localePlaceholder = '{{__LOCALE__}}';\n\n // fast-glob strips leading \"./\" from returned paths; normalize both sides\n const normalize = (path: string) =>\n path.startsWith('./') ? path.slice(2) : path;\n\n const normalizedFilePath = normalize(filePath);\n const normalizedMask = normalize(maskPattern);\n\n const localesAlternation = locales.join('|');\n\n // Escape special regex chars, then convert glob wildcards to regex equivalents.\n // Must replace ** before * to avoid double-replacing.\n let regexStr = `^${escapeRegex(normalizedMask)}$`;\n regexStr = regexStr.replace(/\\\\\\*\\\\\\*/g, '.*'); // ** → match any path segments\n regexStr = regexStr.replace(/\\\\\\*/g, '[^/]*'); // * → match within a single segment\n\n regexStr = regexStr.replace(\n escapeRegex(localePlaceholder),\n `(?<locale>${localesAlternation})`\n );\n\n if (normalizedMask.includes(keyPlaceholder)) {\n regexStr = regexStr.replace(escapeRegex(keyPlaceholder), '(?<key>.+)');\n }\n\n const maskRegex = new RegExp(regexStr);\n const match = maskRegex.exec(normalizedFilePath);\n\n if (!match?.groups) {\n return null;\n }\n\n let locale = match.groups.locale as Locale | undefined;\n let key = (match.groups.key as string | undefined) ?? 'index';\n\n if (typeof key === 'undefined') {\n key = 'index';\n }\n\n if (typeof locale === 'undefined') {\n locale = defaultLocale;\n }\n\n return {\n key,\n locale,\n };\n};\n\nconst listMessages = async (\n source: FilePathPattern,\n configuration: IntlayerConfig\n): Promise<MessagesRecord> => {\n const { system, internationalization } = configuration;\n\n const { baseDir } = system;\n const { locales } = internationalization;\n\n const result: MessagesRecord = {} as MessagesRecord;\n\n for (const locale of locales) {\n const globPatternLocale = await parseFilePathPattern(source, {\n key: '**',\n locale,\n } as any as FilePathPatternContext);\n\n const maskPatternLocale = await parseFilePathPattern(source, {\n key: '{{__KEY__}}',\n locale,\n } as any as FilePathPatternContext);\n\n if (!globPatternLocale || !maskPatternLocale) {\n continue;\n }\n\n const normalizedGlobPattern = globPatternLocale.startsWith('./')\n ? globPatternLocale.slice(2)\n : globPatternLocale;\n\n const files = await fg(normalizedGlobPattern, {\n cwd: baseDir,\n });\n\n for (const file of files) {\n const extraction = extractKeyAndLocaleFromPath(\n file,\n maskPatternLocale,\n locales,\n locale\n );\n\n if (!extraction) {\n continue;\n }\n\n const { key, locale: extractedLocale } = extraction;\n\n // Generate what the path SHOULD be for this key/locale using the current builder\n const expectedPath = await parseFilePathPattern(source, {\n key,\n locale: extractedLocale,\n } as any as FilePathPatternContext);\n\n // Resolve both to absolute paths to ensure safe comparison\n const absoluteFoundPath = isAbsolute(file)\n ? file\n : resolve(baseDir, file);\n const absoluteExpectedPath = isAbsolute(expectedPath)\n ? expectedPath\n : resolve(baseDir, expectedPath);\n\n // If the file found doesn't exactly match the file expected, it belongs to another plugin/structure\n if (absoluteFoundPath !== absoluteExpectedPath) {\n continue;\n }\n\n const usedLocale = extractedLocale as Locale;\n if (!result[usedLocale]) {\n result[usedLocale] = {};\n }\n\n result[usedLocale][key as Dictionary['key']] = absoluteFoundPath;\n }\n }\n\n // Ensure all declared locales are present even if the file doesn't exist yet\n const maskWithKey = await parseFilePathPattern(source, {\n key: '{{__KEY__}}',\n locale: locales[0],\n } as any as FilePathPatternContext);\n\n const hasKeyInMask = maskWithKey.includes('{{__KEY__}}');\n const discoveredKeys = new Set<string>();\n\n for (const locale of Object.keys(result)) {\n for (const key of Object.keys(result[locale as Locale] ?? {})) {\n discoveredKeys.add(key);\n }\n }\n\n if (!hasKeyInMask) {\n discoveredKeys.add('index');\n }\n\n const keysToEnsure =\n discoveredKeys.size > 0 ? Array.from(discoveredKeys) : [];\n\n for (const locale of locales) {\n if (!result[locale]) {\n result[locale] = {} as Record<Dictionary['key'], FilePath>;\n }\n\n for (const key of keysToEnsure) {\n if (!result[locale][key as Dictionary['key']]) {\n const builtPath = await parseFilePathPattern(source, {\n key,\n locale,\n } as any as FilePathPatternContext);\n const absoluteBuiltPath = isAbsolute(builtPath)\n ? builtPath\n : resolve(baseDir, builtPath);\n\n result[locale][key as Dictionary['key']] = absoluteBuiltPath;\n }\n }\n }\n\n return result;\n};\n\ntype DictionariesMap = { path: string; locale: Locale; key: string }[];\n\nconst loadMessagePathMap = async (\n source: MessagesRecord | FilePathPattern,\n configuration: IntlayerConfig\n) => {\n const messages: MessagesRecord = await listMessages(\n source as FilePathPattern,\n configuration\n );\n\n const dictionariesPathMap: DictionariesMap = Object.entries(messages).flatMap(\n ([locale, keysRecord]) =>\n Object.entries(keysRecord).map(([key, path]) => {\n const absolutePath = isAbsolute(path)\n ? path\n : resolve(configuration.system.baseDir, path);\n\n return {\n path: absolutePath,\n locale,\n key,\n } as DictionariesMap[number];\n })\n );\n\n return dictionariesPathMap;\n};\n\ntype SyncJSONPluginOptions = {\n /**\n * The source of the plugin.\n * Is a function to build the source from the key and locale.\n *\n * ```ts\n * syncJSON({\n * source: ({ key, locale }) => `./messages/${locale}/${key}.json`\n * })\n * ```\n */\n source: FilePathPattern;\n\n /**\n * Because Intlayer transform the JSON files into Dictionary, we need to identify the plugin in the dictionary.\n * Used to identify the plugin in the dictionary.\n *\n * In the case you have multiple plugins, you can use this to identify the plugin in the dictionary.\n *\n * ```ts\n * // Example usage:\n * const config = {\n * plugins: [\n * syncJSON({\n * source: ({ key, locale }) => `./resources/${locale}/${key}.json`,\n * location: 'plugin-i18next',\n * }),\n * syncJSON({\n * source: ({ key, locale }) => `./messages/${locale}/${key}.json`,\n * location: 'plugin-next-intl',\n * }),\n * ]\n * }\n * ```\n */\n location?: DictionaryLocation | (string & {});\n\n /**\n * The priority of the dictionaries created by the plugin.\n *\n * In the case of conflicts with remote dictionaries, or .content files, the dictionary with the highest priority will override the other dictionaries.\n *\n * Default is -1. (.content file priority is 0)\n *\n */\n priority?: number;\n\n /**\n * The format of the dictionaries created by the plugin.\n *\n * Default: 'intlayer'\n *\n * The format of the dictionaries created by the plugin.\n */\n format?: DictionaryFormat;\n};\n\nexport const syncJSON = async (\n options: SyncJSONPluginOptions\n): Promise<Plugin> => {\n // Generate a unique default location based on the source pattern.\n // This ensures that if you have multiple plugins, they don't share the same 'plugin' ID.\n const patternMarker = await parseFilePathPattern(options.source, {\n key: '{{key}}',\n locale: '{{locale}}',\n } as any as FilePathPatternContext);\n const defaultLocation = `sync-json::${patternMarker}`;\n\n const { location, priority, format } = {\n location: defaultLocation,\n priority: 0,\n ...options,\n };\n\n return {\n name: 'sync-json',\n\n loadDictionaries: async ({ configuration }) => {\n const appLogger = getAppLogger(configuration);\n const dictionariesMap: DictionariesMap = await loadMessagePathMap(\n options.source,\n configuration\n );\n\n if (dictionariesMap.length === 0) {\n const pattern = await parseFilePathPattern(options.source, {\n key: '{{key}}',\n locale: '{{locale}}',\n } as any as FilePathPatternContext);\n\n appLogger(\n `[sync-json] No dictionaries found at locations matching source pattern: ${colorizePath(pattern)}`,\n { level: 'warn' }\n );\n }\n\n let fill: string = await parseFilePathPattern(options.source, {\n key: '{{key}}',\n locale: '{{locale}}',\n } as any as FilePathPatternContext);\n\n if (fill) {\n fill = relative(\n configuration.system.baseDir,\n resolve(configuration.system.baseDir, fill)\n );\n }\n\n const dictionaries: Dictionary[] = [];\n\n for (const { locale, path, key } of dictionariesMap) {\n // loadExternalFile swallows errors and returns undefined for missing files;\n // the try/catch does not help here — use ?? {} to guarantee a plain object.\n const json: JSONContent =\n (await loadExternalFile(path, { logError: false })) ?? {};\n\n const filePath = relative(configuration.system.baseDir, path);\n\n const dictionary: Dictionary = {\n key,\n locale,\n fill,\n format,\n localId: `${key}::${location}::${filePath}` as LocalDictionaryId,\n location: location as Dictionary['location'],\n filled:\n locale !== configuration.internationalization.defaultLocale\n ? true\n : undefined,\n content: json,\n filePath,\n priority,\n };\n\n dictionaries.push(dictionary);\n }\n\n return dictionaries;\n },\n\n formatOutput: async ({ dictionary, configuration }) => {\n // Lazy import intlayer modules to avoid circular dependencies\n const { formatDictionaryOutput } = await import(\n '@intlayer/chokidar/build'\n );\n\n if (!dictionary.filePath || !dictionary.locale) return dictionary;\n\n const builderPath = await parseFilePathPattern(options.source, {\n key: dictionary.key,\n locale: dictionary.locale,\n } as FilePathPatternContext);\n\n // Verification to ensure we are formatting the correct file\n if (\n resolve(configuration.system.baseDir, builderPath) !==\n resolve(configuration.system.baseDir, dictionary.filePath)\n ) {\n return dictionary;\n }\n\n const formattedOutput = formatDictionaryOutput(dictionary, format);\n\n return formattedOutput.content;\n },\n\n afterBuild: async ({ dictionaries, configuration }) => {\n // Lazy import intlayer modules to avoid circular dependencies\n const { getPerLocaleDictionary } = await import('@intlayer/core/plugins');\n const { parallelize } = await import('@intlayer/chokidar/utils');\n const { formatDictionaryOutput } = await import(\n '@intlayer/chokidar/build'\n );\n\n const { locales } = configuration.internationalization;\n\n type RecordList = {\n key: string;\n dictionary: Dictionary;\n locale: Locale;\n };\n\n // We get all dictionaries, but we need to filter them\n const recordList: RecordList[] = Object.entries(\n dictionaries.mergedDictionaries\n ).flatMap(([key, dictionary]) =>\n locales.map((locale) => ({\n key,\n dictionary: dictionary.dictionary as Dictionary,\n locale,\n }))\n );\n\n await parallelize(recordList, async ({ key, dictionary, locale }) => {\n // Only process dictionaries that belong to THIS plugin instance.\n if (dictionary.location !== location) {\n return;\n }\n\n const builderPath = await parseFilePathPattern(options.source, {\n key,\n locale,\n } as any as FilePathPatternContext);\n\n const localizedDictionary = getPerLocaleDictionary(dictionary, locale);\n\n const formattedOutput = formatDictionaryOutput(\n localizedDictionary,\n format\n );\n\n const content = JSON.parse(JSON.stringify(formattedOutput.content));\n\n if (\n typeof content === 'undefined' ||\n (typeof content === 'object' &&\n Object.keys(content as Record<string, unknown>).length === 0)\n ) {\n return;\n }\n\n await mkdir(dirname(builderPath), { recursive: true });\n\n const stringContent = JSON.stringify(content, null, 2);\n\n await writeFile(builderPath, `${stringContent}\\n`, 'utf-8');\n });\n },\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"syncJSON.cjs","names":["jsonCodec","readJSONEntry"],"sources":["../../src/syncJSON.ts"],"sourcesContent":["import { parseFilePathPattern } from '@intlayer/config/utils';\nimport {\n buildFilePathPatternContext,\n createFileAdapter,\n createSyncPlugin,\n} from '@intlayer/engine/syncPluginKit';\nimport type {\n DictionaryFormat,\n DictionaryLocation,\n} from '@intlayer/types/dictionary';\nimport type { FilePathPattern } from '@intlayer/types/filePathPattern';\nimport type { Plugin } from '@intlayer/types/plugin';\nimport { jsonCodec, readJSONEntry } from './jsonCodec';\n\n// Re-exported for backward compatibility: this helper used to live here.\nexport { extractKeyAndLocaleFromPath } from '@intlayer/engine/syncPluginKit';\n\nexport type SyncJSONPluginOptions = {\n /**\n * The source of the plugin.\n * Is a function to build the source from the key and locale.\n *\n * ```ts\n * syncJSON({\n * source: ({ key, locale }) => `./messages/${locale}/${key}.json`\n * })\n * ```\n */\n source: FilePathPattern;\n\n /**\n * Because Intlayer transform the JSON files into Dictionary, we need to identify the plugin in the dictionary.\n * Used to identify the plugin in the dictionary.\n *\n * In the case you have multiple plugins, you can use this to identify the plugin in the dictionary.\n *\n * ```ts\n * // Example usage:\n * const config = {\n * plugins: [\n * syncJSON({\n * source: ({ key, locale }) => `./resources/${locale}/${key}.json`,\n * location: 'plugin-i18next',\n * }),\n * syncJSON({\n * source: ({ key, locale }) => `./messages/${locale}/${key}.json`,\n * location: 'plugin-next-intl',\n * }),\n * ]\n * }\n * ```\n */\n location?: DictionaryLocation | (string & {});\n\n /**\n * The priority of the dictionaries created by the plugin.\n *\n * In the case of conflicts with remote dictionaries, or .content files, the dictionary with the highest priority will override the other dictionaries.\n *\n * Default is -1. (.content file priority is 0)\n *\n */\n priority?: number;\n\n /**\n * The format of the dictionaries created by the plugin.\n *\n * Default: 'intlayer'\n *\n * The format of the dictionaries created by the plugin.\n */\n format?: DictionaryFormat;\n\n /**\n * Whether each top-level key of the JSON file should become its own\n * dictionary (keyed by that top-level key) instead of a single dictionary\n * holding the whole file.\n *\n * This matches the namespace model of libraries such as `next-intl` /\n * `react-intl`, where a single `messages/{locale}.json` file groups several\n * namespaces by its first-level keys and each namespace is addressed\n * independently (e.g. `useTranslations('Hero')` → dictionary `Hero`).\n *\n * When omitted, it is auto-detected: the file is split when the `source`\n * pattern has no `{{key}}` segment (i.e. one file holds every namespace),\n * and kept as a single dictionary otherwise (one file per key).\n *\n * @example\n * ```ts\n * // messages/en.json → dictionaries: Hero, Nav, About, …\n * syncJSON({\n * source: ({ locale }) => `./messages/${locale}.json`,\n * splitKeys: true,\n * })\n * ```\n */\n splitKeys?: boolean;\n};\n\n/**\n * Two-way JSON synchronization plugin: ingests JSON message files as\n * dictionaries and writes the merged build output back to the same files.\n */\nexport const syncJSON = async (\n options: SyncJSONPluginOptions\n): Promise<Plugin> => {\n // Generate a unique default location based on the source pattern.\n // This ensures that if you have multiple plugins, they don't share the same 'plugin' ID.\n const patternMarker = await parseFilePathPattern(\n options.source,\n buildFilePathPatternContext('{{key}}', '{{locale}}')\n );\n const defaultLocation = `sync-json::${patternMarker}`;\n\n return createSyncPlugin({\n name: 'sync-json',\n adapter: createFileAdapter({\n source: options.source,\n codec: jsonCodec,\n readEntry: readJSONEntry,\n discovery: 'strict',\n }),\n direction: 'both',\n location: options.location ?? defaultLocation,\n priority: options.priority ?? 0,\n format: options.format,\n // Auto-detected by the file adapter when omitted: split when the source\n // pattern has no `{{key}}` segment (one file holds every namespace).\n splitKeys: options.splitKeys,\n });\n};\n"],"mappings":";;;;;;;;;;AAuGA,MAAa,WAAW,OACtB,YACoB;CAOpB,MAAM,kBAAkB,cAAc,uDAHpC,QAAQ,wEACoB,WAAW,YAAY,CACrD;CAGA,4DAAwB;EACtB,MAAM;EACN,+DAA2B;GACzB,QAAQ,QAAQ;GAChB,OAAOA;GACP,WAAWC;GACX,WAAW;EACb,CAAC;EACD,WAAW;EACX,UAAU,QAAQ,YAAY;EAC9B,UAAU,QAAQ,YAAY;EAC9B,QAAQ,QAAQ;EAGhB,WAAW,QAAQ;CACrB,CAAC;AACH"}
@@ -1,4 +1,5 @@
1
- import { extractKeyAndLocaleFromPath, syncJSON } from "./syncJSON.mjs";
1
+ import { jsonCodec, readJSONEntry } from "./jsonCodec.mjs";
2
2
  import { loadJSON } from "./loadJSON.mjs";
3
+ import { extractKeyAndLocaleFromPath, syncJSON } from "./syncJSON.mjs";
3
4
 
4
- export { extractKeyAndLocaleFromPath, loadJSON, syncJSON };
5
+ export { extractKeyAndLocaleFromPath, jsonCodec, loadJSON, readJSONEntry, syncJSON };
@@ -0,0 +1,21 @@
1
+ import { loadExternalFile } from "@intlayer/config/file";
2
+
3
+ //#region src/jsonCodec.ts
4
+ /**
5
+ * JSON payload codec: standard JSON parsing, pretty-printed serialization
6
+ * (2-space indentation, trailing newline).
7
+ */
8
+ const jsonCodec = {
9
+ parse: (raw) => JSON.parse(raw),
10
+ serialize: (content) => `${JSON.stringify(content, null, 2)}\n`
11
+ };
12
+ /**
13
+ * Read one JSON source through `loadExternalFile`, which also supports JSON5,
14
+ * JSONC and transpiled JS/TS message files. Returns `undefined` for missing or
15
+ * unreadable files so callers fall back to empty content.
16
+ */
17
+ const readJSONEntry = async (absoluteFilePath) => await loadExternalFile(absoluteFilePath, { logError: false });
18
+
19
+ //#endregion
20
+ export { jsonCodec, readJSONEntry };
21
+ //# sourceMappingURL=jsonCodec.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonCodec.mjs","names":[],"sources":["../../src/jsonCodec.ts"],"sourcesContent":["import { loadExternalFile } from '@intlayer/config/file';\nimport type { FormatCodec, SyncContent } from '@intlayer/engine/syncPluginKit';\n\n/**\n * JSON payload codec: standard JSON parsing, pretty-printed serialization\n * (2-space indentation, trailing newline).\n */\nexport const jsonCodec: FormatCodec = {\n parse: (raw) => JSON.parse(raw),\n serialize: (content) => `${JSON.stringify(content, null, 2)}\\n`,\n};\n\n/**\n * Read one JSON source through `loadExternalFile`, which also supports JSON5,\n * JSONC and transpiled JS/TS message files. Returns `undefined` for missing or\n * unreadable files so callers fall back to empty content.\n */\nexport const readJSONEntry = async (\n absoluteFilePath: string\n): Promise<SyncContent | undefined> =>\n await loadExternalFile(absoluteFilePath, { logError: false });\n"],"mappings":";;;;;;;AAOA,MAAa,YAAyB;CACpC,QAAQ,QAAQ,KAAK,MAAM,GAAG;CAC9B,YAAY,YAAY,GAAG,KAAK,UAAU,SAAS,MAAM,CAAC,EAAE;AAC9D;;;;;;AAOA,MAAa,gBAAgB,OAC3B,qBAEA,MAAM,iBAAiB,kBAAkB,EAAE,UAAU,MAAM,CAAC"}
@@ -1,97 +1,26 @@
1
- import { extractKeyAndLocaleFromPath } from "./syncJSON.mjs";
2
- import { isAbsolute, relative, resolve } from "node:path";
3
- import { loadExternalFile } from "@intlayer/config/file";
4
- import { colorizePath, getAppLogger } from "@intlayer/config/logger";
5
- import { parseFilePathPattern } from "@intlayer/config/utils";
6
- import fg from "fast-glob";
1
+ import { jsonCodec, readJSONEntry } from "./jsonCodec.mjs";
2
+ import { createFileAdapter, createSyncPlugin } from "@intlayer/engine/syncPluginKit";
7
3
 
8
4
  //#region src/loadJSON.ts
9
- const listMessages = async (source, configuration) => {
10
- const { system, internationalization } = configuration;
11
- const { baseDir } = system;
12
- const { locales } = internationalization;
13
- const result = {};
14
- for (const locale of locales) {
15
- const globPatternLocale = await parseFilePathPattern(source, {
16
- key: "**",
17
- locale
18
- });
19
- const maskPatternLocale = await parseFilePathPattern(source, {
20
- key: "{{__KEY__}}",
21
- locale
22
- });
23
- if (!globPatternLocale || !maskPatternLocale) continue;
24
- const files = await fg(globPatternLocale.startsWith("./") ? globPatternLocale.slice(2) : globPatternLocale, { cwd: baseDir });
25
- for (const file of files) {
26
- const hasLocaleInMask = maskPatternLocale.includes("{{__LOCALE__}}");
27
- const hasKeyInMask = maskPatternLocale.includes("{{__KEY__}}");
28
- let key;
29
- let extractedLocale;
30
- if (hasLocaleInMask || hasKeyInMask) {
31
- const extraction = extractKeyAndLocaleFromPath(file, maskPatternLocale, locales, locale);
32
- if (!extraction) continue;
33
- key = extraction.key;
34
- extractedLocale = extraction.locale;
35
- } else {
36
- key = "index";
37
- extractedLocale = locale;
38
- }
39
- const absolutePath = isAbsolute(file) ? file : resolve(baseDir, file);
40
- const usedLocale = extractedLocale;
41
- if (!result[usedLocale]) result[usedLocale] = {};
42
- result[usedLocale][key] = absolutePath;
43
- }
44
- }
45
- return result;
46
- };
47
- const loadMessagePathMap = async (source, configuration) => {
48
- const messages = await listMessages(source, configuration);
49
- return Object.entries(messages).flatMap(([locale, keysRecord]) => Object.entries(keysRecord).map(([key, path]) => {
50
- return {
51
- path: isAbsolute(path) ? path : resolve(configuration.system.baseDir, path),
52
- locale,
53
- key
54
- };
55
- }));
56
- };
57
- const loadJSON = (options) => {
58
- const { location, priority, locale, format } = {
59
- location: "plugin",
60
- priority: 0,
61
- ...options
62
- };
63
- return {
64
- name: "load-json",
65
- loadDictionaries: async ({ configuration }) => {
66
- const appLogger = getAppLogger(configuration);
67
- const dictionariesMap = await loadMessagePathMap(options.source, configuration);
68
- if (dictionariesMap.length === 0) appLogger(`No dictionaries found at locations matching source pattern: ${colorizePath(await parseFilePathPattern(options.source, {
69
- key: "{{key}}",
70
- locale: "{{locale}}"
71
- }))}`, { level: "warn" });
72
- const dictionaries = [];
73
- for (const { path, key, locale: entryLocale } of dictionariesMap) {
74
- const json = await loadExternalFile(path, { logError: false }) ?? {};
75
- const filePath = relative(configuration.system.baseDir, path);
76
- const entryUsedLocale = locale ?? entryLocale;
77
- const dictionary = {
78
- key,
79
- locale: entryUsedLocale,
80
- fill: filePath,
81
- format,
82
- localId: `${key}::${location}::${filePath}`,
83
- location,
84
- filled: entryUsedLocale !== configuration.internationalization.defaultLocale ? true : void 0,
85
- content: json,
86
- filePath,
87
- priority
88
- };
89
- dictionaries.push(dictionary);
90
- }
91
- return dictionaries;
92
- }
93
- };
94
- };
5
+ /**
6
+ * Read-only JSON ingestion plugin: loads JSON message files as dictionaries
7
+ * without ever writing back to them.
8
+ */
9
+ const loadJSON = (options) => createSyncPlugin({
10
+ name: "load-json",
11
+ adapter: createFileAdapter({
12
+ source: options.source,
13
+ codec: jsonCodec,
14
+ readEntry: readJSONEntry,
15
+ discovery: "inclusive"
16
+ }),
17
+ direction: "pull",
18
+ location: options.location ?? "plugin",
19
+ priority: options.priority ?? 0,
20
+ format: options.format,
21
+ splitKeys: options.splitKeys,
22
+ localeOverride: options.locale
23
+ });
95
24
 
96
25
  //#endregion
97
26
  export { loadJSON };
@@ -1 +1 @@
1
- {"version":3,"file":"loadJSON.mjs","names":["sourcePattern"],"sources":["../../src/loadJSON.ts"],"sourcesContent":["import { isAbsolute, relative, resolve } from 'node:path';\nimport { loadExternalFile } from '@intlayer/config/file';\nimport { colorizePath, getAppLogger } from '@intlayer/config/logger';\nimport { parseFilePathPattern } from '@intlayer/config/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type {\n Dictionary,\n DictionaryFormat,\n LocalDictionaryId,\n} from '@intlayer/types/dictionary';\nimport type {\n FilePathPattern,\n FilePathPatternContext,\n} from '@intlayer/types/filePathPattern';\nimport type { Plugin } from '@intlayer/types/plugin';\nimport fg from 'fast-glob';\nimport { extractKeyAndLocaleFromPath } from './syncJSON';\n\ntype JSONContent = Record<string, any>;\n\ntype FilePath = string;\n\ntype MessagesRecord = Record<Locale, Record<Dictionary['key'], FilePath>>;\n\nconst listMessages = async (\n source: FilePathPattern,\n configuration: IntlayerConfig\n): Promise<MessagesRecord> => {\n const { system, internationalization } = configuration;\n\n const { baseDir } = system;\n const { locales } = internationalization;\n\n const result: MessagesRecord = {} as MessagesRecord;\n\n for (const locale of locales) {\n const globPatternLocale = await parseFilePathPattern(source, {\n key: '**',\n locale,\n } as any as FilePathPatternContext);\n\n const maskPatternLocale = await parseFilePathPattern(source, {\n key: '{{__KEY__}}',\n locale,\n } as any as FilePathPatternContext);\n\n if (!globPatternLocale || !maskPatternLocale) {\n continue;\n }\n\n const normalizedGlobPattern = globPatternLocale.startsWith('./')\n ? globPatternLocale.slice(2)\n : globPatternLocale;\n\n const files = await fg(normalizedGlobPattern, {\n cwd: baseDir,\n });\n\n for (const file of files) {\n // extractKeyAndLocaleFromPath requires at least one named capture group\n // ({{__LOCALE__}} or {{__KEY__}}) in the mask to return a non-null result.\n // When the mask is fully concrete (e.g. `messages_ICU/en.json` — the source\n // has {{locale}} but no {{key}}), no groups exist and it returns null.\n // In that case, fall back directly to the loop locale and key = 'index'.\n const hasLocaleInMask = maskPatternLocale.includes('{{__LOCALE__}}');\n const hasKeyInMask = maskPatternLocale.includes('{{__KEY__}}');\n\n let key: string;\n let extractedLocale: Locale;\n\n if (hasLocaleInMask || hasKeyInMask) {\n const extraction = extractKeyAndLocaleFromPath(\n file,\n maskPatternLocale,\n locales,\n locale\n );\n\n if (!extraction) {\n continue;\n }\n\n key = extraction.key;\n extractedLocale = extraction.locale;\n } else {\n // Mask has no placeholders — the file was found via a concrete locale\n // glob. Attribute it directly to the current loop locale.\n key = 'index';\n extractedLocale = locale;\n }\n\n const absolutePath = isAbsolute(file) ? file : resolve(baseDir, file);\n\n const usedLocale = extractedLocale as Locale;\n if (!result[usedLocale]) {\n result[usedLocale] = {};\n }\n\n result[usedLocale][key as Dictionary['key']] = absolutePath;\n }\n }\n\n // For the load plugin we only use actual discovered files; do not fabricate\n // missing locales or keys, since we don't write outputs.\n return result;\n};\n\ntype DictionariesMap = { path: string; locale: Locale; key: string }[];\n\nconst loadMessagePathMap = async (\n source: MessagesRecord | FilePathPattern,\n configuration: IntlayerConfig\n) => {\n const sourcePattern = source as FilePathPattern;\n const messages: MessagesRecord = await listMessages(\n sourcePattern,\n configuration\n );\n\n // Always include all discovered locales — loadJSON is read-only and should\n // ingest every locale file that exists, just like syncJSON does.\n const entries = Object.entries(messages) as [\n Locale,\n Record<Dictionary['key'], FilePath>,\n ][];\n\n const dictionariesPathMap: DictionariesMap = entries.flatMap(\n ([locale, keysRecord]) =>\n Object.entries(keysRecord).map(([key, path]) => {\n const absolutePath = isAbsolute(path)\n ? path\n : resolve(configuration.system.baseDir, path);\n\n return {\n path: absolutePath,\n locale,\n key,\n } as DictionariesMap[number];\n })\n );\n\n return dictionariesPathMap;\n};\n\ntype LoadJSONPluginOptions = {\n /**\n * The source of the plugin.\n * Is a function to build the source from the key and locale.\n *\n * @example\n * ```ts\n * loadJSON({\n * source: ({ key }) => `blog/${'**'}/${key}.i18n.json`,\n * })\n * ```\n */\n source: FilePathPattern;\n\n /**\n * Locale\n *\n * If not provided, the plugin will consider the default locale.\n *\n * @example\n * ```ts\n * loadJSON({\n * source: ({ key }) => `blog/${'**'}/${key}.i18n.json`,\n * locale: Locales.ENGLISH,\n * })\n * ```\n */\n locale?: Locale;\n\n /**\n * Because Intlayer transform the JSON files into Dictionary, we need to identify the plugin in the dictionary.\n * Used to identify the plugin in the dictionary.\n *\n * In the case you have multiple plugins, you can use this to identify the plugin in the dictionary.\n *\n * @example\n * ```ts\n * const config = {\n * plugins: [\n * loadJSON({\n * source: ({ key }) => `./resources/${key}.json`,\n * location: 'plugin-i18next',\n * }),\n * loadJSON({\n * source: ({ key }) => `./messages/${key}.json`,\n * location: 'plugin-next-intl',\n * }),\n * ]\n * }\n * ```\n */\n location?: string;\n\n /**\n * The priority of the dictionaries created by the plugin.\n *\n * In the case of conflicts with remote dictionaries, or .content files, the dictionary with the highest priority will override the other dictionaries.\n *\n * Default is -1. (.content file priority is 0)\n *\n */\n priority?: number;\n\n /**\n * The format of the dictionary content.\n *\n * @example\n * ```ts\n * loadJSON({\n * format: 'icu',\n * })\n * ```\n */\n format?: DictionaryFormat;\n};\n\nexport const loadJSON = (options: LoadJSONPluginOptions): Plugin => {\n const { location, priority, locale, format } = {\n location: 'plugin',\n priority: 0,\n ...options,\n } as const;\n\n return {\n name: 'load-json',\n\n loadDictionaries: async ({ configuration }) => {\n const appLogger = getAppLogger(configuration);\n const dictionariesMap: DictionariesMap = await loadMessagePathMap(\n options.source,\n configuration\n );\n\n if (dictionariesMap.length === 0) {\n const pattern = await parseFilePathPattern(options.source, {\n key: '{{key}}',\n locale: '{{locale}}',\n } as any as FilePathPatternContext);\n\n appLogger(\n `No dictionaries found at locations matching source pattern: ${colorizePath(pattern)}`,\n { level: 'warn' }\n );\n }\n\n const dictionaries: Dictionary[] = [];\n\n for (const { path, key, locale: entryLocale } of dictionariesMap) {\n // loadExternalFile swallows errors and returns undefined for missing files;\n // use ?? {} to guarantee a plain object regardless.\n const json: JSONContent =\n (await loadExternalFile(path, { logError: false })) ?? {};\n\n const filePath = relative(configuration.system.baseDir, path);\n\n // Use the per-entry locale discovered from the file path. If a fixed\n // locale override was provided, use it only as a fallback.\n const entryUsedLocale = (locale ?? entryLocale) as Locale;\n\n const dictionary: Dictionary = {\n key,\n locale: entryUsedLocale,\n fill: filePath,\n format,\n localId: `${key}::${location}::${filePath}` as LocalDictionaryId,\n location: location as Dictionary['location'],\n filled:\n entryUsedLocale !== configuration.internationalization.defaultLocale\n ? true\n : undefined,\n content: json,\n filePath,\n priority,\n };\n\n dictionaries.push(dictionary);\n }\n\n return dictionaries;\n },\n };\n};\n"],"mappings":";;;;;;;;AAyBA,MAAM,eAAe,OACnB,QACA,kBAC4B;CAC5B,MAAM,EAAE,QAAQ,yBAAyB;CAEzC,MAAM,EAAE,YAAY;CACpB,MAAM,EAAE,YAAY;CAEpB,MAAM,SAAyB,EAAE;AAEjC,MAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,oBAAoB,MAAM,qBAAqB,QAAQ;GAC3D,KAAK;GACL;GACD,CAAkC;EAEnC,MAAM,oBAAoB,MAAM,qBAAqB,QAAQ;GAC3D,KAAK;GACL;GACD,CAAkC;AAEnC,MAAI,CAAC,qBAAqB,CAAC,kBACzB;EAOF,MAAM,QAAQ,MAAM,GAJU,kBAAkB,WAAW,KAAK,GAC5D,kBAAkB,MAAM,EAAE,GAC1B,mBAE0C,EAC5C,KAAK,SACN,CAAC;AAEF,OAAK,MAAM,QAAQ,OAAO;GAMxB,MAAM,kBAAkB,kBAAkB,SAAS,iBAAiB;GACpE,MAAM,eAAe,kBAAkB,SAAS,cAAc;GAE9D,IAAI;GACJ,IAAI;AAEJ,OAAI,mBAAmB,cAAc;IACnC,MAAM,aAAa,4BACjB,MACA,mBACA,SACA,OACD;AAED,QAAI,CAAC,WACH;AAGF,UAAM,WAAW;AACjB,sBAAkB,WAAW;UACxB;AAGL,UAAM;AACN,sBAAkB;;GAGpB,MAAM,eAAe,WAAW,KAAK,GAAG,OAAO,QAAQ,SAAS,KAAK;GAErE,MAAM,aAAa;AACnB,OAAI,CAAC,OAAO,YACV,QAAO,cAAc,EAAE;AAGzB,UAAO,YAAY,OAA4B;;;AAMnD,QAAO;;AAKT,MAAM,qBAAqB,OACzB,QACA,kBACG;CAEH,MAAM,WAA2B,MAAM,aACrCA,QACA,cACD;AAwBD,QApBgB,OAAO,QAAQ,SAKqB,CAAC,SAClD,CAAC,QAAQ,gBACR,OAAO,QAAQ,WAAW,CAAC,KAAK,CAAC,KAAK,UAAU;AAK9C,SAAO;GACL,MALmB,WAAW,KAAK,GACjC,OACA,QAAQ,cAAc,OAAO,SAAS,KAAK;GAI7C;GACA;GACD;GACD,CAGoB;;AA+E5B,MAAa,YAAY,YAA2C;CAClE,MAAM,EAAE,UAAU,UAAU,QAAQ,WAAW;EAC7C,UAAU;EACV,UAAU;EACV,GAAG;EACJ;AAED,QAAO;EACL,MAAM;EAEN,kBAAkB,OAAO,EAAE,oBAAoB;GAC7C,MAAM,YAAY,aAAa,cAAc;GAC7C,MAAM,kBAAmC,MAAM,mBAC7C,QAAQ,QACR,cACD;AAED,OAAI,gBAAgB,WAAW,EAM7B,WACE,+DAA+D,aAAa,MANxD,qBAAqB,QAAQ,QAAQ;IACzD,KAAK;IACL,QAAQ;IACT,CAAkC,CAGmD,IACpF,EAAE,OAAO,QAAQ,CAClB;GAGH,MAAM,eAA6B,EAAE;AAErC,QAAK,MAAM,EAAE,MAAM,KAAK,QAAQ,iBAAiB,iBAAiB;IAGhE,MAAM,OACH,MAAM,iBAAiB,MAAM,EAAE,UAAU,OAAO,CAAC,IAAK,EAAE;IAE3D,MAAM,WAAW,SAAS,cAAc,OAAO,SAAS,KAAK;IAI7D,MAAM,kBAAmB,UAAU;IAEnC,MAAM,aAAyB;KAC7B;KACA,QAAQ;KACR,MAAM;KACN;KACA,SAAS,GAAG,IAAI,IAAI,SAAS,IAAI;KACvB;KACV,QACE,oBAAoB,cAAc,qBAAqB,gBACnD,OACA;KACN,SAAS;KACT;KACA;KACD;AAED,iBAAa,KAAK,WAAW;;AAG/B,UAAO;;EAEV"}
1
+ {"version":3,"file":"loadJSON.mjs","names":[],"sources":["../../src/loadJSON.ts"],"sourcesContent":["import {\n createFileAdapter,\n createSyncPlugin,\n} from '@intlayer/engine/syncPluginKit';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { DictionaryFormat } from '@intlayer/types/dictionary';\nimport type { FilePathPattern } from '@intlayer/types/filePathPattern';\nimport type { Plugin } from '@intlayer/types/plugin';\nimport { jsonCodec, readJSONEntry } from './jsonCodec';\n\nexport type LoadJSONPluginOptions = {\n /**\n * The source of the plugin.\n * Is a function to build the source from the key and locale.\n *\n * @example\n * ```ts\n * loadJSON({\n * source: ({ key }) => `blog/${'**'}/${key}.i18n.json`,\n * })\n * ```\n */\n source: FilePathPattern;\n\n /**\n * Locale\n *\n * If not provided, the plugin will consider the default locale.\n *\n * @example\n * ```ts\n * loadJSON({\n * source: ({ key }) => `blog/${'**'}/${key}.i18n.json`,\n * locale: Locales.ENGLISH,\n * })\n * ```\n */\n locale?: Locale;\n\n /**\n * Because Intlayer transform the JSON files into Dictionary, we need to identify the plugin in the dictionary.\n * Used to identify the plugin in the dictionary.\n *\n * In the case you have multiple plugins, you can use this to identify the plugin in the dictionary.\n *\n * @example\n * ```ts\n * const config = {\n * plugins: [\n * loadJSON({\n * source: ({ key }) => `./resources/${key}.json`,\n * location: 'plugin-i18next',\n * }),\n * loadJSON({\n * source: ({ key }) => `./messages/${key}.json`,\n * location: 'plugin-next-intl',\n * }),\n * ]\n * }\n * ```\n */\n location?: string;\n\n /**\n * The priority of the dictionaries created by the plugin.\n *\n * In the case of conflicts with remote dictionaries, or .content files, the dictionary with the highest priority will override the other dictionaries.\n *\n * Default is -1. (.content file priority is 0)\n *\n */\n priority?: number;\n\n /**\n * The format of the dictionary content.\n *\n * @example\n * ```ts\n * loadJSON({\n * format: 'icu',\n * })\n * ```\n */\n format?: DictionaryFormat;\n\n /**\n * Whether each top-level key of the JSON file should become its own\n * dictionary (keyed by that top-level key) instead of a single dictionary\n * holding the whole file.\n *\n * This matches the namespace model of libraries such as `next-intl` /\n * `react-intl`, where a single `messages/{locale}.json` file groups several\n * namespaces by its first-level keys.\n *\n * When omitted, it is auto-detected: the file is split when the `source`\n * pattern has no `{{key}}` segment, and kept as a single dictionary otherwise.\n */\n splitKeys?: boolean;\n};\n\n/**\n * Read-only JSON ingestion plugin: loads JSON message files as dictionaries\n * without ever writing back to them.\n */\nexport const loadJSON = (options: LoadJSONPluginOptions): Plugin =>\n createSyncPlugin({\n name: 'load-json',\n adapter: createFileAdapter({\n source: options.source,\n codec: jsonCodec,\n readEntry: readJSONEntry,\n discovery: 'inclusive',\n }),\n direction: 'pull',\n location: options.location ?? 'plugin',\n priority: options.priority ?? 0,\n format: options.format,\n splitKeys: options.splitKeys,\n localeOverride: options.locale,\n });\n"],"mappings":";;;;;;;;AAwGA,MAAa,YAAY,YACvB,iBAAiB;CACf,MAAM;CACN,SAAS,kBAAkB;EACzB,QAAQ,QAAQ;EAChB,OAAO;EACP,WAAW;EACX,WAAW;CACb,CAAC;CACD,WAAW;CACX,UAAU,QAAQ,YAAY;CAC9B,UAAU,QAAQ,YAAY;CAC9B,QAAQ,QAAQ;CAChB,WAAW,QAAQ;CACnB,gBAAgB,QAAQ;AAC1B,CAAC"}
@@ -1,173 +1,28 @@
1
- import { dirname, isAbsolute, relative, resolve } from "node:path";
2
- import { loadExternalFile } from "@intlayer/config/file";
3
- import { colorizePath, getAppLogger } from "@intlayer/config/logger";
1
+ import { jsonCodec, readJSONEntry } from "./jsonCodec.mjs";
2
+ import { buildFilePathPatternContext, createFileAdapter, createSyncPlugin, extractKeyAndLocaleFromPath } from "@intlayer/engine/syncPluginKit";
4
3
  import { parseFilePathPattern } from "@intlayer/config/utils";
5
- import fg from "fast-glob";
6
- import { mkdir, writeFile } from "node:fs/promises";
7
4
 
8
5
  //#region src/syncJSON.ts
9
- const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10
- const extractKeyAndLocaleFromPath = (filePath, maskPattern, locales, defaultLocale) => {
11
- const keyPlaceholder = "{{__KEY__}}";
12
- const localePlaceholder = "{{__LOCALE__}}";
13
- const normalize = (path) => path.startsWith("./") ? path.slice(2) : path;
14
- const normalizedFilePath = normalize(filePath);
15
- const normalizedMask = normalize(maskPattern);
16
- const localesAlternation = locales.join("|");
17
- let regexStr = `^${escapeRegex(normalizedMask)}$`;
18
- regexStr = regexStr.replace(/\\\*\\\*/g, ".*");
19
- regexStr = regexStr.replace(/\\\*/g, "[^/]*");
20
- regexStr = regexStr.replace(escapeRegex(localePlaceholder), `(?<locale>${localesAlternation})`);
21
- if (normalizedMask.includes(keyPlaceholder)) regexStr = regexStr.replace(escapeRegex(keyPlaceholder), "(?<key>.+)");
22
- const match = new RegExp(regexStr).exec(normalizedFilePath);
23
- if (!match?.groups) return null;
24
- let locale = match.groups.locale;
25
- let key = match.groups.key ?? "index";
26
- if (typeof key === "undefined") key = "index";
27
- if (typeof locale === "undefined") locale = defaultLocale;
28
- return {
29
- key,
30
- locale
31
- };
32
- };
33
- const listMessages = async (source, configuration) => {
34
- const { system, internationalization } = configuration;
35
- const { baseDir } = system;
36
- const { locales } = internationalization;
37
- const result = {};
38
- for (const locale of locales) {
39
- const globPatternLocale = await parseFilePathPattern(source, {
40
- key: "**",
41
- locale
42
- });
43
- const maskPatternLocale = await parseFilePathPattern(source, {
44
- key: "{{__KEY__}}",
45
- locale
46
- });
47
- if (!globPatternLocale || !maskPatternLocale) continue;
48
- const files = await fg(globPatternLocale.startsWith("./") ? globPatternLocale.slice(2) : globPatternLocale, { cwd: baseDir });
49
- for (const file of files) {
50
- const extraction = extractKeyAndLocaleFromPath(file, maskPatternLocale, locales, locale);
51
- if (!extraction) continue;
52
- const { key, locale: extractedLocale } = extraction;
53
- const expectedPath = await parseFilePathPattern(source, {
54
- key,
55
- locale: extractedLocale
56
- });
57
- const absoluteFoundPath = isAbsolute(file) ? file : resolve(baseDir, file);
58
- if (absoluteFoundPath !== (isAbsolute(expectedPath) ? expectedPath : resolve(baseDir, expectedPath))) continue;
59
- const usedLocale = extractedLocale;
60
- if (!result[usedLocale]) result[usedLocale] = {};
61
- result[usedLocale][key] = absoluteFoundPath;
62
- }
63
- }
64
- const hasKeyInMask = (await parseFilePathPattern(source, {
65
- key: "{{__KEY__}}",
66
- locale: locales[0]
67
- })).includes("{{__KEY__}}");
68
- const discoveredKeys = /* @__PURE__ */ new Set();
69
- for (const locale of Object.keys(result)) for (const key of Object.keys(result[locale] ?? {})) discoveredKeys.add(key);
70
- if (!hasKeyInMask) discoveredKeys.add("index");
71
- const keysToEnsure = discoveredKeys.size > 0 ? Array.from(discoveredKeys) : [];
72
- for (const locale of locales) {
73
- if (!result[locale]) result[locale] = {};
74
- for (const key of keysToEnsure) if (!result[locale][key]) {
75
- const builtPath = await parseFilePathPattern(source, {
76
- key,
77
- locale
78
- });
79
- const absoluteBuiltPath = isAbsolute(builtPath) ? builtPath : resolve(baseDir, builtPath);
80
- result[locale][key] = absoluteBuiltPath;
81
- }
82
- }
83
- return result;
84
- };
85
- const loadMessagePathMap = async (source, configuration) => {
86
- const messages = await listMessages(source, configuration);
87
- return Object.entries(messages).flatMap(([locale, keysRecord]) => Object.entries(keysRecord).map(([key, path]) => {
88
- return {
89
- path: isAbsolute(path) ? path : resolve(configuration.system.baseDir, path),
90
- locale,
91
- key
92
- };
93
- }));
94
- };
6
+ /**
7
+ * Two-way JSON synchronization plugin: ingests JSON message files as
8
+ * dictionaries and writes the merged build output back to the same files.
9
+ */
95
10
  const syncJSON = async (options) => {
96
- const { location, priority, format } = {
97
- location: `sync-json::${await parseFilePathPattern(options.source, {
98
- key: "{{key}}",
99
- locale: "{{locale}}"
100
- })}`,
101
- priority: 0,
102
- ...options
103
- };
104
- return {
11
+ const defaultLocation = `sync-json::${await parseFilePathPattern(options.source, buildFilePathPatternContext("{{key}}", "{{locale}}"))}`;
12
+ return createSyncPlugin({
105
13
  name: "sync-json",
106
- loadDictionaries: async ({ configuration }) => {
107
- const appLogger = getAppLogger(configuration);
108
- const dictionariesMap = await loadMessagePathMap(options.source, configuration);
109
- if (dictionariesMap.length === 0) appLogger(`[sync-json] No dictionaries found at locations matching source pattern: ${colorizePath(await parseFilePathPattern(options.source, {
110
- key: "{{key}}",
111
- locale: "{{locale}}"
112
- }))}`, { level: "warn" });
113
- let fill = await parseFilePathPattern(options.source, {
114
- key: "{{key}}",
115
- locale: "{{locale}}"
116
- });
117
- if (fill) fill = relative(configuration.system.baseDir, resolve(configuration.system.baseDir, fill));
118
- const dictionaries = [];
119
- for (const { locale, path, key } of dictionariesMap) {
120
- const json = await loadExternalFile(path, { logError: false }) ?? {};
121
- const filePath = relative(configuration.system.baseDir, path);
122
- const dictionary = {
123
- key,
124
- locale,
125
- fill,
126
- format,
127
- localId: `${key}::${location}::${filePath}`,
128
- location,
129
- filled: locale !== configuration.internationalization.defaultLocale ? true : void 0,
130
- content: json,
131
- filePath,
132
- priority
133
- };
134
- dictionaries.push(dictionary);
135
- }
136
- return dictionaries;
137
- },
138
- formatOutput: async ({ dictionary, configuration }) => {
139
- const { formatDictionaryOutput } = await import("@intlayer/chokidar/build");
140
- if (!dictionary.filePath || !dictionary.locale) return dictionary;
141
- const builderPath = await parseFilePathPattern(options.source, {
142
- key: dictionary.key,
143
- locale: dictionary.locale
144
- });
145
- if (resolve(configuration.system.baseDir, builderPath) !== resolve(configuration.system.baseDir, dictionary.filePath)) return dictionary;
146
- return formatDictionaryOutput(dictionary, format).content;
147
- },
148
- afterBuild: async ({ dictionaries, configuration }) => {
149
- const { getPerLocaleDictionary } = await import("@intlayer/core/plugins");
150
- const { parallelize } = await import("@intlayer/chokidar/utils");
151
- const { formatDictionaryOutput } = await import("@intlayer/chokidar/build");
152
- const { locales } = configuration.internationalization;
153
- await parallelize(Object.entries(dictionaries.mergedDictionaries).flatMap(([key, dictionary]) => locales.map((locale) => ({
154
- key,
155
- dictionary: dictionary.dictionary,
156
- locale
157
- }))), async ({ key, dictionary, locale }) => {
158
- if (dictionary.location !== location) return;
159
- const builderPath = await parseFilePathPattern(options.source, {
160
- key,
161
- locale
162
- });
163
- const formattedOutput = formatDictionaryOutput(getPerLocaleDictionary(dictionary, locale), format);
164
- const content = JSON.parse(JSON.stringify(formattedOutput.content));
165
- if (typeof content === "undefined" || typeof content === "object" && Object.keys(content).length === 0) return;
166
- await mkdir(dirname(builderPath), { recursive: true });
167
- await writeFile(builderPath, `${JSON.stringify(content, null, 2)}\n`, "utf-8");
168
- });
169
- }
170
- };
14
+ adapter: createFileAdapter({
15
+ source: options.source,
16
+ codec: jsonCodec,
17
+ readEntry: readJSONEntry,
18
+ discovery: "strict"
19
+ }),
20
+ direction: "both",
21
+ location: options.location ?? defaultLocation,
22
+ priority: options.priority ?? 0,
23
+ format: options.format,
24
+ splitKeys: options.splitKeys
25
+ });
171
26
  };
172
27
 
173
28
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"syncJSON.mjs","names":[],"sources":["../../src/syncJSON.ts"],"sourcesContent":["import { mkdir, writeFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, relative, resolve } from 'node:path';\nimport { loadExternalFile } from '@intlayer/config/file';\nimport { colorizePath, getAppLogger } from '@intlayer/config/logger';\nimport { parseFilePathPattern } from '@intlayer/config/utils';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type {\n Dictionary,\n DictionaryFormat,\n DictionaryLocation,\n LocalDictionaryId,\n} from '@intlayer/types/dictionary';\nimport type {\n FilePathPattern,\n FilePathPatternContext,\n} from '@intlayer/types/filePathPattern';\nimport type { Plugin } from '@intlayer/types/plugin';\nimport fg from 'fast-glob';\n\ntype JSONContent = Record<string, any>;\n\ntype FilePath = string;\n\ntype MessagesRecord = Record<Locale, Record<Dictionary['key'], FilePath>>;\n\nconst escapeRegex = (str: string) => str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\nexport const extractKeyAndLocaleFromPath = (\n filePath: string,\n maskPattern: string,\n locales: Locale[],\n defaultLocale: Locale\n): { key: string; locale: Locale } | null => {\n const keyPlaceholder = '{{__KEY__}}';\n const localePlaceholder = '{{__LOCALE__}}';\n\n // fast-glob strips leading \"./\" from returned paths; normalize both sides\n const normalize = (path: string) =>\n path.startsWith('./') ? path.slice(2) : path;\n\n const normalizedFilePath = normalize(filePath);\n const normalizedMask = normalize(maskPattern);\n\n const localesAlternation = locales.join('|');\n\n // Escape special regex chars, then convert glob wildcards to regex equivalents.\n // Must replace ** before * to avoid double-replacing.\n let regexStr = `^${escapeRegex(normalizedMask)}$`;\n regexStr = regexStr.replace(/\\\\\\*\\\\\\*/g, '.*'); // ** → match any path segments\n regexStr = regexStr.replace(/\\\\\\*/g, '[^/]*'); // * → match within a single segment\n\n regexStr = regexStr.replace(\n escapeRegex(localePlaceholder),\n `(?<locale>${localesAlternation})`\n );\n\n if (normalizedMask.includes(keyPlaceholder)) {\n regexStr = regexStr.replace(escapeRegex(keyPlaceholder), '(?<key>.+)');\n }\n\n const maskRegex = new RegExp(regexStr);\n const match = maskRegex.exec(normalizedFilePath);\n\n if (!match?.groups) {\n return null;\n }\n\n let locale = match.groups.locale as Locale | undefined;\n let key = (match.groups.key as string | undefined) ?? 'index';\n\n if (typeof key === 'undefined') {\n key = 'index';\n }\n\n if (typeof locale === 'undefined') {\n locale = defaultLocale;\n }\n\n return {\n key,\n locale,\n };\n};\n\nconst listMessages = async (\n source: FilePathPattern,\n configuration: IntlayerConfig\n): Promise<MessagesRecord> => {\n const { system, internationalization } = configuration;\n\n const { baseDir } = system;\n const { locales } = internationalization;\n\n const result: MessagesRecord = {} as MessagesRecord;\n\n for (const locale of locales) {\n const globPatternLocale = await parseFilePathPattern(source, {\n key: '**',\n locale,\n } as any as FilePathPatternContext);\n\n const maskPatternLocale = await parseFilePathPattern(source, {\n key: '{{__KEY__}}',\n locale,\n } as any as FilePathPatternContext);\n\n if (!globPatternLocale || !maskPatternLocale) {\n continue;\n }\n\n const normalizedGlobPattern = globPatternLocale.startsWith('./')\n ? globPatternLocale.slice(2)\n : globPatternLocale;\n\n const files = await fg(normalizedGlobPattern, {\n cwd: baseDir,\n });\n\n for (const file of files) {\n const extraction = extractKeyAndLocaleFromPath(\n file,\n maskPatternLocale,\n locales,\n locale\n );\n\n if (!extraction) {\n continue;\n }\n\n const { key, locale: extractedLocale } = extraction;\n\n // Generate what the path SHOULD be for this key/locale using the current builder\n const expectedPath = await parseFilePathPattern(source, {\n key,\n locale: extractedLocale,\n } as any as FilePathPatternContext);\n\n // Resolve both to absolute paths to ensure safe comparison\n const absoluteFoundPath = isAbsolute(file)\n ? file\n : resolve(baseDir, file);\n const absoluteExpectedPath = isAbsolute(expectedPath)\n ? expectedPath\n : resolve(baseDir, expectedPath);\n\n // If the file found doesn't exactly match the file expected, it belongs to another plugin/structure\n if (absoluteFoundPath !== absoluteExpectedPath) {\n continue;\n }\n\n const usedLocale = extractedLocale as Locale;\n if (!result[usedLocale]) {\n result[usedLocale] = {};\n }\n\n result[usedLocale][key as Dictionary['key']] = absoluteFoundPath;\n }\n }\n\n // Ensure all declared locales are present even if the file doesn't exist yet\n const maskWithKey = await parseFilePathPattern(source, {\n key: '{{__KEY__}}',\n locale: locales[0],\n } as any as FilePathPatternContext);\n\n const hasKeyInMask = maskWithKey.includes('{{__KEY__}}');\n const discoveredKeys = new Set<string>();\n\n for (const locale of Object.keys(result)) {\n for (const key of Object.keys(result[locale as Locale] ?? {})) {\n discoveredKeys.add(key);\n }\n }\n\n if (!hasKeyInMask) {\n discoveredKeys.add('index');\n }\n\n const keysToEnsure =\n discoveredKeys.size > 0 ? Array.from(discoveredKeys) : [];\n\n for (const locale of locales) {\n if (!result[locale]) {\n result[locale] = {} as Record<Dictionary['key'], FilePath>;\n }\n\n for (const key of keysToEnsure) {\n if (!result[locale][key as Dictionary['key']]) {\n const builtPath = await parseFilePathPattern(source, {\n key,\n locale,\n } as any as FilePathPatternContext);\n const absoluteBuiltPath = isAbsolute(builtPath)\n ? builtPath\n : resolve(baseDir, builtPath);\n\n result[locale][key as Dictionary['key']] = absoluteBuiltPath;\n }\n }\n }\n\n return result;\n};\n\ntype DictionariesMap = { path: string; locale: Locale; key: string }[];\n\nconst loadMessagePathMap = async (\n source: MessagesRecord | FilePathPattern,\n configuration: IntlayerConfig\n) => {\n const messages: MessagesRecord = await listMessages(\n source as FilePathPattern,\n configuration\n );\n\n const dictionariesPathMap: DictionariesMap = Object.entries(messages).flatMap(\n ([locale, keysRecord]) =>\n Object.entries(keysRecord).map(([key, path]) => {\n const absolutePath = isAbsolute(path)\n ? path\n : resolve(configuration.system.baseDir, path);\n\n return {\n path: absolutePath,\n locale,\n key,\n } as DictionariesMap[number];\n })\n );\n\n return dictionariesPathMap;\n};\n\ntype SyncJSONPluginOptions = {\n /**\n * The source of the plugin.\n * Is a function to build the source from the key and locale.\n *\n * ```ts\n * syncJSON({\n * source: ({ key, locale }) => `./messages/${locale}/${key}.json`\n * })\n * ```\n */\n source: FilePathPattern;\n\n /**\n * Because Intlayer transform the JSON files into Dictionary, we need to identify the plugin in the dictionary.\n * Used to identify the plugin in the dictionary.\n *\n * In the case you have multiple plugins, you can use this to identify the plugin in the dictionary.\n *\n * ```ts\n * // Example usage:\n * const config = {\n * plugins: [\n * syncJSON({\n * source: ({ key, locale }) => `./resources/${locale}/${key}.json`,\n * location: 'plugin-i18next',\n * }),\n * syncJSON({\n * source: ({ key, locale }) => `./messages/${locale}/${key}.json`,\n * location: 'plugin-next-intl',\n * }),\n * ]\n * }\n * ```\n */\n location?: DictionaryLocation | (string & {});\n\n /**\n * The priority of the dictionaries created by the plugin.\n *\n * In the case of conflicts with remote dictionaries, or .content files, the dictionary with the highest priority will override the other dictionaries.\n *\n * Default is -1. (.content file priority is 0)\n *\n */\n priority?: number;\n\n /**\n * The format of the dictionaries created by the plugin.\n *\n * Default: 'intlayer'\n *\n * The format of the dictionaries created by the plugin.\n */\n format?: DictionaryFormat;\n};\n\nexport const syncJSON = async (\n options: SyncJSONPluginOptions\n): Promise<Plugin> => {\n // Generate a unique default location based on the source pattern.\n // This ensures that if you have multiple plugins, they don't share the same 'plugin' ID.\n const patternMarker = await parseFilePathPattern(options.source, {\n key: '{{key}}',\n locale: '{{locale}}',\n } as any as FilePathPatternContext);\n const defaultLocation = `sync-json::${patternMarker}`;\n\n const { location, priority, format } = {\n location: defaultLocation,\n priority: 0,\n ...options,\n };\n\n return {\n name: 'sync-json',\n\n loadDictionaries: async ({ configuration }) => {\n const appLogger = getAppLogger(configuration);\n const dictionariesMap: DictionariesMap = await loadMessagePathMap(\n options.source,\n configuration\n );\n\n if (dictionariesMap.length === 0) {\n const pattern = await parseFilePathPattern(options.source, {\n key: '{{key}}',\n locale: '{{locale}}',\n } as any as FilePathPatternContext);\n\n appLogger(\n `[sync-json] No dictionaries found at locations matching source pattern: ${colorizePath(pattern)}`,\n { level: 'warn' }\n );\n }\n\n let fill: string = await parseFilePathPattern(options.source, {\n key: '{{key}}',\n locale: '{{locale}}',\n } as any as FilePathPatternContext);\n\n if (fill) {\n fill = relative(\n configuration.system.baseDir,\n resolve(configuration.system.baseDir, fill)\n );\n }\n\n const dictionaries: Dictionary[] = [];\n\n for (const { locale, path, key } of dictionariesMap) {\n // loadExternalFile swallows errors and returns undefined for missing files;\n // the try/catch does not help here — use ?? {} to guarantee a plain object.\n const json: JSONContent =\n (await loadExternalFile(path, { logError: false })) ?? {};\n\n const filePath = relative(configuration.system.baseDir, path);\n\n const dictionary: Dictionary = {\n key,\n locale,\n fill,\n format,\n localId: `${key}::${location}::${filePath}` as LocalDictionaryId,\n location: location as Dictionary['location'],\n filled:\n locale !== configuration.internationalization.defaultLocale\n ? true\n : undefined,\n content: json,\n filePath,\n priority,\n };\n\n dictionaries.push(dictionary);\n }\n\n return dictionaries;\n },\n\n formatOutput: async ({ dictionary, configuration }) => {\n // Lazy import intlayer modules to avoid circular dependencies\n const { formatDictionaryOutput } = await import(\n '@intlayer/chokidar/build'\n );\n\n if (!dictionary.filePath || !dictionary.locale) return dictionary;\n\n const builderPath = await parseFilePathPattern(options.source, {\n key: dictionary.key,\n locale: dictionary.locale,\n } as FilePathPatternContext);\n\n // Verification to ensure we are formatting the correct file\n if (\n resolve(configuration.system.baseDir, builderPath) !==\n resolve(configuration.system.baseDir, dictionary.filePath)\n ) {\n return dictionary;\n }\n\n const formattedOutput = formatDictionaryOutput(dictionary, format);\n\n return formattedOutput.content;\n },\n\n afterBuild: async ({ dictionaries, configuration }) => {\n // Lazy import intlayer modules to avoid circular dependencies\n const { getPerLocaleDictionary } = await import('@intlayer/core/plugins');\n const { parallelize } = await import('@intlayer/chokidar/utils');\n const { formatDictionaryOutput } = await import(\n '@intlayer/chokidar/build'\n );\n\n const { locales } = configuration.internationalization;\n\n type RecordList = {\n key: string;\n dictionary: Dictionary;\n locale: Locale;\n };\n\n // We get all dictionaries, but we need to filter them\n const recordList: RecordList[] = Object.entries(\n dictionaries.mergedDictionaries\n ).flatMap(([key, dictionary]) =>\n locales.map((locale) => ({\n key,\n dictionary: dictionary.dictionary as Dictionary,\n locale,\n }))\n );\n\n await parallelize(recordList, async ({ key, dictionary, locale }) => {\n // Only process dictionaries that belong to THIS plugin instance.\n if (dictionary.location !== location) {\n return;\n }\n\n const builderPath = await parseFilePathPattern(options.source, {\n key,\n locale,\n } as any as FilePathPatternContext);\n\n const localizedDictionary = getPerLocaleDictionary(dictionary, locale);\n\n const formattedOutput = formatDictionaryOutput(\n localizedDictionary,\n format\n );\n\n const content = JSON.parse(JSON.stringify(formattedOutput.content));\n\n if (\n typeof content === 'undefined' ||\n (typeof content === 'object' &&\n Object.keys(content as Record<string, unknown>).length === 0)\n ) {\n return;\n }\n\n await mkdir(dirname(builderPath), { recursive: true });\n\n const stringContent = JSON.stringify(content, null, 2);\n\n await writeFile(builderPath, `${stringContent}\\n`, 'utf-8');\n });\n },\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"syncJSON.mjs","names":[],"sources":["../../src/syncJSON.ts"],"sourcesContent":["import { parseFilePathPattern } from '@intlayer/config/utils';\nimport {\n buildFilePathPatternContext,\n createFileAdapter,\n createSyncPlugin,\n} from '@intlayer/engine/syncPluginKit';\nimport type {\n DictionaryFormat,\n DictionaryLocation,\n} from '@intlayer/types/dictionary';\nimport type { FilePathPattern } from '@intlayer/types/filePathPattern';\nimport type { Plugin } from '@intlayer/types/plugin';\nimport { jsonCodec, readJSONEntry } from './jsonCodec';\n\n// Re-exported for backward compatibility: this helper used to live here.\nexport { extractKeyAndLocaleFromPath } from '@intlayer/engine/syncPluginKit';\n\nexport type SyncJSONPluginOptions = {\n /**\n * The source of the plugin.\n * Is a function to build the source from the key and locale.\n *\n * ```ts\n * syncJSON({\n * source: ({ key, locale }) => `./messages/${locale}/${key}.json`\n * })\n * ```\n */\n source: FilePathPattern;\n\n /**\n * Because Intlayer transform the JSON files into Dictionary, we need to identify the plugin in the dictionary.\n * Used to identify the plugin in the dictionary.\n *\n * In the case you have multiple plugins, you can use this to identify the plugin in the dictionary.\n *\n * ```ts\n * // Example usage:\n * const config = {\n * plugins: [\n * syncJSON({\n * source: ({ key, locale }) => `./resources/${locale}/${key}.json`,\n * location: 'plugin-i18next',\n * }),\n * syncJSON({\n * source: ({ key, locale }) => `./messages/${locale}/${key}.json`,\n * location: 'plugin-next-intl',\n * }),\n * ]\n * }\n * ```\n */\n location?: DictionaryLocation | (string & {});\n\n /**\n * The priority of the dictionaries created by the plugin.\n *\n * In the case of conflicts with remote dictionaries, or .content files, the dictionary with the highest priority will override the other dictionaries.\n *\n * Default is -1. (.content file priority is 0)\n *\n */\n priority?: number;\n\n /**\n * The format of the dictionaries created by the plugin.\n *\n * Default: 'intlayer'\n *\n * The format of the dictionaries created by the plugin.\n */\n format?: DictionaryFormat;\n\n /**\n * Whether each top-level key of the JSON file should become its own\n * dictionary (keyed by that top-level key) instead of a single dictionary\n * holding the whole file.\n *\n * This matches the namespace model of libraries such as `next-intl` /\n * `react-intl`, where a single `messages/{locale}.json` file groups several\n * namespaces by its first-level keys and each namespace is addressed\n * independently (e.g. `useTranslations('Hero')` → dictionary `Hero`).\n *\n * When omitted, it is auto-detected: the file is split when the `source`\n * pattern has no `{{key}}` segment (i.e. one file holds every namespace),\n * and kept as a single dictionary otherwise (one file per key).\n *\n * @example\n * ```ts\n * // messages/en.json → dictionaries: Hero, Nav, About, …\n * syncJSON({\n * source: ({ locale }) => `./messages/${locale}.json`,\n * splitKeys: true,\n * })\n * ```\n */\n splitKeys?: boolean;\n};\n\n/**\n * Two-way JSON synchronization plugin: ingests JSON message files as\n * dictionaries and writes the merged build output back to the same files.\n */\nexport const syncJSON = async (\n options: SyncJSONPluginOptions\n): Promise<Plugin> => {\n // Generate a unique default location based on the source pattern.\n // This ensures that if you have multiple plugins, they don't share the same 'plugin' ID.\n const patternMarker = await parseFilePathPattern(\n options.source,\n buildFilePathPatternContext('{{key}}', '{{locale}}')\n );\n const defaultLocation = `sync-json::${patternMarker}`;\n\n return createSyncPlugin({\n name: 'sync-json',\n adapter: createFileAdapter({\n source: options.source,\n codec: jsonCodec,\n readEntry: readJSONEntry,\n discovery: 'strict',\n }),\n direction: 'both',\n location: options.location ?? defaultLocation,\n priority: options.priority ?? 0,\n format: options.format,\n // Auto-detected by the file adapter when omitted: split when the source\n // pattern has no `{{key}}` segment (one file holds every namespace).\n splitKeys: options.splitKeys,\n });\n};\n"],"mappings":";;;;;;;;;AAuGA,MAAa,WAAW,OACtB,YACoB;CAOpB,MAAM,kBAAkB,cAAc,MAJV,qBAC1B,QAAQ,QACR,4BAA4B,WAAW,YAAY,CACrD;CAGA,OAAO,iBAAiB;EACtB,MAAM;EACN,SAAS,kBAAkB;GACzB,QAAQ,QAAQ;GAChB,OAAO;GACP,WAAW;GACX,WAAW;EACb,CAAC;EACD,WAAW;EACX,UAAU,QAAQ,YAAY;EAC9B,UAAU,QAAQ,YAAY;EAC9B,QAAQ,QAAQ;EAGhB,WAAW,QAAQ;CACrB,CAAC;AACH"}
@@ -1,3 +1,4 @@
1
- import { loadJSON } from "./loadJSON.js";
2
- import { extractKeyAndLocaleFromPath, syncJSON } from "./syncJSON.js";
3
- export { extractKeyAndLocaleFromPath, loadJSON, syncJSON };
1
+ import { jsonCodec, readJSONEntry } from "./jsonCodec.js";
2
+ import { LoadJSONPluginOptions, loadJSON } from "./loadJSON.js";
3
+ import { SyncJSONPluginOptions, extractKeyAndLocaleFromPath, syncJSON } from "./syncJSON.js";
4
+ export { LoadJSONPluginOptions, SyncJSONPluginOptions, extractKeyAndLocaleFromPath, jsonCodec, loadJSON, readJSONEntry, syncJSON };
@@ -0,0 +1,16 @@
1
+ import { FormatCodec, SyncContent } from "@intlayer/engine/syncPluginKit";
2
+ //#region src/jsonCodec.d.ts
3
+ /**
4
+ * JSON payload codec: standard JSON parsing, pretty-printed serialization
5
+ * (2-space indentation, trailing newline).
6
+ */
7
+ declare const jsonCodec: FormatCodec;
8
+ /**
9
+ * Read one JSON source through `loadExternalFile`, which also supports JSON5,
10
+ * JSONC and transpiled JS/TS message files. Returns `undefined` for missing or
11
+ * unreadable files so callers fall back to empty content.
12
+ */
13
+ declare const readJSONEntry: (absoluteFilePath: string) => Promise<SyncContent | undefined>;
14
+ //#endregion
15
+ export { jsonCodec, readJSONEntry };
16
+ //# sourceMappingURL=jsonCodec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonCodec.d.ts","names":[],"sources":["../../src/jsonCodec.ts"],"mappings":";;;;;;cAOa,WAAW;;;;;;cAUX,gBAAa,6BAEvB,QAAQ"}
@@ -2,7 +2,6 @@ import { Locale } from "@intlayer/types/allLocales";
2
2
  import { DictionaryFormat } from "@intlayer/types/dictionary";
3
3
  import { FilePathPattern } from "@intlayer/types/filePathPattern";
4
4
  import { Plugin } from "@intlayer/types/plugin";
5
-
6
5
  //#region src/loadJSON.d.ts
7
6
  type LoadJSONPluginOptions = {
8
7
  /**
@@ -74,8 +73,25 @@ type LoadJSONPluginOptions = {
74
73
  * ```
75
74
  */
76
75
  format?: DictionaryFormat;
76
+ /**
77
+ * Whether each top-level key of the JSON file should become its own
78
+ * dictionary (keyed by that top-level key) instead of a single dictionary
79
+ * holding the whole file.
80
+ *
81
+ * This matches the namespace model of libraries such as `next-intl` /
82
+ * `react-intl`, where a single `messages/{locale}.json` file groups several
83
+ * namespaces by its first-level keys.
84
+ *
85
+ * When omitted, it is auto-detected: the file is split when the `source`
86
+ * pattern has no `{{key}}` segment, and kept as a single dictionary otherwise.
87
+ */
88
+ splitKeys?: boolean;
77
89
  };
90
+ /**
91
+ * Read-only JSON ingestion plugin: loads JSON message files as dictionaries
92
+ * without ever writing back to them.
93
+ */
78
94
  declare const loadJSON: (options: LoadJSONPluginOptions) => Plugin;
79
95
  //#endregion
80
- export { loadJSON };
96
+ export { LoadJSONPluginOptions, loadJSON };
81
97
  //# sourceMappingURL=loadJSON.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"loadJSON.d.ts","names":[],"sources":["../../src/loadJSON.ts"],"mappings":";;;;;;KAiJK,qBAAA;;AAlIgD;;;;;;;;;;EA8InD,MAAA,EAAQ,eAAA;EAeC;;;;;;;AAiDX;;;;;;EAjDE,MAAA,GAAS,MAAA;EAkHV;;;;;;;;;;;;;;;;;;;;;;EA1FC,QAAA;;;;;;;;;EAUA,QAAA;;;;;;;;;;;EAYA,MAAA,GAAS,gBAAA;AAAA;AAAA,cAGE,QAAA,GAAY,OAAA,EAAS,qBAAA,KAAwB,MAAA"}
1
+ {"version":3,"file":"loadJSON.d.ts","names":[],"sources":["../../src/loadJSON.ts"],"mappings":";;;;;KAUY;;;;;;;;;;;;EAYV,QAAQ;;;;;;;;;;;;;;EAeR,SAAS;;;;;;;;;;;;;;;;;;;;;;;EAwBT;;;;;;;;;EAUA;;;;;;;;;;;EAYA,SAAS;;;;;;;;;;;;;EAcT;;;;;;cAOW,WAAQ,SAAa,0BAAwB"}
@@ -1,13 +1,8 @@
1
- import { Locale } from "@intlayer/types/allLocales";
1
+ import { extractKeyAndLocaleFromPath } from "@intlayer/engine/syncPluginKit";
2
2
  import { DictionaryFormat, DictionaryLocation } from "@intlayer/types/dictionary";
3
3
  import { FilePathPattern } from "@intlayer/types/filePathPattern";
4
4
  import { Plugin } from "@intlayer/types/plugin";
5
-
6
5
  //#region src/syncJSON.d.ts
7
- declare const extractKeyAndLocaleFromPath: (filePath: string, maskPattern: string, locales: Locale[], defaultLocale: Locale) => {
8
- key: string;
9
- locale: Locale;
10
- } | null;
11
6
  type SyncJSONPluginOptions = {
12
7
  /**
13
8
  * The source of the plugin.
@@ -60,8 +55,36 @@ type SyncJSONPluginOptions = {
60
55
  * The format of the dictionaries created by the plugin.
61
56
  */
62
57
  format?: DictionaryFormat;
58
+ /**
59
+ * Whether each top-level key of the JSON file should become its own
60
+ * dictionary (keyed by that top-level key) instead of a single dictionary
61
+ * holding the whole file.
62
+ *
63
+ * This matches the namespace model of libraries such as `next-intl` /
64
+ * `react-intl`, where a single `messages/{locale}.json` file groups several
65
+ * namespaces by its first-level keys and each namespace is addressed
66
+ * independently (e.g. `useTranslations('Hero')` → dictionary `Hero`).
67
+ *
68
+ * When omitted, it is auto-detected: the file is split when the `source`
69
+ * pattern has no `{{key}}` segment (i.e. one file holds every namespace),
70
+ * and kept as a single dictionary otherwise (one file per key).
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * // messages/en.json → dictionaries: Hero, Nav, About, …
75
+ * syncJSON({
76
+ * source: ({ locale }) => `./messages/${locale}.json`,
77
+ * splitKeys: true,
78
+ * })
79
+ * ```
80
+ */
81
+ splitKeys?: boolean;
63
82
  };
83
+ /**
84
+ * Two-way JSON synchronization plugin: ingests JSON message files as
85
+ * dictionaries and writes the merged build output back to the same files.
86
+ */
64
87
  declare const syncJSON: (options: SyncJSONPluginOptions) => Promise<Plugin>;
65
88
  //#endregion
66
- export { extractKeyAndLocaleFromPath, syncJSON };
89
+ export { SyncJSONPluginOptions, extractKeyAndLocaleFromPath, syncJSON };
67
90
  //# sourceMappingURL=syncJSON.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"syncJSON.d.ts","names":[],"sources":["../../src/syncJSON.ts"],"mappings":";;;;;;cA4Ba,2BAAA,GACX,QAAA,UACA,WAAA,UACA,OAAA,EAAS,MAAA,IACT,aAAA,EAAe,MAAA;EACZ,GAAA;EAAa,MAAA,EAAQ,MAAA;AAAA;AAAA,KA0MrB,qBAAA;EA5MM;;;;;;;;;;EAuNT,MAAA,EAAQ,eAAA;EArNL;;;;;AAkDH;;;;;;;;;;;;;;;;;EA2LA,QAAA,GAAW,kBAAA;EAkMZ;;;;;;;;EAxLC,QAAA;EAcC;;;;;;;EALD,MAAA,GAAS,gBAAA;AAAA;AAAA,cAGE,QAAA,GACX,OAAA,EAAS,qBAAA,KACR,OAAA,CAAQ,MAAA"}
1
+ {"version":3,"file":"syncJSON.d.ts","names":[],"sources":["../../src/syncJSON.ts"],"mappings":";;;;;KAiBY;;;;;;;;;;;EAWV,QAAQ;;;;;;;;;;;;;;;;;;;;;;;EAwBR,WAAW;;;;;;;;;EAUX;;;;;;;;EASA,SAAS;;;;;;;;;;;;;;;;;;;;;;;;EAyBT;;;;;;cAOW,WAAQ,SACV,0BACR,QAAQ"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intlayer/sync-json-plugin",
3
- "version": "9.0.0-canary.2",
3
+ "version": "9.0.0-canary.20",
4
4
  "private": false,
5
5
  "description": "A plugin for Intlayer that syncs JSON files to dictionaries.",
6
6
  "keywords": [
@@ -71,21 +71,21 @@
71
71
  "typecheck": "tsc --noEmit --project tsconfig.types.json"
72
72
  },
73
73
  "dependencies": {
74
- "@intlayer/chokidar": "9.0.0-canary.2",
75
- "@intlayer/config": "9.0.0-canary.2",
76
- "@intlayer/core": "9.0.0-canary.2",
77
- "@intlayer/types": "9.0.0-canary.2",
74
+ "@intlayer/config": "9.0.0-canary.20",
75
+ "@intlayer/core": "9.0.0-canary.20",
76
+ "@intlayer/engine": "9.0.0-canary.20",
77
+ "@intlayer/types": "9.0.0-canary.20",
78
78
  "fast-glob": "3.3.3"
79
79
  },
80
80
  "devDependencies": {
81
- "@types/node": "25.9.3",
81
+ "@types/node": "26.1.1",
82
82
  "@utils/ts-config": "1.0.4",
83
83
  "@utils/ts-config-types": "1.0.4",
84
84
  "@utils/tsdown-config": "1.0.4",
85
85
  "rimraf": "6.1.3",
86
- "tsdown": "0.21.10",
87
- "typescript": "6.0.3",
88
- "vitest": "4.1.9"
86
+ "tsdown": "0.22.12",
87
+ "typescript": "7.0.2",
88
+ "vitest": "4.1.10"
89
89
  },
90
90
  "engines": {
91
91
  "node": ">=14.18"