@intlayer/engine 9.2.0 → 9.3.0
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/buildIntlayerDictionary/buildIntlayerDictionary.cjs +14 -2
- package/dist/cjs/buildIntlayerDictionary/buildIntlayerDictionary.cjs.map +1 -1
- package/dist/cjs/buildIntlayerDictionary/writeDynamicDictionary.cjs +24 -4
- package/dist/cjs/buildIntlayerDictionary/writeDynamicDictionary.cjs.map +1 -1
- package/dist/cjs/buildIntlayerDictionary/writeFetchDictionary.cjs +43 -6
- package/dist/cjs/buildIntlayerDictionary/writeFetchDictionary.cjs.map +1 -1
- package/dist/cjs/buildIntlayerDictionary/writeNestedDictionary.cjs +96 -0
- package/dist/cjs/buildIntlayerDictionary/writeNestedDictionary.cjs.map +1 -0
- package/dist/cjs/fetchDistantDictionaries.cjs +107 -13
- package/dist/cjs/fetchDistantDictionaries.cjs.map +1 -1
- package/dist/cjs/init/frameworkSetup/tanstackStart/transforms.cjs +1 -1
- package/dist/cjs/init/frameworkSetup/tanstackStart/transforms.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/loadDictionaries.cjs +4 -2
- package/dist/cjs/loadDictionaries/loadDictionaries.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs +3 -2
- package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/log.cjs +28 -5
- package/dist/cjs/loadDictionaries/log.cjs.map +1 -1
- package/dist/cjs/utils/chunkArray.cjs +24 -0
- package/dist/cjs/utils/chunkArray.cjs.map +1 -0
- package/dist/cjs/utils/index.cjs +2 -0
- package/dist/cjs/writeContentDeclaration/transformJSFile.cjs +153 -92
- package/dist/cjs/writeContentDeclaration/transformJSFile.cjs.map +1 -1
- package/dist/esm/buildIntlayerDictionary/buildIntlayerDictionary.mjs +15 -3
- package/dist/esm/buildIntlayerDictionary/buildIntlayerDictionary.mjs.map +1 -1
- package/dist/esm/buildIntlayerDictionary/writeDynamicDictionary.mjs +24 -4
- package/dist/esm/buildIntlayerDictionary/writeDynamicDictionary.mjs.map +1 -1
- package/dist/esm/buildIntlayerDictionary/writeFetchDictionary.mjs +43 -6
- package/dist/esm/buildIntlayerDictionary/writeFetchDictionary.mjs.map +1 -1
- package/dist/esm/buildIntlayerDictionary/writeNestedDictionary.mjs +92 -0
- package/dist/esm/buildIntlayerDictionary/writeNestedDictionary.mjs.map +1 -0
- package/dist/esm/fetchDistantDictionaries.mjs +107 -13
- package/dist/esm/fetchDistantDictionaries.mjs.map +1 -1
- package/dist/esm/init/frameworkSetup/tanstackStart/transforms.mjs +1 -1
- package/dist/esm/init/frameworkSetup/tanstackStart/transforms.mjs.map +1 -1
- package/dist/esm/loadDictionaries/loadDictionaries.mjs +4 -2
- package/dist/esm/loadDictionaries/loadDictionaries.mjs.map +1 -1
- package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs +3 -2
- package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs.map +1 -1
- package/dist/esm/loadDictionaries/log.mjs +28 -5
- package/dist/esm/loadDictionaries/log.mjs.map +1 -1
- package/dist/esm/utils/chunkArray.mjs +22 -0
- package/dist/esm/utils/chunkArray.mjs.map +1 -0
- package/dist/esm/utils/index.mjs +2 -1
- package/dist/esm/writeContentDeclaration/transformJSFile.mjs +153 -92
- package/dist/esm/writeContentDeclaration/transformJSFile.mjs.map +1 -1
- package/dist/types/buildIntlayerDictionary/buildIntlayerDictionary.d.ts.map +1 -1
- package/dist/types/buildIntlayerDictionary/writeDynamicDictionary.d.ts +14 -2
- package/dist/types/buildIntlayerDictionary/writeDynamicDictionary.d.ts.map +1 -1
- package/dist/types/buildIntlayerDictionary/writeFetchDictionary.d.ts +16 -2
- package/dist/types/buildIntlayerDictionary/writeFetchDictionary.d.ts.map +1 -1
- package/dist/types/buildIntlayerDictionary/writeNestedDictionary.d.ts +54 -0
- package/dist/types/buildIntlayerDictionary/writeNestedDictionary.d.ts.map +1 -0
- package/dist/types/fetchDistantDictionaries.d.ts +26 -0
- package/dist/types/fetchDistantDictionaries.d.ts.map +1 -1
- package/dist/types/formatDictionary.d.ts +2 -0
- package/dist/types/formatDictionary.d.ts.map +1 -1
- package/dist/types/loadDictionaries/log.d.ts +16 -1
- package/dist/types/loadDictionaries/log.d.ts.map +1 -1
- package/dist/types/utils/chunkArray.d.ts +16 -0
- package/dist/types/utils/chunkArray.d.ts.map +1 -0
- package/dist/types/utils/index.d.ts +2 -1
- package/dist/types/writeContentDeclaration/transformJSFile.d.ts.map +1 -1
- package/package.json +11 -11
|
@@ -17,15 +17,35 @@ const DICTIONARIES_SUBDIR = "json";
|
|
|
17
17
|
* emitted JavaScript.
|
|
18
18
|
*/
|
|
19
19
|
const escapeJsLiteral = (value) => value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
20
|
+
/** Loader expression for one `(key, locale)` per-locale chunk. */
|
|
21
|
+
const buildPerLocaleLoader = (key, locale, format) => {
|
|
22
|
+
const path = `./${DICTIONARIES_SUBDIR}/${key}/${locale}.json`;
|
|
23
|
+
return format === "esm" ? `import('${path}').then(m => m.default)` : `Promise.resolve(require('${path}'))`;
|
|
24
|
+
};
|
|
20
25
|
/**
|
|
21
26
|
* Generates the content of a dictionary entry point file.
|
|
27
|
+
*
|
|
28
|
+
* When the dictionary references other dictionaries through `nest()`, the
|
|
29
|
+
* loader for a locale resolves the referenced dictionaries alongside it and
|
|
30
|
+
* attaches them as `nestedDictionaries`. The nest targets then travel in the
|
|
31
|
+
* same lazy chunk as their consumer, and `getNesting` resolves them from that
|
|
32
|
+
* local reference instead of the global registry — which the build
|
|
33
|
+
* optimization strips.
|
|
34
|
+
*
|
|
35
|
+
* @param key - The dictionary key.
|
|
36
|
+
* @param locales - Locales to emit a loader for.
|
|
37
|
+
* @param format - Output module format.
|
|
38
|
+
* @param nestedKeys - Keys referenced through `nest()`, transitively.
|
|
22
39
|
*/
|
|
23
|
-
const generateDictionaryEntryPoint = (key, locales, format = "esm") => {
|
|
40
|
+
const generateDictionaryEntryPoint = (key, locales, format = "esm", nestedKeys = []) => {
|
|
24
41
|
const sortedLocales = [...locales].sort((a, b) => String(a).localeCompare(String(b)));
|
|
42
|
+
const sortedNestedKeys = [...nestedKeys].sort((a, b) => a.localeCompare(b));
|
|
25
43
|
const safeKey = escapeJsLiteral(key);
|
|
26
44
|
const localeEntries = sortedLocales.map((locale) => {
|
|
27
45
|
const safeLocale = escapeJsLiteral(locale);
|
|
28
|
-
|
|
46
|
+
const ownLoader = buildPerLocaleLoader(safeKey, safeLocale, format);
|
|
47
|
+
if (sortedNestedKeys.length === 0) return ` '${safeLocale}': () => ${ownLoader}`;
|
|
48
|
+
return ` '${safeLocale}': () => Promise.all([\n ${ownLoader},\n ${sortedNestedKeys.map((nestedKey) => buildPerLocaleLoader(escapeJsLiteral(nestedKey), safeLocale, format)).join(",\n ")}\n ]).then(([_dictionary, ..._nested]) => ({\n ..._dictionary,\n nestedDictionaries: { ${sortedNestedKeys.map((nestedKey, index) => `'${escapeJsLiteral(nestedKey)}': _nested[${index}]`).join(", ")} }\n }))`;
|
|
29
49
|
}).join(",\n");
|
|
30
50
|
if (format === "esm") return `const content = {\n${localeEntries}\n};\n\nexport default content;\n`;
|
|
31
51
|
return `module.exports = {\n${localeEntries}\n};\n`;
|
|
@@ -94,7 +114,7 @@ const generateQualifiedDictionaryEntryPoint = (key, qualifierTypes, entries, loc
|
|
|
94
114
|
* // .intlayer/dynamic_dictionary/dictionaries/fr_home.json
|
|
95
115
|
* ```
|
|
96
116
|
*/
|
|
97
|
-
const writeDynamicDictionary = async (mergedDictionaries, configuration, formats = OUTPUT_FORMAT) => {
|
|
117
|
+
const writeDynamicDictionary = async (mergedDictionaries, configuration, formats = OUTPUT_FORMAT, nestedDictionaryGraph = /* @__PURE__ */ new Map()) => {
|
|
98
118
|
const { locales, defaultLocale } = configuration.internationalization;
|
|
99
119
|
const { dynamicDictionariesDir } = configuration.system;
|
|
100
120
|
const dictDir = resolve(dynamicDictionariesDir, DICTIONARIES_SUBDIR);
|
|
@@ -120,7 +140,7 @@ const writeDynamicDictionary = async (mergedDictionaries, configuration, formats
|
|
|
120
140
|
resultDictionariesPaths[key] = localizedDictionariesPathsRecord;
|
|
121
141
|
await parallelize(formats, async (format) => {
|
|
122
142
|
const extension = format === "cjs" ? "cjs" : "mjs";
|
|
123
|
-
const content = generateDictionaryEntryPoint(key, locales, format);
|
|
143
|
+
const content = generateDictionaryEntryPoint(key, locales, format, [...nestedDictionaryGraph.get(key) ?? []]);
|
|
124
144
|
const dynEntryPath = resolve(dynamicDictionariesDir, `${key}.${extension}`);
|
|
125
145
|
assertPathWithin(dynEntryPath, dynamicDictionariesDir);
|
|
126
146
|
await writeFileIfChanged(dynEntryPath, content).catch((err) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writeDynamicDictionary.mjs","names":[],"sources":["../../../src/buildIntlayerDictionary/writeDynamicDictionary.ts"],"sourcesContent":["import { mkdir } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { OUTPUT_FORMAT } from '@intlayer/config/defaultValues';\nimport { colorizePath } from '@intlayer/config/logger';\nimport { assertPathWithin } from '@intlayer/config/utils';\nimport {\n COMPOSITE_ID_SEPARATOR,\n QUALIFIER_DYNAMIC_TYPES_KEY,\n reconstructQualifiedEntry,\n} from '@intlayer/core/dictionaryManipulator';\nimport { getPerLocaleDictionary } from '@intlayer/core/plugins';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type {\n Dictionary,\n DictionaryQualifierType,\n QualifiedDictionaryGroup,\n} from '@intlayer/types/dictionary';\nimport { parallelize } from '../utils/parallelize';\nimport { writeFileIfChanged } from '../writeFileIfChanged';\nimport { writeJsonIfChanged } from '../writeJsonIfChanged';\nimport type { PlainMergedDictionaryOutput } from './writeMergedDictionary';\n\nexport type DictionaryResult = {\n dictionaryPath: string;\n dictionary: Dictionary;\n};\n\nexport type LocalizedDictionaryResult = Partial<\n Record<Locale, DictionaryResult>\n>;\n\nexport type LocalizedDictionaryOutput = Record<\n string,\n LocalizedDictionaryResult\n>;\n\nconst DICTIONARIES_SUBDIR = 'json'; // Necessary to add a static first dir for Turbopack\n\n/**\n * Escapes a value interpolated into a single-quoted literal of a generated\n * loader module, so keys or segments containing `'` or `\\` cannot break the\n * emitted JavaScript.\n */\nconst escapeJsLiteral = (value: string): string =>\n value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\");\n\n/**\n * Generates the content of a dictionary entry point file.\n */\nexport const generateDictionaryEntryPoint = (\n key: string,\n locales: string[],\n format: 'cjs' | 'esm' = 'esm'\n): string => {\n const sortedLocales = [...locales].sort((a, b) =>\n String(a).localeCompare(String(b))\n );\n\n const safeKey = escapeJsLiteral(key);\n\n const localeEntries = sortedLocales\n .map((locale) => {\n const safeLocale = escapeJsLiteral(locale);\n return format === 'esm'\n ? ` '${safeLocale}': () => import('./${DICTIONARIES_SUBDIR}/${safeKey}/${safeLocale}.json').then(m => m.default)`\n : ` '${safeLocale}': () => Promise.resolve(require('./${DICTIONARIES_SUBDIR}/${safeKey}/${safeLocale}.json'))`;\n })\n .join(',\\n');\n\n if (format === 'esm') {\n return (\n `const content = {\\n${localeEntries}\\n};\\n\\n` +\n `export default content;\\n`\n );\n }\n return `module.exports = {\\n${localeEntries}\\n};\\n`;\n};\n\n/**\n * A nested loader tree: one level per declared dimension, leaves are loader\n * expression strings.\n */\ntype LoaderTree = { [segment: string]: LoaderTree | string };\n\n/**\n * One entry of a qualified loader map.\n *\n * `treeSegments` is where the entry lives in the loader tree (its composite id\n * segments); `chunkSegments` is the chunk path it loads. They differ when the\n * entry aliases another entry with identical content (e.g. array-variant\n * fan-out): the alias keeps its own tree position but points at the canonical\n * entry's chunk, so equal content is emitted — and downloaded — only once.\n */\nexport type QualifiedEntrySegments = {\n treeSegments: string[];\n chunkSegments: string[];\n};\n\nconst buildLoaderExpression = (\n key: string,\n chunkSegments: string[],\n locale: string,\n format: 'cjs' | 'esm'\n): string => {\n const path = escapeJsLiteral(\n `./${DICTIONARIES_SUBDIR}/${key}/${chunkSegments.join('/')}/${locale}.json`\n );\n\n return format === 'esm'\n ? `() => import('${path}').then(m => m.default)`\n : `() => Promise.resolve(require('${path}'))`;\n};\n\nconst buildLoaderTree = (\n key: string,\n entries: QualifiedEntrySegments[],\n locale: string,\n format: 'cjs' | 'esm'\n): LoaderTree => {\n const root: LoaderTree = {};\n\n for (const { treeSegments, chunkSegments } of entries) {\n let node = root;\n\n treeSegments.forEach((segment, index) => {\n if (index === treeSegments.length - 1) {\n node[segment] = buildLoaderExpression(\n key,\n chunkSegments,\n locale,\n format\n );\n return;\n }\n\n node[segment] = (node[segment] as LoaderTree | undefined) ?? {};\n node = node[segment] as LoaderTree;\n });\n }\n\n return root;\n};\n\nconst serializeLoaderTree = (tree: LoaderTree, indentLevel: number): string => {\n const pad = ' '.repeat(indentLevel);\n const innerPad = ' '.repeat(indentLevel + 1);\n\n const lines = Object.keys(tree)\n .sort((a, b) => a.localeCompare(b))\n .map((segment) => {\n const value = tree[segment]!;\n const serialized =\n typeof value === 'string'\n ? value\n : serializeLoaderTree(value, indentLevel + 1);\n\n return `${innerPad}'${escapeJsLiteral(segment)}': ${serialized}`;\n });\n\n return `{\\n${lines.join(',\\n')}\\n${pad}}`;\n};\n\n/**\n * Generates the entry point of a qualified dictionary (collection / variant,\n * possibly combined). Under each locale the loader map nests one\n * level per declared dimension (canonical order) and carries a marker listing\n * those dimensions so the runtime can walk the tree.\n *\n * One static `import()` is emitted per leaf `(locale, …segments)` chunk, which\n * keeps the output compatible with bundlers that reject template-literal\n * dynamic imports (Turbopack). Entries whose content is identical share one\n * chunk: their leaves point at the canonical entry's import path.\n */\nexport const generateQualifiedDictionaryEntryPoint = (\n key: string,\n qualifierTypes: DictionaryQualifierType[],\n entries: QualifiedEntrySegments[],\n locales: string[],\n format: 'cjs' | 'esm' = 'esm'\n): string => {\n const sortedLocales = [...locales].sort((a, b) =>\n String(a).localeCompare(String(b))\n );\n\n const localeEntries = sortedLocales\n .map((locale) => {\n const tree = buildLoaderTree(key, entries, locale, format);\n return ` '${escapeJsLiteral(locale)}': ${serializeLoaderTree(tree, 1)}`;\n })\n .join(',\\n');\n\n const marker = ` '${QUALIFIER_DYNAMIC_TYPES_KEY}': ${JSON.stringify(qualifierTypes)}`;\n\n if (format === 'esm') {\n return (\n `const content = {\\n${marker},\\n${localeEntries}\\n};\\n\\n` +\n `export default content;\\n`\n );\n }\n return `module.exports = {\\n${marker},\\n${localeEntries}\\n};\\n`;\n};\n\n/**\n * Write the localized dictionaries to the dictionariesDir\n * @param mergedDictionaries - The merged dictionaries\n * @param configuration - The configuration\n * @returns The final dictionaries\n *\n * @example\n * ```ts\n * const unmergedDictionaries = await writeUnmergedDictionaries(dictionaries);\n * const finalDictionaries = await writeFinalDictionaries(unmergedDictionaries);\n * console.log(finalDictionaries);\n *\n * // .intlayer/dynamic_dictionary/dictionaries/en_home.json\n * // .intlayer/dynamic_dictionary/dictionaries/fr_home.json\n * ```\n */\nexport const writeDynamicDictionary = async (\n mergedDictionaries: PlainMergedDictionaryOutput,\n configuration: IntlayerConfig,\n formats: ('cjs' | 'esm')[] = OUTPUT_FORMAT\n): Promise<LocalizedDictionaryOutput> => {\n const { locales, defaultLocale } = configuration.internationalization;\n const { dynamicDictionariesDir } = configuration.system;\n\n const dictDir = resolve(dynamicDictionariesDir, DICTIONARIES_SUBDIR);\n await mkdir(dictDir, { recursive: true });\n\n const resultDictionariesPaths: LocalizedDictionaryOutput = {};\n\n // Merge dictionaries with the same key and write to dictionariesDir\n await parallelize(\n Object.entries(mergedDictionaries).sort(([a], [b]) =>\n String(a).localeCompare(String(b))\n ),\n async ([key, dictionaryEntry]) => {\n if (key === 'undefined') return;\n\n const localizedDictionariesPathsRecord: LocalizedDictionaryResult = {};\n\n const keyDir = resolve(dictDir, key);\n assertPathWithin(keyDir, dictDir);\n await mkdir(keyDir, { recursive: true });\n\n await parallelize(locales, async (locale) => {\n const localizedDictionary = getPerLocaleDictionary(\n dictionaryEntry.dictionary,\n locale,\n defaultLocale\n );\n\n // Directory structure: json/key/locale.json\n const resultFilePath = resolve(keyDir, `${locale}.json`);\n\n await writeJsonIfChanged(resultFilePath, localizedDictionary).catch(\n (err) => {\n console.error(\n `Error creating localized ${key}/${locale}.json:`,\n err\n );\n }\n );\n\n localizedDictionariesPathsRecord[locale] = {\n dictionaryPath: resultFilePath,\n dictionary: localizedDictionary,\n };\n });\n\n resultDictionariesPaths[key] = localizedDictionariesPathsRecord;\n\n await parallelize(formats, async (format) => {\n const extension = format === 'cjs' ? 'cjs' : 'mjs';\n const content = generateDictionaryEntryPoint(key, locales, format);\n\n const dynEntryPath = resolve(\n dynamicDictionariesDir,\n `${key}.${extension}`\n );\n assertPathWithin(dynEntryPath, dynamicDictionariesDir);\n\n await writeFileIfChanged(dynEntryPath, content).catch((err) => {\n console.error(\n `Error creating dynamic ${colorizePath(dynEntryPath)}:`,\n err\n );\n });\n });\n }\n );\n\n return resultDictionariesPaths;\n};\n\nexport type QualifiedMergedDictionaryResult = {\n dictionaryPath: string;\n dictionary: QualifiedDictionaryGroup;\n};\n\nexport type QualifiedMergedDictionaryOutput = Record<\n string,\n QualifiedMergedDictionaryResult\n>;\n\n/**\n * Writes the dynamic chunks and entry points of qualified dictionaries\n * (collections, variants — possibly combined) in\n * `importMode: 'dynamic'`.\n *\n * Each entry is reduced to one per-locale chunk written to a path nested by\n * dimension — `json/{key}/{seg1}/{seg2}/{locale}.json` — and a single\n * `{key}.{ext}` entry point exposes the matching nested loader tree, so the\n * entry point is discovered and aggregated exactly like a plain dynamic one.\n */\nexport const writeDynamicQualifiedDictionaries = async (\n qualifiedDictionaries: QualifiedMergedDictionaryOutput,\n configuration: IntlayerConfig,\n formats: ('cjs' | 'esm')[] = OUTPUT_FORMAT\n): Promise<void> => {\n const { locales, defaultLocale } = configuration.internationalization;\n const { dynamicDictionariesDir } = configuration.system;\n\n const dictDir = resolve(dynamicDictionariesDir, DICTIONARIES_SUBDIR);\n await mkdir(dictDir, { recursive: true });\n\n await parallelize(\n Object.entries(qualifiedDictionaries).sort(([a], [b]) =>\n String(a).localeCompare(String(b))\n ),\n async ([key, { dictionary: group }]) => {\n if (key === 'undefined') return;\n\n const entryIds = Object.keys(group.content);\n\n const keyDir = resolve(dictDir, key);\n assertPathWithin(keyDir, dictDir);\n\n // Entries with identical content (e.g. array-variant fan-out) share one\n // chunk: only the first entry per content identity writes files; the\n // others become aliases whose loaders point at the canonical chunk path.\n const canonicalSegmentsByContent = new Map<string, string[]>();\n const entries: QualifiedEntrySegments[] = [];\n const canonicalEntryIds: string[] = [];\n\n for (const entryId of entryIds) {\n const treeSegments = entryId.split(COMPOSITE_ID_SEPARATOR);\n const contentIdentity = JSON.stringify(group.content[entryId]);\n const canonicalSegments =\n canonicalSegmentsByContent.get(contentIdentity);\n\n if (canonicalSegments) {\n entries.push({ treeSegments, chunkSegments: canonicalSegments });\n continue;\n }\n\n canonicalSegmentsByContent.set(contentIdentity, treeSegments);\n entries.push({ treeSegments, chunkSegments: treeSegments });\n canonicalEntryIds.push(entryId);\n }\n\n await parallelize(canonicalEntryIds, async (entryId) => {\n // Rebuild a resolvable dictionary from the content node + composite id\n // so per-locale extraction sees the same `{ key, content }` shape.\n const entry = reconstructQualifiedEntry(group, entryId);\n\n const segments = entryId.split(COMPOSITE_ID_SEPARATOR);\n\n const entryDir = resolve(keyDir, ...segments);\n assertPathWithin(entryDir, keyDir);\n await mkdir(entryDir, { recursive: true });\n\n await parallelize(locales, async (locale) => {\n const localizedDictionary = getPerLocaleDictionary(\n entry,\n locale,\n defaultLocale\n );\n\n // Directory structure: json/key/<…segments>/locale.json\n const resultFilePath = resolve(entryDir, `${locale}.json`);\n\n await writeJsonIfChanged(resultFilePath, localizedDictionary).catch(\n (err) => {\n console.error(\n `Error creating localized ${key}/${segments.join('/')}/${locale}.json:`,\n err\n );\n }\n );\n });\n });\n\n await parallelize(formats, async (format) => {\n const extension = format === 'cjs' ? 'cjs' : 'mjs';\n const content = generateQualifiedDictionaryEntryPoint(\n key,\n group.qualifierTypes,\n entries,\n locales,\n format\n );\n\n const dynEntryPath = resolve(\n dynamicDictionariesDir,\n `${key}.${extension}`\n );\n assertPathWithin(dynEntryPath, dynamicDictionariesDir);\n\n await writeFileIfChanged(dynEntryPath, content).catch((err) => {\n console.error(\n `Error creating dynamic ${colorizePath(dynEntryPath)}:`,\n err\n );\n });\n });\n }\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"writeDynamicDictionary.mjs","names":[],"sources":["../../../src/buildIntlayerDictionary/writeDynamicDictionary.ts"],"sourcesContent":["import { mkdir } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { OUTPUT_FORMAT } from '@intlayer/config/defaultValues';\nimport { colorizePath } from '@intlayer/config/logger';\nimport { assertPathWithin } from '@intlayer/config/utils';\nimport {\n COMPOSITE_ID_SEPARATOR,\n QUALIFIER_DYNAMIC_TYPES_KEY,\n reconstructQualifiedEntry,\n} from '@intlayer/core/dictionaryManipulator';\nimport { getPerLocaleDictionary } from '@intlayer/core/plugins';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type {\n Dictionary,\n DictionaryQualifierType,\n QualifiedDictionaryGroup,\n} from '@intlayer/types/dictionary';\nimport { parallelize } from '../utils/parallelize';\nimport { writeFileIfChanged } from '../writeFileIfChanged';\nimport { writeJsonIfChanged } from '../writeJsonIfChanged';\nimport type { PlainMergedDictionaryOutput } from './writeMergedDictionary';\n\nexport type DictionaryResult = {\n dictionaryPath: string;\n dictionary: Dictionary;\n};\n\nexport type LocalizedDictionaryResult = Partial<\n Record<Locale, DictionaryResult>\n>;\n\nexport type LocalizedDictionaryOutput = Record<\n string,\n LocalizedDictionaryResult\n>;\n\nconst DICTIONARIES_SUBDIR = 'json'; // Necessary to add a static first dir for Turbopack\n\n/**\n * Escapes a value interpolated into a single-quoted literal of a generated\n * loader module, so keys or segments containing `'` or `\\` cannot break the\n * emitted JavaScript.\n */\nconst escapeJsLiteral = (value: string): string =>\n value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\");\n\n/** Loader expression for one `(key, locale)` per-locale chunk. */\nconst buildPerLocaleLoader = (\n key: string,\n locale: string,\n format: 'cjs' | 'esm'\n): string => {\n const path = `./${DICTIONARIES_SUBDIR}/${key}/${locale}.json`;\n\n return format === 'esm'\n ? `import('${path}').then(m => m.default)`\n : `Promise.resolve(require('${path}'))`;\n};\n\n/**\n * Generates the content of a dictionary entry point file.\n *\n * When the dictionary references other dictionaries through `nest()`, the\n * loader for a locale resolves the referenced dictionaries alongside it and\n * attaches them as `nestedDictionaries`. The nest targets then travel in the\n * same lazy chunk as their consumer, and `getNesting` resolves them from that\n * local reference instead of the global registry — which the build\n * optimization strips.\n *\n * @param key - The dictionary key.\n * @param locales - Locales to emit a loader for.\n * @param format - Output module format.\n * @param nestedKeys - Keys referenced through `nest()`, transitively.\n */\nexport const generateDictionaryEntryPoint = (\n key: string,\n locales: string[],\n format: 'cjs' | 'esm' = 'esm',\n nestedKeys: string[] = []\n): string => {\n const sortedLocales = [...locales].sort((a, b) =>\n String(a).localeCompare(String(b))\n );\n const sortedNestedKeys = [...nestedKeys].sort((a, b) => a.localeCompare(b));\n\n const safeKey = escapeJsLiteral(key);\n\n const localeEntries = sortedLocales\n .map((locale) => {\n const safeLocale = escapeJsLiteral(locale);\n const ownLoader = buildPerLocaleLoader(safeKey, safeLocale, format);\n\n if (sortedNestedKeys.length === 0) {\n return ` '${safeLocale}': () => ${ownLoader}`;\n }\n\n const nestedLoaders = sortedNestedKeys\n .map((nestedKey) =>\n buildPerLocaleLoader(escapeJsLiteral(nestedKey), safeLocale, format)\n )\n .join(',\\n ');\n\n const attachedEntries = sortedNestedKeys\n .map(\n (nestedKey, index) =>\n `'${escapeJsLiteral(nestedKey)}': _nested[${index}]`\n )\n .join(', ');\n\n return (\n ` '${safeLocale}': () => Promise.all([\\n` +\n ` ${ownLoader},\\n` +\n ` ${nestedLoaders}\\n` +\n ` ]).then(([_dictionary, ..._nested]) => ({\\n` +\n ` ..._dictionary,\\n` +\n ` nestedDictionaries: { ${attachedEntries} }\\n` +\n ` }))`\n );\n })\n .join(',\\n');\n\n if (format === 'esm') {\n return (\n `const content = {\\n${localeEntries}\\n};\\n\\n` +\n `export default content;\\n`\n );\n }\n return `module.exports = {\\n${localeEntries}\\n};\\n`;\n};\n\n/**\n * A nested loader tree: one level per declared dimension, leaves are loader\n * expression strings.\n */\ntype LoaderTree = { [segment: string]: LoaderTree | string };\n\n/**\n * One entry of a qualified loader map.\n *\n * `treeSegments` is where the entry lives in the loader tree (its composite id\n * segments); `chunkSegments` is the chunk path it loads. They differ when the\n * entry aliases another entry with identical content (e.g. array-variant\n * fan-out): the alias keeps its own tree position but points at the canonical\n * entry's chunk, so equal content is emitted — and downloaded — only once.\n */\nexport type QualifiedEntrySegments = {\n treeSegments: string[];\n chunkSegments: string[];\n};\n\nconst buildLoaderExpression = (\n key: string,\n chunkSegments: string[],\n locale: string,\n format: 'cjs' | 'esm'\n): string => {\n const path = escapeJsLiteral(\n `./${DICTIONARIES_SUBDIR}/${key}/${chunkSegments.join('/')}/${locale}.json`\n );\n\n return format === 'esm'\n ? `() => import('${path}').then(m => m.default)`\n : `() => Promise.resolve(require('${path}'))`;\n};\n\nconst buildLoaderTree = (\n key: string,\n entries: QualifiedEntrySegments[],\n locale: string,\n format: 'cjs' | 'esm'\n): LoaderTree => {\n const root: LoaderTree = {};\n\n for (const { treeSegments, chunkSegments } of entries) {\n let node = root;\n\n treeSegments.forEach((segment, index) => {\n if (index === treeSegments.length - 1) {\n node[segment] = buildLoaderExpression(\n key,\n chunkSegments,\n locale,\n format\n );\n return;\n }\n\n node[segment] = (node[segment] as LoaderTree | undefined) ?? {};\n node = node[segment] as LoaderTree;\n });\n }\n\n return root;\n};\n\nconst serializeLoaderTree = (tree: LoaderTree, indentLevel: number): string => {\n const pad = ' '.repeat(indentLevel);\n const innerPad = ' '.repeat(indentLevel + 1);\n\n const lines = Object.keys(tree)\n .sort((a, b) => a.localeCompare(b))\n .map((segment) => {\n const value = tree[segment]!;\n const serialized =\n typeof value === 'string'\n ? value\n : serializeLoaderTree(value, indentLevel + 1);\n\n return `${innerPad}'${escapeJsLiteral(segment)}': ${serialized}`;\n });\n\n return `{\\n${lines.join(',\\n')}\\n${pad}}`;\n};\n\n/**\n * Generates the entry point of a qualified dictionary (collection / variant,\n * possibly combined). Under each locale the loader map nests one\n * level per declared dimension (canonical order) and carries a marker listing\n * those dimensions so the runtime can walk the tree.\n *\n * One static `import()` is emitted per leaf `(locale, …segments)` chunk, which\n * keeps the output compatible with bundlers that reject template-literal\n * dynamic imports (Turbopack). Entries whose content is identical share one\n * chunk: their leaves point at the canonical entry's import path.\n */\nexport const generateQualifiedDictionaryEntryPoint = (\n key: string,\n qualifierTypes: DictionaryQualifierType[],\n entries: QualifiedEntrySegments[],\n locales: string[],\n format: 'cjs' | 'esm' = 'esm'\n): string => {\n const sortedLocales = [...locales].sort((a, b) =>\n String(a).localeCompare(String(b))\n );\n\n const localeEntries = sortedLocales\n .map((locale) => {\n const tree = buildLoaderTree(key, entries, locale, format);\n return ` '${escapeJsLiteral(locale)}': ${serializeLoaderTree(tree, 1)}`;\n })\n .join(',\\n');\n\n const marker = ` '${QUALIFIER_DYNAMIC_TYPES_KEY}': ${JSON.stringify(qualifierTypes)}`;\n\n if (format === 'esm') {\n return (\n `const content = {\\n${marker},\\n${localeEntries}\\n};\\n\\n` +\n `export default content;\\n`\n );\n }\n return `module.exports = {\\n${marker},\\n${localeEntries}\\n};\\n`;\n};\n\n/**\n * Write the localized dictionaries to the dictionariesDir\n * @param mergedDictionaries - The merged dictionaries\n * @param configuration - The configuration\n * @returns The final dictionaries\n *\n * @example\n * ```ts\n * const unmergedDictionaries = await writeUnmergedDictionaries(dictionaries);\n * const finalDictionaries = await writeFinalDictionaries(unmergedDictionaries);\n * console.log(finalDictionaries);\n *\n * // .intlayer/dynamic_dictionary/dictionaries/en_home.json\n * // .intlayer/dynamic_dictionary/dictionaries/fr_home.json\n * ```\n */\nexport const writeDynamicDictionary = async (\n mergedDictionaries: PlainMergedDictionaryOutput,\n configuration: IntlayerConfig,\n formats: ('cjs' | 'esm')[] = OUTPUT_FORMAT,\n nestedDictionaryGraph: Map<string, Set<string>> = new Map()\n): Promise<LocalizedDictionaryOutput> => {\n const { locales, defaultLocale } = configuration.internationalization;\n const { dynamicDictionariesDir } = configuration.system;\n\n const dictDir = resolve(dynamicDictionariesDir, DICTIONARIES_SUBDIR);\n await mkdir(dictDir, { recursive: true });\n\n const resultDictionariesPaths: LocalizedDictionaryOutput = {};\n\n // Merge dictionaries with the same key and write to dictionariesDir\n await parallelize(\n Object.entries(mergedDictionaries).sort(([a], [b]) =>\n String(a).localeCompare(String(b))\n ),\n async ([key, dictionaryEntry]) => {\n if (key === 'undefined') return;\n\n const localizedDictionariesPathsRecord: LocalizedDictionaryResult = {};\n\n const keyDir = resolve(dictDir, key);\n assertPathWithin(keyDir, dictDir);\n await mkdir(keyDir, { recursive: true });\n\n await parallelize(locales, async (locale) => {\n const localizedDictionary = getPerLocaleDictionary(\n dictionaryEntry.dictionary,\n locale,\n defaultLocale\n );\n\n // Directory structure: json/key/locale.json\n const resultFilePath = resolve(keyDir, `${locale}.json`);\n\n await writeJsonIfChanged(resultFilePath, localizedDictionary).catch(\n (err) => {\n console.error(\n `Error creating localized ${key}/${locale}.json:`,\n err\n );\n }\n );\n\n localizedDictionariesPathsRecord[locale] = {\n dictionaryPath: resultFilePath,\n dictionary: localizedDictionary,\n };\n });\n\n resultDictionariesPaths[key] = localizedDictionariesPathsRecord;\n\n await parallelize(formats, async (format) => {\n const extension = format === 'cjs' ? 'cjs' : 'mjs';\n const content = generateDictionaryEntryPoint(key, locales, format, [\n ...(nestedDictionaryGraph.get(key) ?? []),\n ]);\n\n const dynEntryPath = resolve(\n dynamicDictionariesDir,\n `${key}.${extension}`\n );\n assertPathWithin(dynEntryPath, dynamicDictionariesDir);\n\n await writeFileIfChanged(dynEntryPath, content).catch((err) => {\n console.error(\n `Error creating dynamic ${colorizePath(dynEntryPath)}:`,\n err\n );\n });\n });\n }\n );\n\n return resultDictionariesPaths;\n};\n\nexport type QualifiedMergedDictionaryResult = {\n dictionaryPath: string;\n dictionary: QualifiedDictionaryGroup;\n};\n\nexport type QualifiedMergedDictionaryOutput = Record<\n string,\n QualifiedMergedDictionaryResult\n>;\n\n/**\n * Writes the dynamic chunks and entry points of qualified dictionaries\n * (collections, variants — possibly combined) in\n * `importMode: 'dynamic'`.\n *\n * Each entry is reduced to one per-locale chunk written to a path nested by\n * dimension — `json/{key}/{seg1}/{seg2}/{locale}.json` — and a single\n * `{key}.{ext}` entry point exposes the matching nested loader tree, so the\n * entry point is discovered and aggregated exactly like a plain dynamic one.\n */\nexport const writeDynamicQualifiedDictionaries = async (\n qualifiedDictionaries: QualifiedMergedDictionaryOutput,\n configuration: IntlayerConfig,\n formats: ('cjs' | 'esm')[] = OUTPUT_FORMAT\n): Promise<void> => {\n const { locales, defaultLocale } = configuration.internationalization;\n const { dynamicDictionariesDir } = configuration.system;\n\n const dictDir = resolve(dynamicDictionariesDir, DICTIONARIES_SUBDIR);\n await mkdir(dictDir, { recursive: true });\n\n await parallelize(\n Object.entries(qualifiedDictionaries).sort(([a], [b]) =>\n String(a).localeCompare(String(b))\n ),\n async ([key, { dictionary: group }]) => {\n if (key === 'undefined') return;\n\n const entryIds = Object.keys(group.content);\n\n const keyDir = resolve(dictDir, key);\n assertPathWithin(keyDir, dictDir);\n\n // Entries with identical content (e.g. array-variant fan-out) share one\n // chunk: only the first entry per content identity writes files; the\n // others become aliases whose loaders point at the canonical chunk path.\n const canonicalSegmentsByContent = new Map<string, string[]>();\n const entries: QualifiedEntrySegments[] = [];\n const canonicalEntryIds: string[] = [];\n\n for (const entryId of entryIds) {\n const treeSegments = entryId.split(COMPOSITE_ID_SEPARATOR);\n const contentIdentity = JSON.stringify(group.content[entryId]);\n const canonicalSegments =\n canonicalSegmentsByContent.get(contentIdentity);\n\n if (canonicalSegments) {\n entries.push({ treeSegments, chunkSegments: canonicalSegments });\n continue;\n }\n\n canonicalSegmentsByContent.set(contentIdentity, treeSegments);\n entries.push({ treeSegments, chunkSegments: treeSegments });\n canonicalEntryIds.push(entryId);\n }\n\n await parallelize(canonicalEntryIds, async (entryId) => {\n // Rebuild a resolvable dictionary from the content node + composite id\n // so per-locale extraction sees the same `{ key, content }` shape.\n const entry = reconstructQualifiedEntry(group, entryId);\n\n const segments = entryId.split(COMPOSITE_ID_SEPARATOR);\n\n const entryDir = resolve(keyDir, ...segments);\n assertPathWithin(entryDir, keyDir);\n await mkdir(entryDir, { recursive: true });\n\n await parallelize(locales, async (locale) => {\n const localizedDictionary = getPerLocaleDictionary(\n entry,\n locale,\n defaultLocale\n );\n\n // Directory structure: json/key/<…segments>/locale.json\n const resultFilePath = resolve(entryDir, `${locale}.json`);\n\n await writeJsonIfChanged(resultFilePath, localizedDictionary).catch(\n (err) => {\n console.error(\n `Error creating localized ${key}/${segments.join('/')}/${locale}.json:`,\n err\n );\n }\n );\n });\n });\n\n await parallelize(formats, async (format) => {\n const extension = format === 'cjs' ? 'cjs' : 'mjs';\n const content = generateQualifiedDictionaryEntryPoint(\n key,\n group.qualifierTypes,\n entries,\n locales,\n format\n );\n\n const dynEntryPath = resolve(\n dynamicDictionariesDir,\n `${key}.${extension}`\n );\n assertPathWithin(dynEntryPath, dynamicDictionariesDir);\n\n await writeFileIfChanged(dynEntryPath, content).catch((err) => {\n console.error(\n `Error creating dynamic ${colorizePath(dynEntryPath)}:`,\n err\n );\n });\n });\n }\n );\n};\n"],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parallelize } from "../utils/parallelize.mjs";
|
|
2
2
|
import { writeFileIfChanged } from "../writeFileIfChanged.mjs";
|
|
3
|
+
import { getPathHash } from "../utils/getPathHash.mjs";
|
|
3
4
|
import { mkdir } from "node:fs/promises";
|
|
4
5
|
import { relative, resolve } from "node:path";
|
|
5
6
|
import { colorizePath } from "@intlayer/config/logger";
|
|
@@ -7,11 +8,47 @@ import { assertPathWithin, normalizePath } from "@intlayer/config/utils";
|
|
|
7
8
|
import { OUTPUT_FORMAT } from "@intlayer/config/defaultValues";
|
|
8
9
|
|
|
9
10
|
//#region src/buildIntlayerDictionary/writeFetchDictionary.ts
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Local identifier of the dynamic loader backing a nest target's fallback.
|
|
13
|
+
* Hashed so any dictionary key yields a valid identifier.
|
|
14
|
+
*/
|
|
15
|
+
const dynamicLoaderIdentifier = (key) => `_dyn${getPathHash(key)}`;
|
|
16
|
+
/**
|
|
17
|
+
* Fetches one dictionary for a locale from the live-sync server, falling back
|
|
18
|
+
* to its own build-time dynamic chunk when the request fails.
|
|
19
|
+
*
|
|
20
|
+
* `fallbackLoader` must be the loader of `key` itself — a nest target falling
|
|
21
|
+
* back to its consumer's chunk would attach the wrong content.
|
|
22
|
+
*/
|
|
23
|
+
const buildLiveFetchExpression = (key, locale, fallbackLoader) => `(async () => {
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch(\`\${editor.liveSyncURL}/dictionaries/${key}/${locale}\`);\n return await res.json();\n } catch {\n return ${fallbackLoader}['${locale}']();\n }\n })()`;
|
|
26
|
+
/**
|
|
27
|
+
* Generates the live (fetch-mode) entry point of a dictionary.
|
|
28
|
+
*
|
|
29
|
+
* When the dictionary references others through `nest()`, those are fetched
|
|
30
|
+
* live alongside it and attached as `nestedDictionaries`, so a remote edit to a
|
|
31
|
+
* nested dictionary propagates through the reference instead of resolving
|
|
32
|
+
* against the build-time snapshot.
|
|
33
|
+
*
|
|
34
|
+
* @param key - The dictionary key.
|
|
35
|
+
* @param locales - Locales to emit a loader for.
|
|
36
|
+
* @param relativePrefix - Relative path to the dynamic dictionaries directory.
|
|
37
|
+
* @param format - Output module format.
|
|
38
|
+
* @param nestedKeys - Keys referenced through `nest()`, transitively.
|
|
39
|
+
*/
|
|
40
|
+
const generateDictionaryEntryPoint = (key, locales, relativePrefix, format = "esm", nestedKeys = []) => {
|
|
11
41
|
const extension = format === "cjs" ? "cjs" : "mjs";
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
42
|
+
const sortedNestedKeys = [...nestedKeys].sort((a, b) => a.localeCompare(b));
|
|
43
|
+
const localeEntries = locales.sort((a, b) => String(a).localeCompare(String(b))).map((locale) => {
|
|
44
|
+
if (sortedNestedKeys.length === 0) return ` '${locale}': async () => {\n try {\n const res = await fetch(\`\${editor.liveSyncURL}/dictionaries/${key}/${locale}\`);\n return await res.json();\n } catch {\n return dynContent['${locale}']();\n }\n }`;
|
|
45
|
+
const nestedFetches = sortedNestedKeys.map((nestedKey) => buildLiveFetchExpression(nestedKey, locale, dynamicLoaderIdentifier(nestedKey))).join(",\n ");
|
|
46
|
+
const attachedEntries = sortedNestedKeys.map((nestedKey, index) => `'${nestedKey}': _nested[${index}]`).join(", ");
|
|
47
|
+
return ` '${locale}': async () => {\n const [_dictionary, ..._nested] = await Promise.all([\n ${buildLiveFetchExpression(key, locale, "dynContent")},\n ${nestedFetches}\n ]);\n return { ..._dictionary, nestedDictionaries: { ${attachedEntries} } };\n }`;
|
|
48
|
+
}).join(",\n");
|
|
49
|
+
const nestedImports = sortedNestedKeys.map((nestedKey) => format === "esm" ? `import ${dynamicLoaderIdentifier(nestedKey)} from '${relativePrefix}/${nestedKey}.${extension}';` : `const ${dynamicLoaderIdentifier(nestedKey)} = require('${relativePrefix}/${nestedKey}.${extension}');`).join("\n");
|
|
50
|
+
if (format === "esm") return `import { editor } from 'intlayer';\nimport dynContent from '${relativePrefix}/${key}.${extension}';\n${nestedImports}\n\nconst content = {\n${localeEntries}\n};\n\nexport default content;\n`;
|
|
51
|
+
return `const { editor } = require('intlayer');\nconst dynContent = require('${relativePrefix}/${key}.${extension}');\n${nestedImports}\n\nmodule.exports = {\n${localeEntries}\n};\n`;
|
|
15
52
|
};
|
|
16
53
|
/**
|
|
17
54
|
* Write the localized dictionaries to the dictionariesDir
|
|
@@ -29,7 +66,7 @@ const generateDictionaryEntryPoint = (key, locales, relativePrefix, format = "es
|
|
|
29
66
|
* // .intlayer/fetch_dictionary/home.cjs
|
|
30
67
|
* ```
|
|
31
68
|
*/
|
|
32
|
-
const writeFetchDictionary = async (dynamicDictionaries, configuration, formats = OUTPUT_FORMAT) => {
|
|
69
|
+
const writeFetchDictionary = async (dynamicDictionaries, configuration, formats = OUTPUT_FORMAT, nestedDictionaryGraph = /* @__PURE__ */ new Map()) => {
|
|
33
70
|
const { fetchDictionariesDir, dynamicDictionariesDir } = configuration.system;
|
|
34
71
|
const { locales } = configuration.internationalization;
|
|
35
72
|
let relativePrefix = normalizePath(relative(fetchDictionariesDir, dynamicDictionariesDir));
|
|
@@ -40,7 +77,7 @@ const writeFetchDictionary = async (dynamicDictionaries, configuration, formats
|
|
|
40
77
|
if (key === "undefined") return;
|
|
41
78
|
await parallelize(formats, async (format) => {
|
|
42
79
|
const extension = format === "cjs" ? "cjs" : "mjs";
|
|
43
|
-
const content = generateDictionaryEntryPoint(key, locales, relativePrefix, format);
|
|
80
|
+
const content = generateDictionaryEntryPoint(key, locales, relativePrefix, format, [...nestedDictionaryGraph.get(key) ?? []]);
|
|
44
81
|
const fetchEntryPath = resolve(fetchDictionariesDir, `${key}.${extension}`);
|
|
45
82
|
assertPathWithin(fetchEntryPath, fetchDictionariesDir);
|
|
46
83
|
await writeFileIfChanged(fetchEntryPath, content).catch((err) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writeFetchDictionary.mjs","names":[],"sources":["../../../src/buildIntlayerDictionary/writeFetchDictionary.ts"],"sourcesContent":["import { mkdir } from 'node:fs/promises';\nimport { relative, resolve } from 'node:path';\nimport { OUTPUT_FORMAT } from '@intlayer/config/defaultValues';\nimport { colorizePath } from '@intlayer/config/logger';\nimport { assertPathWithin, normalizePath } from '@intlayer/config/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { parallelize } from '../utils/parallelize';\nimport { writeFileIfChanged } from '../writeFileIfChanged';\nimport type { LocalizedDictionaryOutput } from './writeDynamicDictionary';\n\nexport const generateDictionaryEntryPoint = (\n key: string,\n locales: string[],\n relativePrefix: string,\n format: 'cjs' | 'esm' = 'esm'\n): string => {\n const extension = format === 'cjs' ? 'cjs' : 'mjs';\n\n const localeEntries = locales\n .sort((a, b) => String(a).localeCompare(String(b)))\n .map(\n (
|
|
1
|
+
{"version":3,"file":"writeFetchDictionary.mjs","names":[],"sources":["../../../src/buildIntlayerDictionary/writeFetchDictionary.ts"],"sourcesContent":["import { mkdir } from 'node:fs/promises';\nimport { relative, resolve } from 'node:path';\nimport { OUTPUT_FORMAT } from '@intlayer/config/defaultValues';\nimport { colorizePath } from '@intlayer/config/logger';\nimport { assertPathWithin, normalizePath } from '@intlayer/config/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { getPathHash } from '../utils/getPathHash';\nimport { parallelize } from '../utils/parallelize';\nimport { writeFileIfChanged } from '../writeFileIfChanged';\nimport type { LocalizedDictionaryOutput } from './writeDynamicDictionary';\n\n/**\n * Local identifier of the dynamic loader backing a nest target's fallback.\n * Hashed so any dictionary key yields a valid identifier.\n */\nconst dynamicLoaderIdentifier = (key: string): string =>\n `_dyn${getPathHash(key)}`;\n\n/**\n * Fetches one dictionary for a locale from the live-sync server, falling back\n * to its own build-time dynamic chunk when the request fails.\n *\n * `fallbackLoader` must be the loader of `key` itself — a nest target falling\n * back to its consumer's chunk would attach the wrong content.\n */\nconst buildLiveFetchExpression = (\n key: string,\n locale: string,\n fallbackLoader: string\n): string =>\n `(async () => {\\n` +\n ` try {\\n` +\n ` const res = await fetch(\\`\\${editor.liveSyncURL}/dictionaries/${key}/${locale}\\`);\\n` +\n ` return await res.json();\\n` +\n ` } catch {\\n` +\n ` return ${fallbackLoader}['${locale}']();\\n` +\n ` }\\n` +\n ` })()`;\n\n/**\n * Generates the live (fetch-mode) entry point of a dictionary.\n *\n * When the dictionary references others through `nest()`, those are fetched\n * live alongside it and attached as `nestedDictionaries`, so a remote edit to a\n * nested dictionary propagates through the reference instead of resolving\n * against the build-time snapshot.\n *\n * @param key - The dictionary key.\n * @param locales - Locales to emit a loader for.\n * @param relativePrefix - Relative path to the dynamic dictionaries directory.\n * @param format - Output module format.\n * @param nestedKeys - Keys referenced through `nest()`, transitively.\n */\nexport const generateDictionaryEntryPoint = (\n key: string,\n locales: string[],\n relativePrefix: string,\n format: 'cjs' | 'esm' = 'esm',\n nestedKeys: string[] = []\n): string => {\n const extension = format === 'cjs' ? 'cjs' : 'mjs';\n const sortedNestedKeys = [...nestedKeys].sort((a, b) => a.localeCompare(b));\n\n const localeEntries = locales\n .sort((a, b) => String(a).localeCompare(String(b)))\n .map((locale) => {\n if (sortedNestedKeys.length === 0) {\n return (\n ` '${locale}': async () => {\\n` +\n ` try {\\n` +\n ` const res = await fetch(\\`\\${editor.liveSyncURL}/dictionaries/${key}/${locale}\\`);\\n` +\n ` return await res.json();\\n` +\n ` } catch {\\n` +\n ` return dynContent['${locale}']();\\n` +\n ` }\\n` +\n ` }`\n );\n }\n\n const nestedFetches = sortedNestedKeys\n .map((nestedKey) =>\n buildLiveFetchExpression(\n nestedKey,\n locale,\n dynamicLoaderIdentifier(nestedKey)\n )\n )\n .join(',\\n ');\n\n const attachedEntries = sortedNestedKeys\n .map((nestedKey, index) => `'${nestedKey}': _nested[${index}]`)\n .join(', ');\n\n return (\n ` '${locale}': async () => {\\n` +\n ` const [_dictionary, ..._nested] = await Promise.all([\\n` +\n ` ${buildLiveFetchExpression(key, locale, 'dynContent')},\\n` +\n ` ${nestedFetches}\\n` +\n ` ]);\\n` +\n ` return { ..._dictionary, nestedDictionaries: { ${attachedEntries} } };\\n` +\n ` }`\n );\n })\n .join(',\\n');\n\n // Each nest target falls back to its own dynamic chunk when the live request\n // fails, so a failure never attaches the consumer's content under its key.\n const nestedImports = sortedNestedKeys\n .map((nestedKey) =>\n format === 'esm'\n ? `import ${dynamicLoaderIdentifier(nestedKey)} from '${relativePrefix}/${nestedKey}.${extension}';`\n : `const ${dynamicLoaderIdentifier(nestedKey)} = require('${relativePrefix}/${nestedKey}.${extension}');`\n )\n .join('\\n');\n\n if (format === 'esm') {\n return (\n `import { editor } from 'intlayer';\\n` +\n `import dynContent from '${relativePrefix}/${key}.${extension}';\\n` +\n `${nestedImports}\\n\\n` +\n `const content = {\\n${localeEntries}\\n};\\n\\n` +\n `export default content;\\n`\n );\n }\n return (\n `const { editor } = require('intlayer');\\n` +\n `const dynContent = require('${relativePrefix}/${key}.${extension}');\\n` +\n `${nestedImports}\\n\\n` +\n `module.exports = {\\n${localeEntries}\\n};\\n`\n );\n};\n\n/**\n * Write the localized dictionaries to the dictionariesDir\n * @param mergedDictionaries - The merged dictionaries\n * @param configuration - The configuration\n * @returns The final dictionaries\n *\n * @example\n * ```ts\n * const unmergedDictionaries = await writeUnmergedDictionaries(dictionaries);\n * const finalDictionaries = await writeFinalDictionaries(unmergedDictionaries);\n * console.log(finalDictionaries);\n *\n * // .intlayer/fetch_dictionary/home.mjs\n * // .intlayer/fetch_dictionary/home.cjs\n * ```\n */\nexport const writeFetchDictionary = async (\n dynamicDictionaries: LocalizedDictionaryOutput,\n configuration: IntlayerConfig,\n formats: ('cjs' | 'esm')[] = OUTPUT_FORMAT,\n nestedDictionaryGraph: Map<string, Set<string>> = new Map()\n): Promise<LocalizedDictionaryOutput> => {\n const { fetchDictionariesDir, dynamicDictionariesDir } = configuration.system;\n const { locales } = configuration.internationalization;\n\n // Compute relative path from fetch dir to dynamic dir\n let relativePrefix = normalizePath(\n relative(fetchDictionariesDir, dynamicDictionariesDir)\n );\n if (!relativePrefix.startsWith('.')) {\n relativePrefix = `./${relativePrefix}`;\n }\n\n await mkdir(resolve(fetchDictionariesDir), { recursive: true });\n\n const resultDictionariesPaths: LocalizedDictionaryOutput = {};\n\n // Write entry points for each dictionary in parallel\n await parallelize(Object.entries(dynamicDictionaries), async ([key]) => {\n if (key === 'undefined') return;\n\n await parallelize(formats, async (format) => {\n const extension = format === 'cjs' ? 'cjs' : 'mjs';\n const content = generateDictionaryEntryPoint(\n key,\n locales,\n relativePrefix,\n format,\n [...(nestedDictionaryGraph.get(key) ?? [])]\n );\n\n const fetchEntryPath = resolve(\n fetchDictionariesDir,\n `${key}.${extension}`\n );\n assertPathWithin(fetchEntryPath, fetchDictionariesDir);\n\n await writeFileIfChanged(fetchEntryPath, content).catch((err) => {\n console.error(\n `Error creating fetch ${colorizePath(fetchEntryPath)}:`,\n err\n );\n });\n });\n });\n\n return resultDictionariesPaths;\n};\n"],"mappings":";;;;;;;;;;;;;;AAeA,MAAM,2BAA2B,QAC/B,OAAO,YAAY,GAAG;;;;;;;;AASxB,MAAM,4BACJ,KACA,QACA,mBAEA;;wEAEyE,IAAI,GAAG,OAAO,0EAGrE,eAAe,IAAI,OAAO;;;;;;;;;;;;;;;AAkB9C,MAAa,gCACX,KACA,SACA,gBACA,SAAwB,OACxB,aAAuB,CAAC,MACb;CACX,MAAM,YAAY,WAAW,QAAQ,QAAQ;CAC7C,MAAM,mBAAmB,CAAC,GAAG,UAAU,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC;CAE1E,MAAM,gBAAgB,QACnB,MAAM,GAAG,MAAM,OAAO,CAAC,CAAC,CAAC,cAAc,OAAO,CAAC,CAAC,CAAC,CAAC,CAClD,KAAK,WAAW;EACf,IAAI,iBAAiB,WAAW,GAC9B,OACE,MAAM,OAAO,mGAE0D,IAAI,GAAG,OAAO,gFAGzD,OAAO;EAMvC,MAAM,gBAAgB,iBACnB,KAAK,cACJ,yBACE,WACA,QACA,wBAAwB,SAAS,CACnC,CACF,CAAC,CACA,KAAK,SAAS;EAEjB,MAAM,kBAAkB,iBACrB,KAAK,WAAW,UAAU,IAAI,UAAU,aAAa,MAAM,EAAE,CAAC,CAC9D,KAAK,IAAI;EAEZ,OACE,MAAM,OAAO,mFAEN,yBAAyB,KAAK,QAAQ,YAAY,EAAE,SACpD,cAAc,gEAEiC,gBAAgB;CAG1E,CAAC,CAAC,CACD,KAAK,KAAK;CAIb,MAAM,gBAAgB,iBACnB,KAAK,cACJ,WAAW,QACP,UAAU,wBAAwB,SAAS,EAAE,SAAS,eAAe,GAAG,UAAU,GAAG,UAAU,MAC/F,SAAS,wBAAwB,SAAS,EAAE,cAAc,eAAe,GAAG,UAAU,GAAG,UAAU,IACzG,CAAC,CACA,KAAK,IAAI;CAEZ,IAAI,WAAW,OACb,OACE,+DAC2B,eAAe,GAAG,IAAI,GAAG,UAAU,MAC3D,cAAc,yBACK,cAAc;CAIxC,OACE,wEAC+B,eAAe,GAAG,IAAI,GAAG,UAAU,OAC/D,cAAc,0BACM,cAAc;AAEzC;;;;;;;;;;;;;;;;;AAkBA,MAAa,uBAAuB,OAClC,qBACA,eACA,UAA6B,eAC7B,wCAAkD,IAAI,IAAI,MACnB;CACvC,MAAM,EAAE,sBAAsB,2BAA2B,cAAc;CACvE,MAAM,EAAE,YAAY,cAAc;CAGlC,IAAI,iBAAiB,cACnB,SAAS,sBAAsB,sBAAsB,CACvD;CACA,IAAI,CAAC,eAAe,WAAW,GAAG,GAChC,iBAAiB,KAAK;CAGxB,MAAM,MAAM,QAAQ,oBAAoB,GAAG,EAAE,WAAW,KAAK,CAAC;CAE9D,MAAM,0BAAqD,CAAC;CAG5D,MAAM,YAAY,OAAO,QAAQ,mBAAmB,GAAG,OAAO,CAAC,SAAS;EACtE,IAAI,QAAQ,aAAa;EAEzB,MAAM,YAAY,SAAS,OAAO,WAAW;GAC3C,MAAM,YAAY,WAAW,QAAQ,QAAQ;GAC7C,MAAM,UAAU,6BACd,KACA,SACA,gBACA,QACA,CAAC,GAAI,sBAAsB,IAAI,GAAG,KAAK,CAAC,CAAE,CAC5C;GAEA,MAAM,iBAAiB,QACrB,sBACA,GAAG,IAAI,GAAG,WACZ;GACA,iBAAiB,gBAAgB,oBAAoB;GAErD,MAAM,mBAAmB,gBAAgB,OAAO,CAAC,CAAC,OAAO,QAAQ;IAC/D,QAAQ,MACN,wBAAwB,aAAa,cAAc,EAAE,IACrD,GACF;GACF,CAAC;EACH,CAAC;CACH,CAAC;CAED,OAAO;AACT"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { parallelize } from "../utils/parallelize.mjs";
|
|
2
|
+
import { writeFileIfChanged } from "../writeFileIfChanged.mjs";
|
|
3
|
+
import { getPathHash } from "../utils/getPathHash.mjs";
|
|
4
|
+
import { mkdir } from "node:fs/promises";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import { colorizePath } from "@intlayer/config/logger";
|
|
7
|
+
import { assertPathWithin } from "@intlayer/config/utils";
|
|
8
|
+
import { OUTPUT_FORMAT } from "@intlayer/config/defaultValues";
|
|
9
|
+
|
|
10
|
+
//#region src/buildIntlayerDictionary/writeNestedDictionary.ts
|
|
11
|
+
/**
|
|
12
|
+
* Subdirectory of `dictionariesDir` holding the companion modules. Nested under
|
|
13
|
+
* the compiled dictionaries so both stay together and are cleaned as one, and
|
|
14
|
+
* because the `**\/*.json` globs listing compiled dictionaries never match the
|
|
15
|
+
* `.mjs` / `.cjs` files written here.
|
|
16
|
+
*/
|
|
17
|
+
const NESTED_DICTIONARIES_SUBDIR = "nested";
|
|
18
|
+
/**
|
|
19
|
+
* Escapes a value interpolated into a single-quoted literal of a generated
|
|
20
|
+
* module, so keys containing `'` or `\` cannot break the emitted JavaScript.
|
|
21
|
+
*/
|
|
22
|
+
const escapeJsLiteral = (value) => value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
23
|
+
/**
|
|
24
|
+
* Path of the companion module carrying the `nest()` targets of a dictionary.
|
|
25
|
+
*
|
|
26
|
+
* Shared with the bundler plugins, which re-point the static import they inject
|
|
27
|
+
* at a `useIntlayer` call site to this module whenever the dictionary has nest
|
|
28
|
+
* targets.
|
|
29
|
+
*
|
|
30
|
+
* @param dictionariesDir - The compiled dictionaries directory.
|
|
31
|
+
* @param key - The dictionary key.
|
|
32
|
+
* @param extension - Module extension, `'mjs'` (default) or `'cjs'`.
|
|
33
|
+
*/
|
|
34
|
+
const getNestedDictionaryPath = (dictionariesDir, key, extension = "mjs") => resolve(dictionariesDir, NESTED_DICTIONARIES_SUBDIR, `${key}.${extension}`);
|
|
35
|
+
/**
|
|
36
|
+
* Generates a companion module that re-exports a compiled dictionary with its
|
|
37
|
+
* `nest()` targets attached.
|
|
38
|
+
*
|
|
39
|
+
* The optimizer imports this instead of the raw JSON, so `getNesting` resolves
|
|
40
|
+
* the reference from `nestedDictionaries` rather from the global registry
|
|
41
|
+
* — which lets the registry be stripped, and puts each nest target in the same
|
|
42
|
+
* chunk as the dictionary that references it.
|
|
43
|
+
*
|
|
44
|
+
* @param key - The dictionary key.
|
|
45
|
+
* @param nestedKeys - Keys of the dictionaries it references, transitively.
|
|
46
|
+
* @param format - Output module format.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```js
|
|
50
|
+
* import _dictionary from '../dashboard.json' with { type: 'json' };
|
|
51
|
+
* import _1a2b3c from '../common.json' with { type: 'json' };
|
|
52
|
+
*
|
|
53
|
+
* export default { ..._dictionary, nestedDictionaries: { 'common': _1a2b3c } };
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
const generateNestedDictionaryEntryPoint = (key, nestedKeys, format = "esm") => {
|
|
57
|
+
const sortedKeys = [...nestedKeys].sort((a, b) => a.localeCompare(b));
|
|
58
|
+
const identifierOf = (nestedKey) => `_${getPathHash(nestedKey)}`;
|
|
59
|
+
const importLines = sortedKeys.map((nestedKey) => format === "esm" ? `import ${identifierOf(nestedKey)} from '../${escapeJsLiteral(nestedKey)}.json' with { type: 'json' };` : `const ${identifierOf(nestedKey)} = require('../${escapeJsLiteral(nestedKey)}.json');`).join("\n");
|
|
60
|
+
const attachedEntries = sortedKeys.map((nestedKey) => ` '${escapeJsLiteral(nestedKey)}': ${identifierOf(nestedKey)}`).join(",\n");
|
|
61
|
+
const safeKey = escapeJsLiteral(key);
|
|
62
|
+
if (format === "esm") return `import _dictionary from '../${safeKey}.json' with { type: 'json' };\n${importLines}\n\nconst nestedDictionaries = {\n${attachedEntries}\n};\n\nexport default { ..._dictionary, nestedDictionaries };\n`;
|
|
63
|
+
return `const _dictionary = require('../${safeKey}.json');\n${importLines}\n\nconst nestedDictionaries = {\n${attachedEntries}\n};\n\nmodule.exports = { ..._dictionary, nestedDictionaries };\n`;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Writes one companion module per dictionary holding `nest()` references.
|
|
67
|
+
*
|
|
68
|
+
* @param nestedDictionaryGraph - Dictionary key → keys it nests, transitively.
|
|
69
|
+
* @param configuration - The resolved intlayer configuration.
|
|
70
|
+
* @param formats - Module formats to emit.
|
|
71
|
+
*/
|
|
72
|
+
const writeNestedDictionaries = async (nestedDictionaryGraph, configuration, formats = OUTPUT_FORMAT) => {
|
|
73
|
+
if (nestedDictionaryGraph.size === 0) return;
|
|
74
|
+
const { dictionariesDir } = configuration.system;
|
|
75
|
+
const nestedDir = resolve(dictionariesDir, NESTED_DICTIONARIES_SUBDIR);
|
|
76
|
+
await mkdir(nestedDir, { recursive: true });
|
|
77
|
+
await parallelize([...nestedDictionaryGraph.entries()].sort(([a], [b]) => a.localeCompare(b)), async ([key, nestedKeys]) => {
|
|
78
|
+
await parallelize(formats, async (format) => {
|
|
79
|
+
const extension = format === "cjs" ? "cjs" : "mjs";
|
|
80
|
+
const content = generateNestedDictionaryEntryPoint(key, [...nestedKeys], format);
|
|
81
|
+
const entryPath = getNestedDictionaryPath(dictionariesDir, key, extension);
|
|
82
|
+
assertPathWithin(entryPath, nestedDir);
|
|
83
|
+
await writeFileIfChanged(entryPath, content).catch((error) => {
|
|
84
|
+
console.error(`Error creating nested ${colorizePath(entryPath)}:`, error);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
export { NESTED_DICTIONARIES_SUBDIR, generateNestedDictionaryEntryPoint, getNestedDictionaryPath, writeNestedDictionaries };
|
|
92
|
+
//# sourceMappingURL=writeNestedDictionary.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writeNestedDictionary.mjs","names":[],"sources":["../../../src/buildIntlayerDictionary/writeNestedDictionary.ts"],"sourcesContent":["import { mkdir } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { OUTPUT_FORMAT } from '@intlayer/config/defaultValues';\nimport { colorizePath } from '@intlayer/config/logger';\nimport { assertPathWithin } from '@intlayer/config/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { getPathHash } from '../utils/getPathHash';\nimport { parallelize } from '../utils/parallelize';\nimport { writeFileIfChanged } from '../writeFileIfChanged';\n\n/**\n * Subdirectory of `dictionariesDir` holding the companion modules. Nested under\n * the compiled dictionaries so both stay together and are cleaned as one, and\n * because the `**\\/*.json` globs listing compiled dictionaries never match the\n * `.mjs` / `.cjs` files written here.\n */\nexport const NESTED_DICTIONARIES_SUBDIR = 'nested';\n\n/**\n * Escapes a value interpolated into a single-quoted literal of a generated\n * module, so keys containing `'` or `\\` cannot break the emitted JavaScript.\n */\nconst escapeJsLiteral = (value: string): string =>\n value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\");\n\n/**\n * Path of the companion module carrying the `nest()` targets of a dictionary.\n *\n * Shared with the bundler plugins, which re-point the static import they inject\n * at a `useIntlayer` call site to this module whenever the dictionary has nest\n * targets.\n *\n * @param dictionariesDir - The compiled dictionaries directory.\n * @param key - The dictionary key.\n * @param extension - Module extension, `'mjs'` (default) or `'cjs'`.\n */\nexport const getNestedDictionaryPath = (\n dictionariesDir: string,\n key: string,\n extension: 'mjs' | 'cjs' = 'mjs'\n): string =>\n resolve(dictionariesDir, NESTED_DICTIONARIES_SUBDIR, `${key}.${extension}`);\n\n/**\n * Generates a companion module that re-exports a compiled dictionary with its\n * `nest()` targets attached.\n *\n * The optimizer imports this instead of the raw JSON, so `getNesting` resolves\n * the reference from `nestedDictionaries` rather from the global registry\n * — which lets the registry be stripped, and puts each nest target in the same\n * chunk as the dictionary that references it.\n *\n * @param key - The dictionary key.\n * @param nestedKeys - Keys of the dictionaries it references, transitively.\n * @param format - Output module format.\n *\n * @example\n * ```js\n * import _dictionary from '../dashboard.json' with { type: 'json' };\n * import _1a2b3c from '../common.json' with { type: 'json' };\n *\n * export default { ..._dictionary, nestedDictionaries: { 'common': _1a2b3c } };\n * ```\n */\nexport const generateNestedDictionaryEntryPoint = (\n key: string,\n nestedKeys: string[],\n format: 'cjs' | 'esm' = 'esm'\n): string => {\n const sortedKeys = [...nestedKeys].sort((a, b) => a.localeCompare(b));\n\n // Hash the key so the generated identifier is a valid one whatever the\n // dictionary key contains (dashes, dots, non-ASCII).\n const identifierOf = (nestedKey: string) => `_${getPathHash(nestedKey)}`;\n\n const importLines = sortedKeys\n .map((nestedKey) =>\n format === 'esm'\n ? `import ${identifierOf(nestedKey)} from '../${escapeJsLiteral(nestedKey)}.json' with { type: 'json' };`\n : `const ${identifierOf(nestedKey)} = require('../${escapeJsLiteral(nestedKey)}.json');`\n )\n .join('\\n');\n\n const attachedEntries = sortedKeys\n .map(\n (nestedKey) =>\n ` '${escapeJsLiteral(nestedKey)}': ${identifierOf(nestedKey)}`\n )\n .join(',\\n');\n\n const safeKey = escapeJsLiteral(key);\n\n if (format === 'esm') {\n return (\n `import _dictionary from '../${safeKey}.json' with { type: 'json' };\\n` +\n `${importLines}\\n\\n` +\n `const nestedDictionaries = {\\n${attachedEntries}\\n};\\n\\n` +\n `export default { ..._dictionary, nestedDictionaries };\\n`\n );\n }\n\n return (\n `const _dictionary = require('../${safeKey}.json');\\n` +\n `${importLines}\\n\\n` +\n `const nestedDictionaries = {\\n${attachedEntries}\\n};\\n\\n` +\n `module.exports = { ..._dictionary, nestedDictionaries };\\n`\n );\n};\n\n/**\n * Writes one companion module per dictionary holding `nest()` references.\n *\n * @param nestedDictionaryGraph - Dictionary key → keys it nests, transitively.\n * @param configuration - The resolved intlayer configuration.\n * @param formats - Module formats to emit.\n */\nexport const writeNestedDictionaries = async (\n nestedDictionaryGraph: Map<string, Set<string>>,\n configuration: IntlayerConfig,\n formats: ('cjs' | 'esm')[] = OUTPUT_FORMAT\n): Promise<void> => {\n if (nestedDictionaryGraph.size === 0) return;\n\n const { dictionariesDir } = configuration.system;\n const nestedDir = resolve(dictionariesDir, NESTED_DICTIONARIES_SUBDIR);\n\n await mkdir(nestedDir, { recursive: true });\n\n await parallelize(\n [...nestedDictionaryGraph.entries()].sort(([a], [b]) => a.localeCompare(b)),\n async ([key, nestedKeys]) => {\n await parallelize(formats, async (format) => {\n const extension = format === 'cjs' ? 'cjs' : 'mjs';\n const content = generateNestedDictionaryEntryPoint(\n key,\n [...nestedKeys],\n format\n );\n\n const entryPath = getNestedDictionaryPath(\n dictionariesDir,\n key,\n extension\n );\n assertPathWithin(entryPath, nestedDir);\n\n await writeFileIfChanged(entryPath, content).catch((error) => {\n console.error(\n `Error creating nested ${colorizePath(entryPath)}:`,\n error\n );\n });\n });\n }\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,MAAa,6BAA6B;;;;;AAM1C,MAAM,mBAAmB,UACvB,MAAM,QAAQ,OAAO,MAAM,CAAC,CAAC,QAAQ,MAAM,KAAK;;;;;;;;;;;;AAalD,MAAa,2BACX,iBACA,KACA,YAA2B,UAE3B,QAAQ,iBAAiB,4BAA4B,GAAG,IAAI,GAAG,WAAW;;;;;;;;;;;;;;;;;;;;;;AAuB5E,MAAa,sCACX,KACA,YACA,SAAwB,UACb;CACX,MAAM,aAAa,CAAC,GAAG,UAAU,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC;CAIpE,MAAM,gBAAgB,cAAsB,IAAI,YAAY,SAAS;CAErE,MAAM,cAAc,WACjB,KAAK,cACJ,WAAW,QACP,UAAU,aAAa,SAAS,EAAE,YAAY,gBAAgB,SAAS,EAAE,iCACzE,SAAS,aAAa,SAAS,EAAE,iBAAiB,gBAAgB,SAAS,EAAE,SACnF,CAAC,CACA,KAAK,IAAI;CAEZ,MAAM,kBAAkB,WACrB,KACE,cACC,MAAM,gBAAgB,SAAS,EAAE,KAAK,aAAa,SAAS,GAChE,CAAC,CACA,KAAK,KAAK;CAEb,MAAM,UAAU,gBAAgB,GAAG;CAEnC,IAAI,WAAW,OACb,OACE,+BAA+B,QAAQ,iCACpC,YAAY,oCACkB,gBAAgB;CAKrD,OACE,mCAAmC,QAAQ,YACxC,YAAY,oCACkB,gBAAgB;AAGrD;;;;;;;;AASA,MAAa,0BAA0B,OACrC,uBACA,eACA,UAA6B,kBACX;CAClB,IAAI,sBAAsB,SAAS,GAAG;CAEtC,MAAM,EAAE,oBAAoB,cAAc;CAC1C,MAAM,YAAY,QAAQ,iBAAiB,0BAA0B;CAErE,MAAM,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;CAE1C,MAAM,YACJ,CAAC,GAAG,sBAAsB,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,GAC1E,OAAO,CAAC,KAAK,gBAAgB;EAC3B,MAAM,YAAY,SAAS,OAAO,WAAW;GAC3C,MAAM,YAAY,WAAW,QAAQ,QAAQ;GAC7C,MAAM,UAAU,mCACd,KACA,CAAC,GAAG,UAAU,GACd,MACF;GAEA,MAAM,YAAY,wBAChB,iBACA,KACA,SACF;GACA,iBAAiB,WAAW,SAAS;GAErC,MAAM,mBAAmB,WAAW,OAAO,CAAC,CAAC,OAAO,UAAU;IAC5D,QAAQ,MACN,yBAAyB,aAAa,SAAS,EAAE,IACjD,KACF;GACF,CAAC;EACH,CAAC;CACH,CACF;AACF"}
|
|
@@ -1,45 +1,139 @@
|
|
|
1
1
|
import { parallelize } from "./utils/parallelize.mjs";
|
|
2
|
+
import { chunkArray } from "./utils/chunkArray.mjs";
|
|
2
3
|
import { getAppLogger, x } from "@intlayer/config/logger";
|
|
3
4
|
import { getConfiguration } from "@intlayer/config/node";
|
|
5
|
+
import { retryManager } from "@intlayer/config/utils";
|
|
4
6
|
import { createIntlayerCMS } from "@intlayer/api";
|
|
5
7
|
import { dictionaryEndpoint } from "@intlayer/api/dictionary";
|
|
6
8
|
|
|
7
9
|
//#region src/fetchDistantDictionaries.ts
|
|
8
10
|
/**
|
|
11
|
+
* Number of dictionaries requested per HTTP call. Must stay under the batch
|
|
12
|
+
* limit enforced by the backend (`MAX_DICTIONARY_KEYS_PER_REQUEST`).
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_DICTIONARIES_PER_REQUEST = 3;
|
|
15
|
+
/** Number of batch requests kept in flight at the same time. */
|
|
16
|
+
const DEFAULT_PARALLEL_REQUESTS = 5;
|
|
17
|
+
/** Number of retries performed on a failing batch request. */
|
|
18
|
+
const DEFAULT_MAX_RETRY = 2;
|
|
19
|
+
/** Delay between two attempts of a failing batch request, in milliseconds. */
|
|
20
|
+
const DEFAULT_RETRY_DELAY = 250;
|
|
21
|
+
/** HTTP statuses worth another attempt despite being client errors. */
|
|
22
|
+
const RETRYABLE_CLIENT_ERROR_STATUSES = /* @__PURE__ */ new Set([
|
|
23
|
+
408,
|
|
24
|
+
425,
|
|
25
|
+
429
|
|
26
|
+
]);
|
|
27
|
+
/**
|
|
28
|
+
* A request is worth retrying when it failed for a reason another attempt could
|
|
29
|
+
* fix: a network error (no status at all) or a server-side / throttling status.
|
|
30
|
+
* Replaying a `404` or a `401` only multiplies the noise.
|
|
31
|
+
*/
|
|
32
|
+
const isRetryableError = (error) => {
|
|
33
|
+
const status = error?.status;
|
|
34
|
+
if (typeof status !== "number") return true;
|
|
35
|
+
if (status >= 500) return true;
|
|
36
|
+
return RETRYABLE_CLIENT_ERROR_STATUSES.has(status);
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* A backend that predates the batch endpoint routes `/dictionary/by-keys` to
|
|
40
|
+
* its `/dictionary/:dictionaryKey` handler, which answers `404` - either as an
|
|
41
|
+
* unknown dictionary or as an unknown route. Both mean the same thing here:
|
|
42
|
+
* this backend cannot serve batches, so fall back to one request per key.
|
|
43
|
+
*/
|
|
44
|
+
const isBatchEndpointUnsupported = (error) => error?.status === 404;
|
|
45
|
+
/**
|
|
9
46
|
* Fetch distant dictionaries and update the logger with their statuses.
|
|
47
|
+
*
|
|
48
|
+
* Keys are grouped into batches fetched by a single request each, and those
|
|
49
|
+
* requests are kept in flight together — by default 5 concurrent requests of 3
|
|
50
|
+
* dictionaries, so 15 dictionaries travel at any given time. A slot is refilled
|
|
51
|
+
* as soon as its request settles, so no batch waits for the whole wave.
|
|
52
|
+
* Failing requests are retried before their keys are reported as errors.
|
|
10
53
|
*/
|
|
11
54
|
const fetchDistantDictionaries = async (options, onStatusUpdate) => {
|
|
12
55
|
const config = getConfiguration();
|
|
13
56
|
const appLogger = getAppLogger(config);
|
|
14
57
|
try {
|
|
15
58
|
const dictionary = dictionaryEndpoint(createIntlayerCMS(config));
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
59
|
+
const { dictionariesPerRequest = DEFAULT_DICTIONARIES_PER_REQUEST, parallelRequests = DEFAULT_PARALLEL_REQUESTS, maxRetry = DEFAULT_MAX_RETRY, retryDelay = DEFAULT_RETRY_DELAY } = options;
|
|
60
|
+
const distantDictionariesKeys = [...new Set(options.dictionaryKeys)];
|
|
61
|
+
if (distantDictionariesKeys.length === 0) return [];
|
|
62
|
+
const batches = chunkArray(distantDictionariesKeys, dictionariesPerRequest);
|
|
63
|
+
let isBatchEndpointSupported = true;
|
|
64
|
+
let hasLoggedBatchFallback = false;
|
|
65
|
+
/**
|
|
66
|
+
* Fetch each key of a batch with its own request. Used against backends
|
|
67
|
+
* without the batch endpoint. A key missing on the remote only discards
|
|
68
|
+
* itself, never its neighbours.
|
|
69
|
+
*/
|
|
70
|
+
const requestDictionariesOneByOne = async (batchKeys) => {
|
|
71
|
+
return (await parallelize(batchKeys, async (dictionaryKey) => {
|
|
72
|
+
try {
|
|
73
|
+
return (await dictionary.getDictionary(dictionaryKey)).data;
|
|
74
|
+
} catch (_error) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}, batchKeys.length)).filter((distantDictionary) => distantDictionary !== void 0);
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Request one batch of dictionaries, retrying the whole request when it
|
|
81
|
+
* fails for a transient reason - a batch carries several dictionaries, so
|
|
82
|
+
* a network hiccup would otherwise discard all of them at once.
|
|
83
|
+
*/
|
|
84
|
+
const requestBatch = retryManager(async (batchKeys) => {
|
|
85
|
+
if (isBatchEndpointSupported) try {
|
|
86
|
+
return (await dictionary.getDictionariesByKeys(batchKeys)).data ?? [];
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (!isBatchEndpointUnsupported(error)) throw error;
|
|
89
|
+
isBatchEndpointSupported = false;
|
|
90
|
+
if (!hasLoggedBatchFallback) {
|
|
91
|
+
hasLoggedBatchFallback = true;
|
|
92
|
+
appLogger("This backend does not provide the batch dictionary endpoint - falling back to one request per dictionary.", { level: "warn" });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return await requestDictionariesOneByOne(batchKeys);
|
|
96
|
+
}, {
|
|
97
|
+
maxRetry,
|
|
98
|
+
delay: retryDelay,
|
|
99
|
+
shouldRetry: isRetryableError,
|
|
100
|
+
onError: ({ error, attempt }) => appLogger(`Retrying distant dictionaries request (attempt ${attempt + 1}/${maxRetry}): ${error}`, { level: "warn" })
|
|
101
|
+
});
|
|
102
|
+
/**
|
|
103
|
+
* Fetch one batch of dictionaries within a single request, and report the
|
|
104
|
+
* resulting status of every key it contains.
|
|
105
|
+
*/
|
|
106
|
+
const processBatch = async (batchKeys) => {
|
|
107
|
+
onStatusUpdate?.(batchKeys.map((dictionaryKey) => ({
|
|
19
108
|
dictionaryKey,
|
|
20
109
|
type: "remote",
|
|
21
110
|
status: "fetching"
|
|
22
|
-
}
|
|
111
|
+
})));
|
|
23
112
|
try {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
onStatusUpdate?.(
|
|
113
|
+
const distantDictionaries = await requestBatch(batchKeys);
|
|
114
|
+
const fetchedKeys = new Set(distantDictionaries.map((distantDictionary) => distantDictionary.key));
|
|
115
|
+
onStatusUpdate?.(batchKeys.map((dictionaryKey) => fetchedKeys.has(dictionaryKey) ? {
|
|
27
116
|
dictionaryKey,
|
|
28
117
|
type: "remote",
|
|
29
118
|
status: "fetched"
|
|
30
|
-
}
|
|
31
|
-
|
|
119
|
+
} : {
|
|
120
|
+
dictionaryKey,
|
|
121
|
+
type: "remote",
|
|
122
|
+
status: "error",
|
|
123
|
+
error: `Dictionary ${dictionaryKey} not found on remote`
|
|
124
|
+
}));
|
|
125
|
+
return distantDictionaries;
|
|
32
126
|
} catch (error) {
|
|
33
|
-
onStatusUpdate?.(
|
|
127
|
+
onStatusUpdate?.(batchKeys.map((dictionaryKey) => ({
|
|
34
128
|
dictionaryKey,
|
|
35
129
|
type: "remote",
|
|
36
130
|
status: "error",
|
|
37
131
|
error: `Error fetching dictionary ${dictionaryKey}: ${error}`
|
|
38
|
-
}
|
|
39
|
-
return;
|
|
132
|
+
})));
|
|
133
|
+
return [];
|
|
40
134
|
}
|
|
41
135
|
};
|
|
42
|
-
return (await parallelize(
|
|
136
|
+
return (await parallelize(batches, processBatch, parallelRequests)).flat();
|
|
43
137
|
} catch (_error) {
|
|
44
138
|
appLogger(`${x} Failed to fetch distant dictionaries`, { level: "error" });
|
|
45
139
|
return [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetchDistantDictionaries.mjs","names":[],"sources":["../../src/fetchDistantDictionaries.ts"],"sourcesContent":["import { createIntlayerCMS } from '@intlayer/api';\nimport { dictionaryEndpoint } from '@intlayer/api/dictionary';\n// @ts-ignore @intlayer/backend is not build yet\nimport type { DictionaryAPI } from '@intlayer/backend';\nimport { getAppLogger, x } from '@intlayer/config/logger';\nimport { getConfiguration } from '@intlayer/config/node';\nimport type { DictionariesStatus } from './loadDictionaries';\nimport { parallelize } from './utils/parallelize';\n\ntype FetchDistantDictionariesOptions = {\n dictionaryKeys: string[];\n newDictionariesPath?: string;\n logPrefix?: string;\n};\n\n/**\n * Fetch distant dictionaries and update the logger with their statuses.\n */\nexport const fetchDistantDictionaries = async (\n options: FetchDistantDictionariesOptions,\n onStatusUpdate?: (status: DictionariesStatus[]) => void\n): Promise<DictionaryAPI[]> => {\n const config = getConfiguration();\n const appLogger = getAppLogger(config);\n try {\n const dictionary = dictionaryEndpoint(createIntlayerCMS(config));\n\n const distantDictionariesKeys = options.dictionaryKeys;\n // Process dictionaries in parallel with a concurrency limit\n const processDictionary = async (\n dictionaryKey: string\n ): Promise<DictionaryAPI | undefined> => {\n onStatusUpdate?.([\n {\n dictionaryKey,\n type: 'remote',\n status: 'fetching',\n },\n ]);\n\n try {\n // Fetch the dictionary\n const getDictionaryResult =\n await dictionary.getDictionary(dictionaryKey);\n\n const distantDictionary = getDictionaryResult.data;\n\n if (!distantDictionary) {\n throw new Error(`Dictionary ${dictionaryKey} not found on remote`);\n }\n\n onStatusUpdate?.([\n { dictionaryKey, type: 'remote', status: 'fetched' },\n ]);\n\n return distantDictionary;\n } catch (error) {\n onStatusUpdate?.([\n {\n dictionaryKey,\n type: 'remote',\n status: 'error',\n error: `Error fetching dictionary ${dictionaryKey}: ${error}`,\n },\n ]);\n return undefined;\n }\n };\n\n const result = await parallelize(\n distantDictionariesKeys,\n async (dictionaryKey) => await processDictionary(dictionaryKey)\n );\n\n // Remove undefined values\n const filteredResult = result.filter(\n (dict: DictionaryAPI | undefined): dict is DictionaryAPI =>\n dict !== undefined\n );\n\n return filteredResult;\n } catch (_error) {\n appLogger(`${x} Failed to fetch distant dictionaries`, { level: 'error' });\n return [];\n }\n};\n"],"mappings":";;;;;;;;;;AAkBA,MAAa,2BAA2B,OACtC,SACA,mBAC6B;CAC7B,MAAM,SAAS,iBAAiB;CAChC,MAAM,YAAY,aAAa,MAAM;CACrC,IAAI;EACF,MAAM,aAAa,mBAAmB,kBAAkB,MAAM,CAAC;EAE/D,MAAM,0BAA0B,QAAQ;EAExC,MAAM,oBAAoB,OACxB,kBACuC;GACvC,iBAAiB,CACf;IACE;IACA,MAAM;IACN,QAAQ;GACV,CACF,CAAC;GAED,IAAI;IAKF,MAAM,qBAAoB,MAFlB,WAAW,cAAc,aAAa,EAED,CAAC;IAE9C,IAAI,CAAC,mBACH,MAAM,IAAI,MAAM,cAAc,cAAc,qBAAqB;IAGnE,iBAAiB,CACf;KAAE;KAAe,MAAM;KAAU,QAAQ;IAAU,CACrD,CAAC;IAED,OAAO;GACT,SAAS,OAAO;IACd,iBAAiB,CACf;KACE;KACA,MAAM;KACN,QAAQ;KACR,OAAO,6BAA6B,cAAc,IAAI;IACxD,CACF,CAAC;IACD;GACF;EACF;EAaA,QALuB,MANF,YACnB,yBACA,OAAO,kBAAkB,MAAM,kBAAkB,aAAa,CAChE,EAG6B,CAAC,QAC3B,SACC,SAAS,MAGO;CACtB,SAAS,QAAQ;EACf,UAAU,GAAG,EAAE,wCAAwC,EAAE,OAAO,QAAQ,CAAC;EACzE,OAAO,CAAC;CACV;AACF"}
|
|
1
|
+
{"version":3,"file":"fetchDistantDictionaries.mjs","names":[],"sources":["../../src/fetchDistantDictionaries.ts"],"sourcesContent":["import { createIntlayerCMS } from '@intlayer/api';\nimport { dictionaryEndpoint } from '@intlayer/api/dictionary';\n// @ts-ignore @intlayer/backend is not build yet\nimport type { DictionaryAPI } from '@intlayer/backend';\nimport { getAppLogger, x } from '@intlayer/config/logger';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { retryManager } from '@intlayer/config/utils';\nimport type { DictionariesStatus } from './loadDictionaries';\nimport { chunkArray } from './utils/chunkArray';\nimport { parallelize } from './utils/parallelize';\n\n/**\n * Number of dictionaries requested per HTTP call. Must stay under the batch\n * limit enforced by the backend (`MAX_DICTIONARY_KEYS_PER_REQUEST`).\n */\nconst DEFAULT_DICTIONARIES_PER_REQUEST = 3;\n\n/** Number of batch requests kept in flight at the same time. */\nconst DEFAULT_PARALLEL_REQUESTS = 5;\n\n/** Number of retries performed on a failing batch request. */\nconst DEFAULT_MAX_RETRY = 2;\n\n/** Delay between two attempts of a failing batch request, in milliseconds. */\nconst DEFAULT_RETRY_DELAY = 250;\n\n/** HTTP statuses worth another attempt despite being client errors. */\nconst RETRYABLE_CLIENT_ERROR_STATUSES = new Set([408, 425, 429]);\n\n/**\n * A request is worth retrying when it failed for a reason another attempt could\n * fix: a network error (no status at all) or a server-side / throttling status.\n * Replaying a `404` or a `401` only multiplies the noise.\n */\nconst isRetryableError = (error: unknown): boolean => {\n const status = (error as { status?: number })?.status;\n\n if (typeof status !== 'number') return true;\n\n if (status >= 500) return true;\n\n return RETRYABLE_CLIENT_ERROR_STATUSES.has(status);\n};\n\n/**\n * A backend that predates the batch endpoint routes `/dictionary/by-keys` to\n * its `/dictionary/:dictionaryKey` handler, which answers `404` - either as an\n * unknown dictionary or as an unknown route. Both mean the same thing here:\n * this backend cannot serve batches, so fall back to one request per key.\n */\nconst isBatchEndpointUnsupported = (error: unknown): boolean =>\n (error as { status?: number })?.status === 404;\n\ntype FetchDistantDictionariesOptions = {\n dictionaryKeys: string[];\n newDictionariesPath?: string;\n logPrefix?: string;\n /**\n * Number of dictionaries fetched by a single request.\n * @default 3\n */\n dictionariesPerRequest?: number;\n /**\n * Number of requests kept in flight at the same time.\n * @default 5\n */\n parallelRequests?: number;\n /**\n * Number of retries performed when a batch request fails.\n * @default 2\n */\n maxRetry?: number;\n /**\n * Delay between two attempts of a failing batch request, in milliseconds.\n * @default 250\n */\n retryDelay?: number;\n};\n\n/**\n * Fetch distant dictionaries and update the logger with their statuses.\n *\n * Keys are grouped into batches fetched by a single request each, and those\n * requests are kept in flight together — by default 5 concurrent requests of 3\n * dictionaries, so 15 dictionaries travel at any given time. A slot is refilled\n * as soon as its request settles, so no batch waits for the whole wave.\n * Failing requests are retried before their keys are reported as errors.\n */\nexport const fetchDistantDictionaries = async (\n options: FetchDistantDictionariesOptions,\n onStatusUpdate?: (status: DictionariesStatus[]) => void\n): Promise<DictionaryAPI[]> => {\n const config = getConfiguration();\n const appLogger = getAppLogger(config);\n try {\n const dictionary = dictionaryEndpoint(createIntlayerCMS(config));\n\n const {\n dictionariesPerRequest = DEFAULT_DICTIONARIES_PER_REQUEST,\n parallelRequests = DEFAULT_PARALLEL_REQUESTS,\n maxRetry = DEFAULT_MAX_RETRY,\n retryDelay = DEFAULT_RETRY_DELAY,\n } = options;\n\n // A same key can be listed twice when several qualified dictionaries share\n // it - one request per key is enough to retrieve all of its siblings.\n const distantDictionariesKeys = [...new Set(options.dictionaryKeys)];\n\n if (distantDictionariesKeys.length === 0) return [];\n\n const batches = chunkArray(distantDictionariesKeys, dictionariesPerRequest);\n\n // Flipped once for the whole run as soon as a backend proves unable to\n // serve batches, so the remaining batches skip the doomed request.\n let isBatchEndpointSupported = true;\n let hasLoggedBatchFallback = false;\n\n /**\n * Fetch each key of a batch with its own request. Used against backends\n * without the batch endpoint. A key missing on the remote only discards\n * itself, never its neighbours.\n */\n const requestDictionariesOneByOne = async (\n batchKeys: string[]\n ): Promise<DictionaryAPI[]> => {\n const results = await parallelize(\n batchKeys,\n async (dictionaryKey) => {\n try {\n const getDictionaryResult =\n await dictionary.getDictionary(dictionaryKey);\n\n return getDictionaryResult.data;\n } catch (_error) {\n return undefined;\n }\n },\n batchKeys.length\n );\n\n return results.filter(\n (distantDictionary): distantDictionary is DictionaryAPI =>\n distantDictionary !== undefined\n );\n };\n\n /**\n * Request one batch of dictionaries, retrying the whole request when it\n * fails for a transient reason - a batch carries several dictionaries, so\n * a network hiccup would otherwise discard all of them at once.\n */\n const requestBatch = retryManager(\n async (batchKeys: string[]): Promise<DictionaryAPI[]> => {\n if (isBatchEndpointSupported) {\n try {\n const getDictionariesResult =\n await dictionary.getDictionariesByKeys(batchKeys);\n\n return getDictionariesResult.data ?? [];\n } catch (error) {\n if (!isBatchEndpointUnsupported(error)) throw error;\n\n isBatchEndpointSupported = false;\n\n if (!hasLoggedBatchFallback) {\n hasLoggedBatchFallback = true;\n appLogger(\n 'This backend does not provide the batch dictionary endpoint - falling back to one request per dictionary.',\n { level: 'warn' }\n );\n }\n }\n }\n\n return await requestDictionariesOneByOne(batchKeys);\n },\n {\n maxRetry,\n delay: retryDelay,\n shouldRetry: isRetryableError,\n onError: ({ error, attempt }) =>\n appLogger(\n `Retrying distant dictionaries request (attempt ${attempt + 1}/${maxRetry}): ${error}`,\n { level: 'warn' }\n ),\n }\n );\n\n /**\n * Fetch one batch of dictionaries within a single request, and report the\n * resulting status of every key it contains.\n */\n const processBatch = async (\n batchKeys: string[]\n ): Promise<DictionaryAPI[]> => {\n onStatusUpdate?.(\n batchKeys.map((dictionaryKey) => ({\n dictionaryKey,\n type: 'remote',\n status: 'fetching',\n }))\n );\n\n try {\n const distantDictionaries = await requestBatch(batchKeys);\n\n const fetchedKeys = new Set(\n distantDictionaries.map(\n (distantDictionary: DictionaryAPI) => distantDictionary.key\n )\n );\n\n onStatusUpdate?.(\n batchKeys.map(\n (dictionaryKey): DictionariesStatus =>\n fetchedKeys.has(dictionaryKey)\n ? {\n dictionaryKey,\n type: 'remote',\n status: 'fetched',\n }\n : {\n dictionaryKey,\n type: 'remote',\n status: 'error',\n error: `Dictionary ${dictionaryKey} not found on remote`,\n }\n )\n );\n\n return distantDictionaries;\n } catch (error) {\n onStatusUpdate?.(\n batchKeys.map((dictionaryKey) => ({\n dictionaryKey,\n type: 'remote',\n status: 'error',\n error: `Error fetching dictionary ${dictionaryKey}: ${error}`,\n }))\n );\n return [];\n }\n };\n\n const batchResults = await parallelize(\n batches,\n processBatch,\n parallelRequests\n );\n\n return batchResults.flat();\n } catch (_error) {\n appLogger(`${x} Failed to fetch distant dictionaries`, { level: 'error' });\n return [];\n }\n};\n"],"mappings":";;;;;;;;;;;;;AAeA,MAAM,mCAAmC;;AAGzC,MAAM,4BAA4B;;AAGlC,MAAM,oBAAoB;;AAG1B,MAAM,sBAAsB;;AAG5B,MAAM,kDAAkC,IAAI,IAAI;CAAC;CAAK;CAAK;AAAG,CAAC;;;;;;AAO/D,MAAM,oBAAoB,UAA4B;CACpD,MAAM,SAAU,OAA+B;CAE/C,IAAI,OAAO,WAAW,UAAU,OAAO;CAEvC,IAAI,UAAU,KAAK,OAAO;CAE1B,OAAO,gCAAgC,IAAI,MAAM;AACnD;;;;;;;AAQA,MAAM,8BAA8B,UACjC,OAA+B,WAAW;;;;;;;;;;AAqC7C,MAAa,2BAA2B,OACtC,SACA,mBAC6B;CAC7B,MAAM,SAAS,iBAAiB;CAChC,MAAM,YAAY,aAAa,MAAM;CACrC,IAAI;EACF,MAAM,aAAa,mBAAmB,kBAAkB,MAAM,CAAC;EAE/D,MAAM,EACJ,yBAAyB,kCACzB,mBAAmB,2BACnB,WAAW,mBACX,aAAa,wBACX;EAIJ,MAAM,0BAA0B,CAAC,GAAG,IAAI,IAAI,QAAQ,cAAc,CAAC;EAEnE,IAAI,wBAAwB,WAAW,GAAG,OAAO,CAAC;EAElD,MAAM,UAAU,WAAW,yBAAyB,sBAAsB;EAI1E,IAAI,2BAA2B;EAC/B,IAAI,yBAAyB;;;;;;EAO7B,MAAM,8BAA8B,OAClC,cAC6B;GAgB7B,QAAO,MAfe,YACpB,WACA,OAAO,kBAAkB;IACvB,IAAI;KAIF,QAAO,MAFC,WAAW,cAAc,aAAa,EAEpB,CAAC;IAC7B,SAAS,QAAQ;KACf;IACF;GACF,GACA,UAAU,MACZ,EAEc,CAAC,QACZ,sBACC,sBAAsB,MAC1B;EACF;;;;;;EAOA,MAAM,eAAe,aACnB,OAAO,cAAkD;GACvD,IAAI,0BACF,IAAI;IAIF,QAAO,MAFC,WAAW,sBAAsB,SAAS,EAEtB,CAAC,QAAQ,CAAC;GACxC,SAAS,OAAO;IACd,IAAI,CAAC,2BAA2B,KAAK,GAAG,MAAM;IAE9C,2BAA2B;IAE3B,IAAI,CAAC,wBAAwB;KAC3B,yBAAyB;KACzB,UACE,6GACA,EAAE,OAAO,OAAO,CAClB;IACF;GACF;GAGF,OAAO,MAAM,4BAA4B,SAAS;EACpD,GACA;GACE;GACA,OAAO;GACP,aAAa;GACb,UAAU,EAAE,OAAO,cACjB,UACE,kDAAkD,UAAU,EAAE,GAAG,SAAS,KAAK,SAC/E,EAAE,OAAO,OAAO,CAClB;EACJ,CACF;;;;;EAMA,MAAM,eAAe,OACnB,cAC6B;GAC7B,iBACE,UAAU,KAAK,mBAAmB;IAChC;IACA,MAAM;IACN,QAAQ;GACV,EAAE,CACJ;GAEA,IAAI;IACF,MAAM,sBAAsB,MAAM,aAAa,SAAS;IAExD,MAAM,cAAc,IAAI,IACtB,oBAAoB,KACjB,sBAAqC,kBAAkB,GAC1D,CACF;IAEA,iBACE,UAAU,KACP,kBACC,YAAY,IAAI,aAAa,IACzB;KACE;KACA,MAAM;KACN,QAAQ;IACV,IACA;KACE;KACA,MAAM;KACN,QAAQ;KACR,OAAO,cAAc,cAAc;IACrC,CACR,CACF;IAEA,OAAO;GACT,SAAS,OAAO;IACd,iBACE,UAAU,KAAK,mBAAmB;KAChC;KACA,MAAM;KACN,QAAQ;KACR,OAAO,6BAA6B,cAAc,IAAI;IACxD,EAAE,CACJ;IACA,OAAO,CAAC;GACV;EACF;EAQA,QAAO,MANoB,YACzB,SACA,cACA,gBACF,EAEmB,CAAC,KAAK;CAC3B,SAAS,QAAQ;EACf,UAAU,GAAG,EAAE,wCAAwC,EAAE,OAAO,QAAQ,CAAC;EACzE,OAAO,CAAC;CACV;AACF"}
|
|
@@ -80,7 +80,7 @@ const setHtmlLangAndDir = (ast) => {
|
|
|
80
80
|
const upsertAttribute = (name, value) => {
|
|
81
81
|
const existing = node.attributes?.find((attr) => attr.type === "JSXAttribute" && attr.name?.name === name);
|
|
82
82
|
if (existing) existing.value = value;
|
|
83
|
-
else node.attributes
|
|
83
|
+
else node.attributes?.push(b.jsxAttribute(b.jsxIdentifier(name), value));
|
|
84
84
|
};
|
|
85
85
|
upsertAttribute("lang", b.jsxExpressionContainer(b.identifier("locale")));
|
|
86
86
|
upsertAttribute("dir", b.jsxExpressionContainer(b.callExpression(b.identifier("getHTMLTextDir"), [b.identifier("locale")])));
|