@intlayer/sync-json-plugin 9.0.0-canary.2 → 9.0.0-canary.4
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.
- package/dist/cjs/loadJSON.cjs +24 -4
- package/dist/cjs/loadJSON.cjs.map +1 -1
- package/dist/cjs/syncJSON.cjs +53 -8
- package/dist/cjs/syncJSON.cjs.map +1 -1
- package/dist/esm/loadJSON.mjs +24 -4
- package/dist/esm/loadJSON.mjs.map +1 -1
- package/dist/esm/syncJSON.mjs +53 -8
- package/dist/esm/syncJSON.mjs.map +1 -1
- package/dist/types/loadJSON.d.ts +13 -0
- package/dist/types/loadJSON.d.ts.map +1 -1
- package/dist/types/syncJSON.d.ts +24 -0
- package/dist/types/syncJSON.d.ts.map +1 -1
- package/package.json +6 -6
package/dist/cjs/loadJSON.cjs
CHANGED
|
@@ -72,24 +72,44 @@ const loadJSON = (options) => {
|
|
|
72
72
|
key: "{{key}}",
|
|
73
73
|
locale: "{{locale}}"
|
|
74
74
|
}))}`, { level: "warn" });
|
|
75
|
+
const hasKeyPlaceholder = (await (0, _intlayer_config_utils.parseFilePathPattern)(options.source, {
|
|
76
|
+
key: "{{key}}",
|
|
77
|
+
locale: "{{locale}}"
|
|
78
|
+
})).includes("{{key}}");
|
|
79
|
+
const shouldSplitByKeys = options.splitKeys ?? !hasKeyPlaceholder;
|
|
75
80
|
const dictionaries = [];
|
|
76
81
|
for (const { path, key, locale: entryLocale } of dictionariesMap) {
|
|
77
82
|
const json = await (0, _intlayer_config_file.loadExternalFile)(path, { logError: false }) ?? {};
|
|
78
83
|
const filePath = (0, node_path.relative)(configuration.system.baseDir, path);
|
|
79
84
|
const entryUsedLocale = locale ?? entryLocale;
|
|
80
|
-
const
|
|
85
|
+
const filled = entryUsedLocale !== configuration.internationalization.defaultLocale ? true : void 0;
|
|
86
|
+
if (shouldSplitByKeys) {
|
|
87
|
+
for (const [namespaceKey, namespaceContent] of Object.entries(json)) dictionaries.push({
|
|
88
|
+
key: namespaceKey,
|
|
89
|
+
locale: entryUsedLocale,
|
|
90
|
+
fill: filePath,
|
|
91
|
+
format,
|
|
92
|
+
localId: `${namespaceKey}::${location}::${filePath}`,
|
|
93
|
+
location,
|
|
94
|
+
filled,
|
|
95
|
+
content: namespaceContent,
|
|
96
|
+
filePath,
|
|
97
|
+
priority
|
|
98
|
+
});
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
dictionaries.push({
|
|
81
102
|
key,
|
|
82
103
|
locale: entryUsedLocale,
|
|
83
104
|
fill: filePath,
|
|
84
105
|
format,
|
|
85
106
|
localId: `${key}::${location}::${filePath}`,
|
|
86
107
|
location,
|
|
87
|
-
filled
|
|
108
|
+
filled,
|
|
88
109
|
content: json,
|
|
89
110
|
filePath,
|
|
90
111
|
priority
|
|
91
|
-
};
|
|
92
|
-
dictionaries.push(dictionary);
|
|
112
|
+
});
|
|
93
113
|
}
|
|
94
114
|
return dictionaries;
|
|
95
115
|
}
|
|
@@ -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":["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 /**\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\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 // When the source pattern has no `{{key}}` segment, a single file holds\n // every namespace as a first-level key (the next-intl / react-intl model).\n // In that case each top-level key becomes its own dictionary.\n const patternMarker = await parseFilePathPattern(options.source, {\n key: '{{key}}',\n locale: '{{locale}}',\n } as any as FilePathPatternContext);\n const hasKeyPlaceholder = patternMarker.includes('{{key}}');\n const shouldSplitByKeys = options.splitKeys ?? !hasKeyPlaceholder;\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 filled =\n entryUsedLocale !== configuration.internationalization.defaultLocale\n ? true\n : undefined;\n\n if (shouldSplitByKeys) {\n for (const [namespaceKey, namespaceContent] of Object.entries(json)) {\n dictionaries.push({\n key: namespaceKey,\n locale: entryUsedLocale,\n fill: filePath,\n format,\n localId:\n `${namespaceKey}::${location}::${filePath}` as LocalDictionaryId,\n location: location as Dictionary['location'],\n filled,\n content: namespaceContent as JSONContent,\n filePath,\n priority,\n });\n }\n continue;\n }\n\n dictionaries.push({\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 content: json,\n filePath,\n priority,\n });\n }\n\n return dictionaries;\n },\n };\n};\n"],"mappings":"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"}
|
package/dist/cjs/syncJSON.cjs
CHANGED
|
@@ -96,14 +96,17 @@ const loadMessagePathMap = async (source, configuration) => {
|
|
|
96
96
|
}));
|
|
97
97
|
};
|
|
98
98
|
const syncJSON = async (options) => {
|
|
99
|
+
const patternMarker = await (0, _intlayer_config_utils.parseFilePathPattern)(options.source, {
|
|
100
|
+
key: "{{key}}",
|
|
101
|
+
locale: "{{locale}}"
|
|
102
|
+
});
|
|
99
103
|
const { location, priority, format } = {
|
|
100
|
-
location: `sync-json::${
|
|
101
|
-
key: "{{key}}",
|
|
102
|
-
locale: "{{locale}}"
|
|
103
|
-
})}`,
|
|
104
|
+
location: `sync-json::${patternMarker}`,
|
|
104
105
|
priority: 0,
|
|
105
106
|
...options
|
|
106
107
|
};
|
|
108
|
+
const hasKeyPlaceholder = patternMarker.includes("{{key}}");
|
|
109
|
+
const shouldSplitByKeys = options.splitKeys ?? !hasKeyPlaceholder;
|
|
107
110
|
return {
|
|
108
111
|
name: "sync-json",
|
|
109
112
|
loadDictionaries: async ({ configuration }) => {
|
|
@@ -122,25 +125,41 @@ const syncJSON = async (options) => {
|
|
|
122
125
|
for (const { locale, path, key } of dictionariesMap) {
|
|
123
126
|
const json = await (0, _intlayer_config_file.loadExternalFile)(path, { logError: false }) ?? {};
|
|
124
127
|
const filePath = (0, node_path.relative)(configuration.system.baseDir, path);
|
|
125
|
-
const
|
|
128
|
+
const filled = locale !== configuration.internationalization.defaultLocale ? true : void 0;
|
|
129
|
+
if (shouldSplitByKeys) {
|
|
130
|
+
for (const [namespaceKey, namespaceContent] of Object.entries(json)) dictionaries.push({
|
|
131
|
+
key: namespaceKey,
|
|
132
|
+
locale,
|
|
133
|
+
fill,
|
|
134
|
+
format,
|
|
135
|
+
localId: `${namespaceKey}::${location}::${filePath}`,
|
|
136
|
+
location,
|
|
137
|
+
filled,
|
|
138
|
+
content: namespaceContent,
|
|
139
|
+
filePath,
|
|
140
|
+
priority
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
dictionaries.push({
|
|
126
145
|
key,
|
|
127
146
|
locale,
|
|
128
147
|
fill,
|
|
129
148
|
format,
|
|
130
149
|
localId: `${key}::${location}::${filePath}`,
|
|
131
150
|
location,
|
|
132
|
-
filled
|
|
151
|
+
filled,
|
|
133
152
|
content: json,
|
|
134
153
|
filePath,
|
|
135
154
|
priority
|
|
136
|
-
};
|
|
137
|
-
dictionaries.push(dictionary);
|
|
155
|
+
});
|
|
138
156
|
}
|
|
139
157
|
return dictionaries;
|
|
140
158
|
},
|
|
141
159
|
formatOutput: async ({ dictionary, configuration }) => {
|
|
142
160
|
const { formatDictionaryOutput } = await import("@intlayer/chokidar/build");
|
|
143
161
|
if (!dictionary.filePath || !dictionary.locale) return dictionary;
|
|
162
|
+
if (shouldSplitByKeys) return dictionary;
|
|
144
163
|
const builderPath = await (0, _intlayer_config_utils.parseFilePathPattern)(options.source, {
|
|
145
164
|
key: dictionary.key,
|
|
146
165
|
locale: dictionary.locale
|
|
@@ -153,6 +172,32 @@ const syncJSON = async (options) => {
|
|
|
153
172
|
const { parallelize } = await import("@intlayer/chokidar/utils");
|
|
154
173
|
const { formatDictionaryOutput } = await import("@intlayer/chokidar/build");
|
|
155
174
|
const { locales } = configuration.internationalization;
|
|
175
|
+
if (shouldSplitByKeys) {
|
|
176
|
+
const mergedByLocale = {};
|
|
177
|
+
const filePathByLocale = {};
|
|
178
|
+
for (const [key, entry] of Object.entries(dictionaries.mergedDictionaries)) {
|
|
179
|
+
const dictionary = entry.dictionary;
|
|
180
|
+
if (dictionary.location !== location) continue;
|
|
181
|
+
for (const locale of locales) {
|
|
182
|
+
const formattedOutput = formatDictionaryOutput(getPerLocaleDictionary(dictionary, locale), format);
|
|
183
|
+
const content = JSON.parse(JSON.stringify(formattedOutput.content));
|
|
184
|
+
if (typeof content === "undefined" || typeof content === "object" && content !== null && Object.keys(content).length === 0) continue;
|
|
185
|
+
mergedByLocale[locale] ??= {};
|
|
186
|
+
mergedByLocale[locale][key] = content;
|
|
187
|
+
filePathByLocale[locale] = await (0, _intlayer_config_utils.parseFilePathPattern)(options.source, {
|
|
188
|
+
key,
|
|
189
|
+
locale
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
await parallelize(Object.keys(mergedByLocale), async (locale) => {
|
|
194
|
+
const builderPath = filePathByLocale[locale];
|
|
195
|
+
if (!builderPath) return;
|
|
196
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(builderPath), { recursive: true });
|
|
197
|
+
await (0, node_fs_promises.writeFile)(builderPath, `${JSON.stringify(mergedByLocale[locale], null, 2)}\n`, "utf-8");
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
156
201
|
await parallelize(Object.entries(dictionaries.mergedDictionaries).flatMap(([key, dictionary]) => locales.map((locale) => ({
|
|
157
202
|
key,
|
|
158
203
|
dictionary: dictionary.dictionary,
|
|
@@ -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":[],"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 /**\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\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 // When the source pattern has no `{{key}}` segment, a single file holds every\n // namespace as a first-level key (the next-intl / react-intl model). In that\n // case each top-level key becomes its own dictionary. Can be forced via the\n // `splitKeys` option.\n const hasKeyPlaceholder = patternMarker.includes('{{key}}');\n const shouldSplitByKeys = options.splitKeys ?? !hasKeyPlaceholder;\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 filled =\n locale !== configuration.internationalization.defaultLocale\n ? true\n : undefined;\n\n // One file groups several namespaces by its first-level keys: emit one\n // dictionary per top-level key (e.g. `Hero`, `Nav`, …).\n if (shouldSplitByKeys) {\n for (const [namespaceKey, namespaceContent] of Object.entries(json)) {\n dictionaries.push({\n key: namespaceKey,\n locale,\n fill,\n format,\n localId:\n `${namespaceKey}::${location}::${filePath}` as LocalDictionaryId,\n location: location as Dictionary['location'],\n filled,\n content: namespaceContent as JSONContent,\n filePath,\n priority,\n });\n }\n continue;\n }\n\n dictionaries.push({\n key,\n locale,\n fill,\n format,\n localId: `${key}::${location}::${filePath}` as LocalDictionaryId,\n location: location as Dictionary['location'],\n filled,\n content: json,\n filePath,\n priority,\n });\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 // In split mode several namespaces share the same file; the file is\n // re-assembled in `afterBuild`. Skip here to avoid overwriting the whole\n // file with a single namespace.\n if (shouldSplitByKeys) 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 // Split mode: every namespace dictionary writes back into the same\n // per-locale file. Re-assemble them under their top-level key and write\n // each file once, instead of one file per key (which would overwrite).\n if (shouldSplitByKeys) {\n const mergedByLocale: Record<string, Record<string, unknown>> = {};\n const filePathByLocale: Record<string, string> = {};\n\n for (const [key, entry] of Object.entries(\n dictionaries.mergedDictionaries\n )) {\n const dictionary = entry.dictionary as Dictionary;\n\n // Only process dictionaries that belong to THIS plugin instance.\n if (dictionary.location !== location) continue;\n\n for (const locale of locales) {\n const localizedDictionary = getPerLocaleDictionary(\n dictionary,\n locale\n );\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 content !== null &&\n Object.keys(content as Record<string, unknown>).length === 0)\n ) {\n continue;\n }\n\n mergedByLocale[locale] ??= {};\n mergedByLocale[locale][key] = content;\n filePathByLocale[locale] = await parseFilePathPattern(\n options.source,\n { key, locale } as any as FilePathPatternContext\n );\n }\n }\n\n await parallelize(Object.keys(mergedByLocale), async (locale) => {\n const builderPath = filePathByLocale[locale];\n\n if (!builderPath) return;\n\n await mkdir(dirname(builderPath), { recursive: true });\n\n const stringContent = JSON.stringify(mergedByLocale[locale], null, 2);\n\n await writeFile(builderPath, `${stringContent}\\n`, 'utf-8');\n });\n\n return;\n }\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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"}
|
package/dist/esm/loadJSON.mjs
CHANGED
|
@@ -69,24 +69,44 @@ const loadJSON = (options) => {
|
|
|
69
69
|
key: "{{key}}",
|
|
70
70
|
locale: "{{locale}}"
|
|
71
71
|
}))}`, { level: "warn" });
|
|
72
|
+
const hasKeyPlaceholder = (await parseFilePathPattern(options.source, {
|
|
73
|
+
key: "{{key}}",
|
|
74
|
+
locale: "{{locale}}"
|
|
75
|
+
})).includes("{{key}}");
|
|
76
|
+
const shouldSplitByKeys = options.splitKeys ?? !hasKeyPlaceholder;
|
|
72
77
|
const dictionaries = [];
|
|
73
78
|
for (const { path, key, locale: entryLocale } of dictionariesMap) {
|
|
74
79
|
const json = await loadExternalFile(path, { logError: false }) ?? {};
|
|
75
80
|
const filePath = relative(configuration.system.baseDir, path);
|
|
76
81
|
const entryUsedLocale = locale ?? entryLocale;
|
|
77
|
-
const
|
|
82
|
+
const filled = entryUsedLocale !== configuration.internationalization.defaultLocale ? true : void 0;
|
|
83
|
+
if (shouldSplitByKeys) {
|
|
84
|
+
for (const [namespaceKey, namespaceContent] of Object.entries(json)) dictionaries.push({
|
|
85
|
+
key: namespaceKey,
|
|
86
|
+
locale: entryUsedLocale,
|
|
87
|
+
fill: filePath,
|
|
88
|
+
format,
|
|
89
|
+
localId: `${namespaceKey}::${location}::${filePath}`,
|
|
90
|
+
location,
|
|
91
|
+
filled,
|
|
92
|
+
content: namespaceContent,
|
|
93
|
+
filePath,
|
|
94
|
+
priority
|
|
95
|
+
});
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
dictionaries.push({
|
|
78
99
|
key,
|
|
79
100
|
locale: entryUsedLocale,
|
|
80
101
|
fill: filePath,
|
|
81
102
|
format,
|
|
82
103
|
localId: `${key}::${location}::${filePath}`,
|
|
83
104
|
location,
|
|
84
|
-
filled
|
|
105
|
+
filled,
|
|
85
106
|
content: json,
|
|
86
107
|
filePath,
|
|
87
108
|
priority
|
|
88
|
-
};
|
|
89
|
-
dictionaries.push(dictionary);
|
|
109
|
+
});
|
|
90
110
|
}
|
|
91
111
|
return dictionaries;
|
|
92
112
|
}
|
|
@@ -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":["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 /**\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\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 // When the source pattern has no `{{key}}` segment, a single file holds\n // every namespace as a first-level key (the next-intl / react-intl model).\n // In that case each top-level key becomes its own dictionary.\n const patternMarker = await parseFilePathPattern(options.source, {\n key: '{{key}}',\n locale: '{{locale}}',\n } as any as FilePathPatternContext);\n const hasKeyPlaceholder = patternMarker.includes('{{key}}');\n const shouldSplitByKeys = options.splitKeys ?? !hasKeyPlaceholder;\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 filled =\n entryUsedLocale !== configuration.internationalization.defaultLocale\n ? true\n : undefined;\n\n if (shouldSplitByKeys) {\n for (const [namespaceKey, namespaceContent] of Object.entries(json)) {\n dictionaries.push({\n key: namespaceKey,\n locale: entryUsedLocale,\n fill: filePath,\n format,\n localId:\n `${namespaceKey}::${location}::${filePath}` as LocalDictionaryId,\n location: location as Dictionary['location'],\n filled,\n content: namespaceContent as JSONContent,\n filePath,\n priority,\n });\n }\n continue;\n }\n\n dictionaries.push({\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 content: json,\n filePath,\n priority,\n });\n }\n\n return dictionaries;\n },\n };\n};\n"],"mappings":"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"}
|
package/dist/esm/syncJSON.mjs
CHANGED
|
@@ -93,14 +93,17 @@ const loadMessagePathMap = async (source, configuration) => {
|
|
|
93
93
|
}));
|
|
94
94
|
};
|
|
95
95
|
const syncJSON = async (options) => {
|
|
96
|
+
const patternMarker = await parseFilePathPattern(options.source, {
|
|
97
|
+
key: "{{key}}",
|
|
98
|
+
locale: "{{locale}}"
|
|
99
|
+
});
|
|
96
100
|
const { location, priority, format } = {
|
|
97
|
-
location: `sync-json::${
|
|
98
|
-
key: "{{key}}",
|
|
99
|
-
locale: "{{locale}}"
|
|
100
|
-
})}`,
|
|
101
|
+
location: `sync-json::${patternMarker}`,
|
|
101
102
|
priority: 0,
|
|
102
103
|
...options
|
|
103
104
|
};
|
|
105
|
+
const hasKeyPlaceholder = patternMarker.includes("{{key}}");
|
|
106
|
+
const shouldSplitByKeys = options.splitKeys ?? !hasKeyPlaceholder;
|
|
104
107
|
return {
|
|
105
108
|
name: "sync-json",
|
|
106
109
|
loadDictionaries: async ({ configuration }) => {
|
|
@@ -119,25 +122,41 @@ const syncJSON = async (options) => {
|
|
|
119
122
|
for (const { locale, path, key } of dictionariesMap) {
|
|
120
123
|
const json = await loadExternalFile(path, { logError: false }) ?? {};
|
|
121
124
|
const filePath = relative(configuration.system.baseDir, path);
|
|
122
|
-
const
|
|
125
|
+
const filled = locale !== configuration.internationalization.defaultLocale ? true : void 0;
|
|
126
|
+
if (shouldSplitByKeys) {
|
|
127
|
+
for (const [namespaceKey, namespaceContent] of Object.entries(json)) dictionaries.push({
|
|
128
|
+
key: namespaceKey,
|
|
129
|
+
locale,
|
|
130
|
+
fill,
|
|
131
|
+
format,
|
|
132
|
+
localId: `${namespaceKey}::${location}::${filePath}`,
|
|
133
|
+
location,
|
|
134
|
+
filled,
|
|
135
|
+
content: namespaceContent,
|
|
136
|
+
filePath,
|
|
137
|
+
priority
|
|
138
|
+
});
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
dictionaries.push({
|
|
123
142
|
key,
|
|
124
143
|
locale,
|
|
125
144
|
fill,
|
|
126
145
|
format,
|
|
127
146
|
localId: `${key}::${location}::${filePath}`,
|
|
128
147
|
location,
|
|
129
|
-
filled
|
|
148
|
+
filled,
|
|
130
149
|
content: json,
|
|
131
150
|
filePath,
|
|
132
151
|
priority
|
|
133
|
-
};
|
|
134
|
-
dictionaries.push(dictionary);
|
|
152
|
+
});
|
|
135
153
|
}
|
|
136
154
|
return dictionaries;
|
|
137
155
|
},
|
|
138
156
|
formatOutput: async ({ dictionary, configuration }) => {
|
|
139
157
|
const { formatDictionaryOutput } = await import("@intlayer/chokidar/build");
|
|
140
158
|
if (!dictionary.filePath || !dictionary.locale) return dictionary;
|
|
159
|
+
if (shouldSplitByKeys) return dictionary;
|
|
141
160
|
const builderPath = await parseFilePathPattern(options.source, {
|
|
142
161
|
key: dictionary.key,
|
|
143
162
|
locale: dictionary.locale
|
|
@@ -150,6 +169,32 @@ const syncJSON = async (options) => {
|
|
|
150
169
|
const { parallelize } = await import("@intlayer/chokidar/utils");
|
|
151
170
|
const { formatDictionaryOutput } = await import("@intlayer/chokidar/build");
|
|
152
171
|
const { locales } = configuration.internationalization;
|
|
172
|
+
if (shouldSplitByKeys) {
|
|
173
|
+
const mergedByLocale = {};
|
|
174
|
+
const filePathByLocale = {};
|
|
175
|
+
for (const [key, entry] of Object.entries(dictionaries.mergedDictionaries)) {
|
|
176
|
+
const dictionary = entry.dictionary;
|
|
177
|
+
if (dictionary.location !== location) continue;
|
|
178
|
+
for (const locale of locales) {
|
|
179
|
+
const formattedOutput = formatDictionaryOutput(getPerLocaleDictionary(dictionary, locale), format);
|
|
180
|
+
const content = JSON.parse(JSON.stringify(formattedOutput.content));
|
|
181
|
+
if (typeof content === "undefined" || typeof content === "object" && content !== null && Object.keys(content).length === 0) continue;
|
|
182
|
+
mergedByLocale[locale] ??= {};
|
|
183
|
+
mergedByLocale[locale][key] = content;
|
|
184
|
+
filePathByLocale[locale] = await parseFilePathPattern(options.source, {
|
|
185
|
+
key,
|
|
186
|
+
locale
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
await parallelize(Object.keys(mergedByLocale), async (locale) => {
|
|
191
|
+
const builderPath = filePathByLocale[locale];
|
|
192
|
+
if (!builderPath) return;
|
|
193
|
+
await mkdir(dirname(builderPath), { recursive: true });
|
|
194
|
+
await writeFile(builderPath, `${JSON.stringify(mergedByLocale[locale], null, 2)}\n`, "utf-8");
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
153
198
|
await parallelize(Object.entries(dictionaries.mergedDictionaries).flatMap(([key, dictionary]) => locales.map((locale) => ({
|
|
154
199
|
key,
|
|
155
200
|
dictionary: dictionary.dictionary,
|
|
@@ -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 { 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 /**\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\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 // When the source pattern has no `{{key}}` segment, a single file holds every\n // namespace as a first-level key (the next-intl / react-intl model). In that\n // case each top-level key becomes its own dictionary. Can be forced via the\n // `splitKeys` option.\n const hasKeyPlaceholder = patternMarker.includes('{{key}}');\n const shouldSplitByKeys = options.splitKeys ?? !hasKeyPlaceholder;\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 filled =\n locale !== configuration.internationalization.defaultLocale\n ? true\n : undefined;\n\n // One file groups several namespaces by its first-level keys: emit one\n // dictionary per top-level key (e.g. `Hero`, `Nav`, …).\n if (shouldSplitByKeys) {\n for (const [namespaceKey, namespaceContent] of Object.entries(json)) {\n dictionaries.push({\n key: namespaceKey,\n locale,\n fill,\n format,\n localId:\n `${namespaceKey}::${location}::${filePath}` as LocalDictionaryId,\n location: location as Dictionary['location'],\n filled,\n content: namespaceContent as JSONContent,\n filePath,\n priority,\n });\n }\n continue;\n }\n\n dictionaries.push({\n key,\n locale,\n fill,\n format,\n localId: `${key}::${location}::${filePath}` as LocalDictionaryId,\n location: location as Dictionary['location'],\n filled,\n content: json,\n filePath,\n priority,\n });\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 // In split mode several namespaces share the same file; the file is\n // re-assembled in `afterBuild`. Skip here to avoid overwriting the whole\n // file with a single namespace.\n if (shouldSplitByKeys) 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 // Split mode: every namespace dictionary writes back into the same\n // per-locale file. Re-assemble them under their top-level key and write\n // each file once, instead of one file per key (which would overwrite).\n if (shouldSplitByKeys) {\n const mergedByLocale: Record<string, Record<string, unknown>> = {};\n const filePathByLocale: Record<string, string> = {};\n\n for (const [key, entry] of Object.entries(\n dictionaries.mergedDictionaries\n )) {\n const dictionary = entry.dictionary as Dictionary;\n\n // Only process dictionaries that belong to THIS plugin instance.\n if (dictionary.location !== location) continue;\n\n for (const locale of locales) {\n const localizedDictionary = getPerLocaleDictionary(\n dictionary,\n locale\n );\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 content !== null &&\n Object.keys(content as Record<string, unknown>).length === 0)\n ) {\n continue;\n }\n\n mergedByLocale[locale] ??= {};\n mergedByLocale[locale][key] = content;\n filePathByLocale[locale] = await parseFilePathPattern(\n options.source,\n { key, locale } as any as FilePathPatternContext\n );\n }\n }\n\n await parallelize(Object.keys(mergedByLocale), async (locale) => {\n const builderPath = filePathByLocale[locale];\n\n if (!builderPath) return;\n\n await mkdir(dirname(builderPath), { recursive: true });\n\n const stringContent = JSON.stringify(mergedByLocale[locale], null, 2);\n\n await writeFile(builderPath, `${stringContent}\\n`, 'utf-8');\n });\n\n return;\n }\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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"}
|
package/dist/types/loadJSON.d.ts
CHANGED
|
@@ -74,6 +74,19 @@ type LoadJSONPluginOptions = {
|
|
|
74
74
|
* ```
|
|
75
75
|
*/
|
|
76
76
|
format?: DictionaryFormat;
|
|
77
|
+
/**
|
|
78
|
+
* Whether each top-level key of the JSON file should become its own
|
|
79
|
+
* dictionary (keyed by that top-level key) instead of a single dictionary
|
|
80
|
+
* holding the whole file.
|
|
81
|
+
*
|
|
82
|
+
* This matches the namespace model of libraries such as `next-intl` /
|
|
83
|
+
* `react-intl`, where a single `messages/{locale}.json` file groups several
|
|
84
|
+
* namespaces by its first-level keys.
|
|
85
|
+
*
|
|
86
|
+
* When omitted, it is auto-detected: the file is split when the `source`
|
|
87
|
+
* pattern has no `{{key}}` segment, and kept as a single dictionary otherwise.
|
|
88
|
+
*/
|
|
89
|
+
splitKeys?: boolean;
|
|
77
90
|
};
|
|
78
91
|
declare const loadJSON: (options: LoadJSONPluginOptions) => Plugin;
|
|
79
92
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loadJSON.d.ts","names":[],"sources":["../../src/loadJSON.ts"],"mappings":";;;;;;KAiJK,qBAAA;;AAlIgD;;;;;;;;;;EA8InD,MAAA,EAAQ,eAAA;EAeC
|
|
1
|
+
{"version":3,"file":"loadJSON.d.ts","names":[],"sources":["../../src/loadJSON.ts"],"mappings":";;;;;;KAiJK,qBAAA;;AAlIgD;;;;;;;;;;EA8InD,MAAA,EAAQ,eAAA;EAeC;;;;;;;;AA+DX;;;;;EA/DE,MAAA,GAAS,MAAA;EA+D+C;;;;;;;;;;;;;;;;;;;;;;EAvCxD,QAAA;;;;;;;;;EAUA,QAAA;;;;;;;;;;;EAYA,MAAA,GAAS,gBAAA;;;;;;;;;;;;;EAcT,SAAA;AAAA;AAAA,cAGW,QAAA,GAAY,OAAA,EAAS,qBAAA,KAAwB,MAAA"}
|
package/dist/types/syncJSON.d.ts
CHANGED
|
@@ -60,6 +60,30 @@ type SyncJSONPluginOptions = {
|
|
|
60
60
|
* The format of the dictionaries created by the plugin.
|
|
61
61
|
*/
|
|
62
62
|
format?: DictionaryFormat;
|
|
63
|
+
/**
|
|
64
|
+
* Whether each top-level key of the JSON file should become its own
|
|
65
|
+
* dictionary (keyed by that top-level key) instead of a single dictionary
|
|
66
|
+
* holding the whole file.
|
|
67
|
+
*
|
|
68
|
+
* This matches the namespace model of libraries such as `next-intl` /
|
|
69
|
+
* `react-intl`, where a single `messages/{locale}.json` file groups several
|
|
70
|
+
* namespaces by its first-level keys and each namespace is addressed
|
|
71
|
+
* independently (e.g. `useTranslations('Hero')` → dictionary `Hero`).
|
|
72
|
+
*
|
|
73
|
+
* When omitted, it is auto-detected: the file is split when the `source`
|
|
74
|
+
* pattern has no `{{key}}` segment (i.e. one file holds every namespace),
|
|
75
|
+
* and kept as a single dictionary otherwise (one file per key).
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* // messages/en.json → dictionaries: Hero, Nav, About, …
|
|
80
|
+
* syncJSON({
|
|
81
|
+
* source: ({ locale }) => `./messages/${locale}.json`,
|
|
82
|
+
* splitKeys: true,
|
|
83
|
+
* })
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
splitKeys?: boolean;
|
|
63
87
|
};
|
|
64
88
|
declare const syncJSON: (options: SyncJSONPluginOptions) => Promise<Plugin>;
|
|
65
89
|
//#endregion
|
|
@@ -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;
|
|
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;EA+CA;;;;;;;;EArCX,QAAA;EAsCA;;;;;;;EA7BA,MAAA,GAAS,gBAAA;;;;;;;;;;;;;;;;;;;;;;;;EAyBT,SAAA;AAAA;AAAA,cAGW,QAAA,GACX,OAAA,EAAS,qBAAA,KACR,OAAA,CAAQ,MAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intlayer/sync-json-plugin",
|
|
3
|
-
"version": "9.0.0-canary.
|
|
3
|
+
"version": "9.0.0-canary.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A plugin for Intlayer that syncs JSON files to dictionaries.",
|
|
6
6
|
"keywords": [
|
|
@@ -71,14 +71,14 @@
|
|
|
71
71
|
"typecheck": "tsc --noEmit --project tsconfig.types.json"
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
|
-
"@intlayer/chokidar": "9.0.0-canary.
|
|
75
|
-
"@intlayer/config": "9.0.0-canary.
|
|
76
|
-
"@intlayer/core": "9.0.0-canary.
|
|
77
|
-
"@intlayer/types": "9.0.0-canary.
|
|
74
|
+
"@intlayer/chokidar": "9.0.0-canary.4",
|
|
75
|
+
"@intlayer/config": "9.0.0-canary.4",
|
|
76
|
+
"@intlayer/core": "9.0.0-canary.4",
|
|
77
|
+
"@intlayer/types": "9.0.0-canary.4",
|
|
78
78
|
"fast-glob": "3.3.3"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
|
-
"@types/node": "25.9.
|
|
81
|
+
"@types/node": "25.9.4",
|
|
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",
|