@intlayer/engine 9.1.3 → 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.
Files changed (100) hide show
  1. package/dist/assets/initConfig/templates/cjs.txt +4 -1
  2. package/dist/assets/initConfig/templates/json.txt +4 -1
  3. package/dist/assets/initConfig/templates/mjs.txt +4 -1
  4. package/dist/assets/initConfig/templates/ts.txt +4 -1
  5. package/dist/cjs/buildIntlayerDictionary/buildIntlayerDictionary.cjs +14 -2
  6. package/dist/cjs/buildIntlayerDictionary/buildIntlayerDictionary.cjs.map +1 -1
  7. package/dist/cjs/buildIntlayerDictionary/writeDynamicDictionary.cjs +24 -4
  8. package/dist/cjs/buildIntlayerDictionary/writeDynamicDictionary.cjs.map +1 -1
  9. package/dist/cjs/buildIntlayerDictionary/writeFetchDictionary.cjs +43 -6
  10. package/dist/cjs/buildIntlayerDictionary/writeFetchDictionary.cjs.map +1 -1
  11. package/dist/cjs/buildIntlayerDictionary/writeNestedDictionary.cjs +96 -0
  12. package/dist/cjs/buildIntlayerDictionary/writeNestedDictionary.cjs.map +1 -0
  13. package/dist/cjs/docReview/alignBlocks.cjs +57 -8
  14. package/dist/cjs/docReview/alignBlocks.cjs.map +1 -1
  15. package/dist/cjs/docReview/rebuildDocument.cjs +37 -9
  16. package/dist/cjs/docReview/rebuildDocument.cjs.map +1 -1
  17. package/dist/cjs/docReview/segmentDocument.cjs +19 -2
  18. package/dist/cjs/docReview/segmentDocument.cjs.map +1 -1
  19. package/dist/cjs/fetchDistantDictionaries.cjs +107 -13
  20. package/dist/cjs/fetchDistantDictionaries.cjs.map +1 -1
  21. package/dist/cjs/init/frameworkSetup/tanstackStart/transforms.cjs +1 -1
  22. package/dist/cjs/init/frameworkSetup/tanstackStart/transforms.cjs.map +1 -1
  23. package/dist/cjs/loadDictionaries/loadDictionaries.cjs +4 -2
  24. package/dist/cjs/loadDictionaries/loadDictionaries.cjs.map +1 -1
  25. package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs +3 -2
  26. package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs.map +1 -1
  27. package/dist/cjs/loadDictionaries/log.cjs +28 -5
  28. package/dist/cjs/loadDictionaries/log.cjs.map +1 -1
  29. package/dist/cjs/prepareIntlayer.cjs +7 -3
  30. package/dist/cjs/prepareIntlayer.cjs.map +1 -1
  31. package/dist/cjs/utils/chunkArray.cjs +24 -0
  32. package/dist/cjs/utils/chunkArray.cjs.map +1 -0
  33. package/dist/cjs/utils/index.cjs +2 -0
  34. package/dist/cjs/utils/runOnce.cjs +129 -50
  35. package/dist/cjs/utils/runOnce.cjs.map +1 -1
  36. package/dist/cjs/writeContentDeclaration/transformJSFile.cjs +153 -92
  37. package/dist/cjs/writeContentDeclaration/transformJSFile.cjs.map +1 -1
  38. package/dist/cjs/writeFileIfChanged.cjs +14 -7
  39. package/dist/cjs/writeFileIfChanged.cjs.map +1 -1
  40. package/dist/esm/buildIntlayerDictionary/buildIntlayerDictionary.mjs +15 -3
  41. package/dist/esm/buildIntlayerDictionary/buildIntlayerDictionary.mjs.map +1 -1
  42. package/dist/esm/buildIntlayerDictionary/writeDynamicDictionary.mjs +24 -4
  43. package/dist/esm/buildIntlayerDictionary/writeDynamicDictionary.mjs.map +1 -1
  44. package/dist/esm/buildIntlayerDictionary/writeFetchDictionary.mjs +43 -6
  45. package/dist/esm/buildIntlayerDictionary/writeFetchDictionary.mjs.map +1 -1
  46. package/dist/esm/buildIntlayerDictionary/writeNestedDictionary.mjs +92 -0
  47. package/dist/esm/buildIntlayerDictionary/writeNestedDictionary.mjs.map +1 -0
  48. package/dist/esm/docReview/alignBlocks.mjs +57 -8
  49. package/dist/esm/docReview/alignBlocks.mjs.map +1 -1
  50. package/dist/esm/docReview/rebuildDocument.mjs +37 -9
  51. package/dist/esm/docReview/rebuildDocument.mjs.map +1 -1
  52. package/dist/esm/docReview/segmentDocument.mjs +19 -2
  53. package/dist/esm/docReview/segmentDocument.mjs.map +1 -1
  54. package/dist/esm/fetchDistantDictionaries.mjs +107 -13
  55. package/dist/esm/fetchDistantDictionaries.mjs.map +1 -1
  56. package/dist/esm/init/frameworkSetup/tanstackStart/transforms.mjs +1 -1
  57. package/dist/esm/init/frameworkSetup/tanstackStart/transforms.mjs.map +1 -1
  58. package/dist/esm/loadDictionaries/loadDictionaries.mjs +4 -2
  59. package/dist/esm/loadDictionaries/loadDictionaries.mjs.map +1 -1
  60. package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs +3 -2
  61. package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs.map +1 -1
  62. package/dist/esm/loadDictionaries/log.mjs +28 -5
  63. package/dist/esm/loadDictionaries/log.mjs.map +1 -1
  64. package/dist/esm/prepareIntlayer.mjs +7 -3
  65. package/dist/esm/prepareIntlayer.mjs.map +1 -1
  66. package/dist/esm/utils/chunkArray.mjs +22 -0
  67. package/dist/esm/utils/chunkArray.mjs.map +1 -0
  68. package/dist/esm/utils/index.mjs +2 -1
  69. package/dist/esm/utils/runOnce.mjs +129 -50
  70. package/dist/esm/utils/runOnce.mjs.map +1 -1
  71. package/dist/esm/writeContentDeclaration/transformJSFile.mjs +153 -92
  72. package/dist/esm/writeContentDeclaration/transformJSFile.mjs.map +1 -1
  73. package/dist/esm/writeFileIfChanged.mjs +15 -8
  74. package/dist/esm/writeFileIfChanged.mjs.map +1 -1
  75. package/dist/types/buildIntlayerDictionary/buildIntlayerDictionary.d.ts.map +1 -1
  76. package/dist/types/buildIntlayerDictionary/writeDynamicDictionary.d.ts +14 -2
  77. package/dist/types/buildIntlayerDictionary/writeDynamicDictionary.d.ts.map +1 -1
  78. package/dist/types/buildIntlayerDictionary/writeFetchDictionary.d.ts +16 -2
  79. package/dist/types/buildIntlayerDictionary/writeFetchDictionary.d.ts.map +1 -1
  80. package/dist/types/buildIntlayerDictionary/writeNestedDictionary.d.ts +54 -0
  81. package/dist/types/buildIntlayerDictionary/writeNestedDictionary.d.ts.map +1 -0
  82. package/dist/types/docReview/alignBlocks.d.ts +4 -2
  83. package/dist/types/docReview/alignBlocks.d.ts.map +1 -1
  84. package/dist/types/docReview/rebuildDocument.d.ts.map +1 -1
  85. package/dist/types/docReview/segmentDocument.d.ts.map +1 -1
  86. package/dist/types/docReview/types.d.ts +9 -0
  87. package/dist/types/docReview/types.d.ts.map +1 -1
  88. package/dist/types/fetchDistantDictionaries.d.ts +26 -0
  89. package/dist/types/fetchDistantDictionaries.d.ts.map +1 -1
  90. package/dist/types/formatDictionary.d.ts +2 -0
  91. package/dist/types/formatDictionary.d.ts.map +1 -1
  92. package/dist/types/loadDictionaries/log.d.ts +16 -1
  93. package/dist/types/loadDictionaries/log.d.ts.map +1 -1
  94. package/dist/types/utils/chunkArray.d.ts +16 -0
  95. package/dist/types/utils/chunkArray.d.ts.map +1 -0
  96. package/dist/types/utils/index.d.ts +3 -2
  97. package/dist/types/utils/runOnce.d.ts +30 -6
  98. package/dist/types/utils/runOnce.d.ts.map +1 -1
  99. package/dist/types/writeContentDeclaration/transformJSFile.d.ts.map +1 -1
  100. package/package.json +11 -11
@@ -1 +1 @@
1
- {"version":3,"file":"writeFileIfChanged.cjs","names":[],"sources":["../../src/writeFileIfChanged.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { rmSync } from 'node:fs';\nimport {\n chmod,\n mkdir,\n readFile,\n rename,\n rm,\n stat,\n writeFile,\n} from 'node:fs/promises';\nimport { basename, join } from 'node:path';\n\nconst activeTempFiles = new Set<string>();\n\n// Synchronous cleanup on process exit\nprocess.on('exit', () => {\n for (const file of activeTempFiles) {\n try {\n rmSync(file, { force: true });\n } catch {}\n }\n});\n\n/**\n * Permission bits a freshly created file receives on this system. Detected once\n * from the first temp file we actually write (`process.umask()` with no\n * argument is deprecated and thread-unsafe) and cached, so the atomic path can\n * skip restoring the mode whenever the source file already uses the default\n * mode — which is the case for essentially every generated file.\n */\nlet defaultFileMode: number | undefined;\n\n/**\n * Options for {@link writeFileIfChanged}.\n */\nexport type WriteFileIfChangedOptions = {\n /** Encoding used to turn `data` into bytes. Defaults to `'utf8'`. */\n encoding?: BufferEncoding;\n /** Directory to hold the temporary file used for the atomic swap. */\n tempDir?: string;\n /**\n * Write atomically via a temp file + `rename` so readers never observe a\n * half-written file. This creates a fresh inode on every write, which roughly\n * doubles the cost of a changed write compared to overwriting in place. Set to\n * `false` for callers whose consumers tolerate torn reads to write directly.\n * Defaults to `true`.\n */\n atomic?: boolean;\n};\n\n/**\n * Write `data` to `path` only when it differs from the file already on disk,\n * preserving the existing file's permission mode.\n *\n * The whole file is read back and byte-compared because the files written here\n * (dictionaries, entry points, types) are small, so reading beats streaming a\n * hash. When the content is unchanged the write is skipped entirely, which both\n * avoids inode churn and prevents downstream watchers from rebuilding.\n *\n * @returns `true` when the file was written, `false` when it was already up to date.\n */\nexport const writeFileIfChanged = async (\n path: string,\n data: string,\n { encoding = 'utf8', tempDir, atomic = true }: WriteFileIfChangedOptions = {}\n): Promise<boolean> => {\n const newData = Buffer.from(data, encoding);\n\n // Read the current content first: the common case is an unchanged file, and\n // this single read is the whole fast path (no stat/chmod needed to bail out).\n let existingData: Buffer | null = null;\n try {\n existingData = await readFile(path);\n } catch {}\n\n if (existingData?.equals(newData)) {\n return false;\n }\n\n // Fast path: overwrite in place. Truncating an existing file keeps its inode,\n // so its permission mode is preserved automatically (no chmod needed). This\n // skips the temp-file inode churn at the cost of exposing readers to a\n // partially written file.\n if (!atomic) {\n await writeFile(path, newData);\n return true;\n }\n\n // The atomic swap replaces the inode, so the existing mode is not preserved\n // and must be reapplied. Only a file that already exists has a mode to keep.\n let modeToRestore: number | undefined;\n if (existingData !== null) {\n try {\n modeToRestore = (await stat(path)).mode & 0o777;\n } catch {}\n }\n\n if (tempDir) {\n await mkdir(tempDir, { recursive: true });\n }\n\n const tempFileName = `${basename(path)}.${Date.now()}-${randomBytes(4).toString('hex')}.tmp`;\n const tempPath = tempDir\n ? join(tempDir, tempFileName)\n : `${path}.${tempFileName}`;\n activeTempFiles.add(tempPath);\n\n try {\n await writeFile(tempPath, newData);\n\n if (modeToRestore !== undefined) {\n // Learn the ambient default mode once from a real temp file, then only\n // chmod when the source file used a non-default mode.\n if (defaultFileMode === undefined) {\n try {\n defaultFileMode = (await stat(tempPath)).mode & 0o777;\n } catch {}\n }\n if (modeToRestore !== defaultFileMode) {\n await chmod(tempPath, modeToRestore);\n }\n }\n\n await rename(tempPath, path);\n } catch (error) {\n try {\n await rm(tempPath, { force: true });\n } catch {}\n throw error;\n } finally {\n activeTempFiles.delete(tempPath);\n }\n\n return true;\n};\n"],"mappings":";;;;;;;AAaA,MAAM,kCAAkB,IAAI,IAAY;AAGxC,QAAQ,GAAG,cAAc;CACvB,KAAK,MAAM,QAAQ,iBACjB,IAAI;EACF,oBAAO,MAAM,EAAE,OAAO,KAAK,CAAC;CAC9B,QAAQ,CAAC;AAEb,CAAC;;;;;;;;AASD,IAAI;;;;;;;;;;;;AA+BJ,MAAa,qBAAqB,OAChC,MACA,MACA,EAAE,WAAW,QAAQ,SAAS,SAAS,SAAoC,CAAC,MACvD;CACrB,MAAM,UAAU,OAAO,KAAK,MAAM,QAAQ;CAI1C,IAAI,eAA8B;CAClC,IAAI;EACF,eAAe,qCAAe,IAAI;CACpC,QAAQ,CAAC;CAET,IAAI,cAAc,OAAO,OAAO,GAC9B,OAAO;CAOT,IAAI,CAAC,QAAQ;EACX,sCAAgB,MAAM,OAAO;EAC7B,OAAO;CACT;CAIA,IAAI;CACJ,IAAI,iBAAiB,MACnB,IAAI;EACF,iBAAiB,iCAAW,IAAI,EAAC,CAAE,OAAO;CAC5C,QAAQ,CAAC;CAGX,IAAI,SACF,kCAAY,SAAS,EAAE,WAAW,KAAK,CAAC;CAG1C,MAAM,eAAe,2BAAY,IAAI,EAAE,GAAG,KAAK,IAAI,EAAE,gCAAe,CAAC,CAAC,CAAC,SAAS,KAAK,EAAE;CACvF,MAAM,WAAW,8BACR,SAAS,YAAY,IAC1B,GAAG,KAAK,GAAG;CACf,gBAAgB,IAAI,QAAQ;CAE5B,IAAI;EACF,sCAAgB,UAAU,OAAO;EAEjC,IAAI,kBAAkB,QAAW;GAG/B,IAAI,oBAAoB,QACtB,IAAI;IACF,mBAAmB,iCAAW,QAAQ,EAAC,CAAE,OAAO;GAClD,QAAQ,CAAC;GAEX,IAAI,kBAAkB,iBACpB,kCAAY,UAAU,aAAa;EAEvC;EAEA,mCAAa,UAAU,IAAI;CAC7B,SAAS,OAAO;EACd,IAAI;GACF,+BAAS,UAAU,EAAE,OAAO,KAAK,CAAC;EACpC,QAAQ,CAAC;EACT,MAAM;CACR,UAAU;EACR,gBAAgB,OAAO,QAAQ;CACjC;CAEA,OAAO;AACT"}
1
+ {"version":3,"file":"writeFileIfChanged.cjs","names":[],"sources":["../../src/writeFileIfChanged.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { rmSync } from 'node:fs';\nimport {\n chmod,\n mkdir,\n readFile,\n rename,\n rm,\n stat,\n writeFile,\n} from 'node:fs/promises';\nimport { basename, dirname, join } from 'node:path';\n\nconst activeTempFiles = new Set<string>();\n\n// Synchronous cleanup on process exit\nprocess.on('exit', () => {\n for (const file of activeTempFiles) {\n try {\n rmSync(file, { force: true });\n } catch {}\n }\n});\n\n/**\n * Permission bits a freshly created file receives on this system. Detected once\n * from the first temp file we actually write (`process.umask()` with no\n * argument is deprecated and thread-unsafe) and cached, so the atomic path can\n * skip restoring the mode whenever the source file already uses the default\n * mode — which is the case for essentially every generated file.\n */\nlet defaultFileMode: number | undefined;\n\n/**\n * Options for {@link writeFileIfChanged}.\n */\nexport type WriteFileIfChangedOptions = {\n /** Encoding used to turn `data` into bytes. Defaults to `'utf8'`. */\n encoding?: BufferEncoding;\n /** Directory to hold the temporary file used for the atomic swap. */\n tempDir?: string;\n /**\n * Write atomically via a temp file + `rename` so readers never observe a\n * half-written file. This creates a fresh inode on every write, which roughly\n * doubles the cost of a changed write compared to overwriting in place. Set to\n * `false` for callers whose consumers tolerate torn reads to write directly.\n * Defaults to `true`.\n */\n atomic?: boolean;\n};\n\n/**\n * Write `data` to `path` only when it differs from the file already on disk,\n * preserving the existing file's permission mode.\n *\n * The whole file is read back and byte-compared because the files written here\n * (dictionaries, entry points, types) are small, so reading beats streaming a\n * hash. When the content is unchanged the write is skipped entirely, which both\n * avoids inode churn and prevents downstream watchers from rebuilding.\n *\n * @returns `true` when the file was written, `false` when it was already up to date.\n */\nexport const writeFileIfChanged = async (\n path: string,\n data: string,\n { encoding = 'utf8', tempDir, atomic = true }: WriteFileIfChangedOptions = {}\n): Promise<boolean> => {\n const newData = Buffer.from(data, encoding);\n\n // Read the current content first: the common case is an unchanged file, and\n // this single read is the whole fast path (no stat/chmod needed to bail out).\n let existingData: Buffer | null = null;\n try {\n existingData = await readFile(path);\n } catch {}\n\n if (existingData?.equals(newData)) {\n return false;\n }\n\n // Fast path: overwrite in place. Truncating an existing file keeps its inode,\n // so its permission mode is preserved automatically (no chmod needed). This\n // skips the temp-file inode churn at the cost of exposing readers to a\n // partially written file.\n if (!atomic) {\n await writeFile(path, newData);\n return true;\n }\n\n // The atomic swap replaces the inode, so the existing mode is not preserved\n // and must be reapplied. Only a file that already exists has a mode to keep.\n let modeToRestore: number | undefined;\n if (existingData !== null) {\n try {\n modeToRestore = (await stat(path)).mode & 0o777;\n } catch {}\n }\n\n if (tempDir) {\n await mkdir(tempDir, { recursive: true });\n }\n\n const tempFileName = `${basename(path)}.${Date.now()}-${randomBytes(4).toString('hex')}.tmp`;\n const tempPath = tempDir\n ? join(tempDir, tempFileName)\n : `${path}.${tempFileName}`;\n activeTempFiles.add(tempPath);\n\n try {\n for (let attempt = 0; ; attempt++) {\n try {\n await writeFile(tempPath, newData);\n\n if (modeToRestore !== undefined) {\n // Learn the ambient default mode once from a real temp file, then only\n // chmod when the source file used a non-default mode.\n if (defaultFileMode === undefined) {\n try {\n defaultFileMode = (await stat(tempPath)).mode & 0o777;\n } catch {}\n }\n if (modeToRestore !== defaultFileMode) {\n await chmod(tempPath, modeToRestore);\n }\n }\n\n await rename(tempPath, path);\n break;\n } catch (error) {\n const isMissingDirectory =\n (error as NodeJS.ErrnoException).code === 'ENOENT';\n\n if (!isMissingDirectory || attempt > 0) throw error;\n\n await mkdir(dirname(path), { recursive: true });\n if (tempDir) await mkdir(tempDir, { recursive: true });\n }\n }\n } catch (error) {\n try {\n await rm(tempPath, { force: true });\n } catch {}\n throw error;\n } finally {\n activeTempFiles.delete(tempPath);\n }\n\n return true;\n};\n"],"mappings":";;;;;;;AAaA,MAAM,kCAAkB,IAAI,IAAY;AAGxC,QAAQ,GAAG,cAAc;CACvB,KAAK,MAAM,QAAQ,iBACjB,IAAI;EACF,oBAAO,MAAM,EAAE,OAAO,KAAK,CAAC;CAC9B,QAAQ,CAAC;AAEb,CAAC;;;;;;;;AASD,IAAI;;;;;;;;;;;;AA+BJ,MAAa,qBAAqB,OAChC,MACA,MACA,EAAE,WAAW,QAAQ,SAAS,SAAS,SAAoC,CAAC,MACvD;CACrB,MAAM,UAAU,OAAO,KAAK,MAAM,QAAQ;CAI1C,IAAI,eAA8B;CAClC,IAAI;EACF,eAAe,qCAAe,IAAI;CACpC,QAAQ,CAAC;CAET,IAAI,cAAc,OAAO,OAAO,GAC9B,OAAO;CAOT,IAAI,CAAC,QAAQ;EACX,sCAAgB,MAAM,OAAO;EAC7B,OAAO;CACT;CAIA,IAAI;CACJ,IAAI,iBAAiB,MACnB,IAAI;EACF,iBAAiB,iCAAW,IAAI,EAAC,CAAE,OAAO;CAC5C,QAAQ,CAAC;CAGX,IAAI,SACF,kCAAY,SAAS,EAAE,WAAW,KAAK,CAAC;CAG1C,MAAM,eAAe,2BAAY,IAAI,EAAE,GAAG,KAAK,IAAI,EAAE,gCAAe,CAAC,CAAC,CAAC,SAAS,KAAK,EAAE;CACvF,MAAM,WAAW,8BACR,SAAS,YAAY,IAC1B,GAAG,KAAK,GAAG;CACf,gBAAgB,IAAI,QAAQ;CAE5B,IAAI;EACF,KAAK,IAAI,UAAU,IAAK,WACtB,IAAI;GACF,sCAAgB,UAAU,OAAO;GAEjC,IAAI,kBAAkB,QAAW;IAG/B,IAAI,oBAAoB,QACtB,IAAI;KACF,mBAAmB,iCAAW,QAAQ,EAAC,CAAE,OAAO;IAClD,QAAQ,CAAC;IAEX,IAAI,kBAAkB,iBACpB,kCAAY,UAAU,aAAa;GAEvC;GAEA,mCAAa,UAAU,IAAI;GAC3B;EACF,SAAS,OAAO;GAId,IAAI,EAFD,MAAgC,SAAS,aAEjB,UAAU,GAAG,MAAM;GAE9C,yDAAoB,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;GAC9C,IAAI,SAAS,kCAAY,SAAS,EAAE,WAAW,KAAK,CAAC;EACvD;CAEJ,SAAS,OAAO;EACd,IAAI;GACF,+BAAS,UAAU,EAAE,OAAO,KAAK,CAAC;EACpC,QAAQ,CAAC;EACT,MAAM;CACR,UAAU;EACR,gBAAgB,OAAO,QAAQ;CACjC;CAEA,OAAO;AACT"}
@@ -2,10 +2,11 @@ import { readDictionariesFromDisk } from "../utils/readDictionariesFromDisk.mjs"
2
2
  import { writeDynamicDictionary, writeDynamicQualifiedDictionaries } from "./writeDynamicDictionary.mjs";
3
3
  import { writeFetchDictionary } from "./writeFetchDictionary.mjs";
4
4
  import { writeMergedDictionaries } from "./writeMergedDictionary.mjs";
5
+ import { writeNestedDictionaries } from "./writeNestedDictionary.mjs";
5
6
  import { writeUnmergedDictionaries } from "./writeUnmergedDictionary.mjs";
6
7
  import { colorizeKey, getAppLogger } from "@intlayer/config/logger";
7
8
  import { IMPORT_MODE, OUTPUT_FORMAT } from "@intlayer/config/defaultValues";
8
- import { isQualifiedDictionaryGroup } from "@intlayer/core/dictionaryManipulator";
9
+ import { getNestedDictionaryGraph, isQualifiedDictionaryGroup } from "@intlayer/core/dictionaryManipulator";
9
10
 
10
11
  //#region src/buildIntlayerDictionary/buildIntlayerDictionary.ts
11
12
  const defaultOptions = {
@@ -35,6 +36,8 @@ const buildDictionary = async (localDictionariesEntries, configuration, options)
35
36
  }
36
37
  const unmergedDictionaries = await writeUnmergedDictionaries(unmergedDictionariesToUpdate, configuration, env);
37
38
  const mergedDictionaries = await writeMergedDictionaries(unmergedDictionaries, configuration);
39
+ const nestedDictionaryGraph = getNestedDictionaryGraph(Object.values(readDictionariesFromDisk(configuration.system.dictionariesDir)));
40
+ await writeNestedDictionaries(nestedDictionaryGraph, configuration, formats);
38
41
  const dictionariesToBuildDynamic = {};
39
42
  const qualifiedDictionariesToBuildDynamic = {};
40
43
  const keysToBuildFetch = /* @__PURE__ */ new Set();
@@ -56,14 +59,23 @@ const buildDictionary = async (localDictionariesEntries, configuration, options)
56
59
  };
57
60
  if (mode === "fetch") keysToBuildFetch.add(key);
58
61
  }
62
+ for (const consumerKey of Object.keys(dictionariesToBuildDynamic)) for (const nestedKey of nestedDictionaryGraph.get(consumerKey) ?? []) {
63
+ if (dictionariesToBuildDynamic[nestedKey]) continue;
64
+ const nestedResult = mergedDictionaries[nestedKey];
65
+ if (!nestedResult || isQualifiedDictionaryGroup(nestedResult.dictionary)) continue;
66
+ dictionariesToBuildDynamic[nestedKey] = {
67
+ dictionaryPath: nestedResult.dictionaryPath,
68
+ dictionary: nestedResult.dictionary
69
+ };
70
+ }
59
71
  let dynamicDictionaries = null;
60
- if (Object.keys(dictionariesToBuildDynamic).length > 0) dynamicDictionaries = await writeDynamicDictionary(dictionariesToBuildDynamic, configuration, formats);
72
+ if (Object.keys(dictionariesToBuildDynamic).length > 0) dynamicDictionaries = await writeDynamicDictionary(dictionariesToBuildDynamic, configuration, formats, nestedDictionaryGraph);
61
73
  if (Object.keys(qualifiedDictionariesToBuildDynamic).length > 0) await writeDynamicQualifiedDictionaries(qualifiedDictionariesToBuildDynamic, configuration, formats);
62
74
  let fetchDictionaries = null;
63
75
  if (dynamicDictionaries && keysToBuildFetch.size > 0) {
64
76
  const dictionariesToBuildFetch = {};
65
77
  for (const key of keysToBuildFetch) if (dynamicDictionaries[key]) dictionariesToBuildFetch[key] = dynamicDictionaries[key];
66
- if (Object.keys(dictionariesToBuildFetch).length > 0) fetchDictionaries = await writeFetchDictionary(dictionariesToBuildFetch, configuration, formats);
78
+ if (Object.keys(dictionariesToBuildFetch).length > 0) fetchDictionaries = await writeFetchDictionary(dictionariesToBuildFetch, configuration, formats, nestedDictionaryGraph);
67
79
  }
68
80
  return {
69
81
  unmergedDictionaries,
@@ -1 +1 @@
1
- {"version":3,"file":"buildIntlayerDictionary.mjs","names":[],"sources":["../../../src/buildIntlayerDictionary/buildIntlayerDictionary.ts"],"sourcesContent":["import { IMPORT_MODE, OUTPUT_FORMAT } from '@intlayer/config/defaultValues';\nimport { colorizeKey, getAppLogger } from '@intlayer/config/logger';\nimport { isQualifiedDictionaryGroup } from '@intlayer/core/dictionaryManipulator';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\nimport { readDictionariesFromDisk } from '../utils/readDictionariesFromDisk';\nimport {\n type LocalizedDictionaryOutput,\n type QualifiedMergedDictionaryOutput,\n writeDynamicDictionary,\n writeDynamicQualifiedDictionaries,\n} from './writeDynamicDictionary';\nimport { writeFetchDictionary } from './writeFetchDictionary';\nimport {\n type PlainMergedDictionaryOutput,\n writeMergedDictionaries,\n} from './writeMergedDictionary';\nimport { writeUnmergedDictionaries } from './writeUnmergedDictionary';\n\nexport type BuildDictionariesOptions = Partial<{\n formats: typeof OUTPUT_FORMAT;\n importOtherDictionaries: boolean;\n env: 'prod' | 'dev';\n}>;\n\nconst defaultOptions = {\n formats: OUTPUT_FORMAT,\n importOtherDictionaries: true,\n env: 'dev',\n} as const satisfies BuildDictionariesOptions;\n\n/**\n * This function transpile the bundled code to to make dictionaries as JSON files\n */\nexport const buildDictionary = async (\n localDictionariesEntries: Dictionary[],\n configuration: IntlayerConfig,\n options?: BuildDictionariesOptions\n) => {\n const importMode =\n configuration?.build?.importMode ??\n configuration?.dictionary?.importMode ??\n IMPORT_MODE;\n\n const { importOtherDictionaries, env, formats } = {\n ...defaultOptions,\n ...options,\n };\n\n const unmergedDictionariesToUpdate: Dictionary[] = [\n ...localDictionariesEntries,\n ];\n\n if (importOtherDictionaries) {\n const prevUnmergedDictionaries: Record<string, Dictionary[]> =\n readDictionariesFromDisk(configuration.system.unmergedDictionariesDir);\n\n // Reinsert other dictionaries with the same key to avoid merging errors\n for (const dictionaryToWrite of localDictionariesEntries) {\n const allPrebuiltUnmergedDictionaries =\n prevUnmergedDictionaries[dictionaryToWrite.key]!;\n\n if (allPrebuiltUnmergedDictionaries?.length > 0) {\n // Do not add the same dictionary again by filtering out the one with the same localId\n const otherUnmergedDictionaries =\n allPrebuiltUnmergedDictionaries?.filter(\n (unmergedDictionary) =>\n unmergedDictionary.localId !== dictionaryToWrite.localId\n );\n\n unmergedDictionariesToUpdate.push(...(otherUnmergedDictionaries ?? []));\n }\n }\n }\n\n const unmergedDictionaries = await writeUnmergedDictionaries(\n unmergedDictionariesToUpdate,\n configuration,\n env\n );\n\n const mergedDictionaries = await writeMergedDictionaries(\n unmergedDictionaries,\n configuration\n );\n\n const dictionariesToBuildDynamic: PlainMergedDictionaryOutput = {};\n const qualifiedDictionariesToBuildDynamic: QualifiedMergedDictionaryOutput =\n {};\n const keysToBuildFetch = new Set<string>();\n\n for (const [key, mergedResult] of Object.entries(mergedDictionaries)) {\n const dictionary = mergedResult.dictionary;\n\n if (isQualifiedDictionaryGroup(dictionary)) {\n // Collections / variants resolve their qualifier at\n // runtime from a selector. Static mode keeps every entry in one JSON;\n // dynamic mode splits them into one chunk per (locale, qualifierId).\n const entryMode = dictionary.importMode ?? importMode;\n\n if (entryMode === 'dynamic') {\n qualifiedDictionariesToBuildDynamic[key] = {\n dictionaryPath: mergedResult.dictionaryPath,\n dictionary,\n };\n } else if (entryMode === 'fetch') {\n const appLogger = getAppLogger(configuration);\n appLogger(\n `Dictionary ${colorizeKey(key)} uses 'fetch' import mode with (${dictionary.qualifierTypes.join(', ')}) entries — fetch mode is not qualifier-aware yet, falling back to static import.`,\n { level: 'warn' }\n );\n }\n\n continue;\n }\n\n const mode = dictionary.importMode ?? importMode;\n\n if (mode === 'dynamic' || mode === 'fetch') {\n dictionariesToBuildDynamic[key] = {\n dictionaryPath: mergedResult.dictionaryPath,\n dictionary,\n };\n }\n\n if (mode === 'fetch') {\n keysToBuildFetch.add(key);\n }\n }\n\n let dynamicDictionaries: LocalizedDictionaryOutput | null = null;\n\n if (Object.keys(dictionariesToBuildDynamic).length > 0) {\n dynamicDictionaries = await writeDynamicDictionary(\n dictionariesToBuildDynamic,\n configuration,\n formats\n );\n }\n\n if (Object.keys(qualifiedDictionariesToBuildDynamic).length > 0) {\n await writeDynamicQualifiedDictionaries(\n qualifiedDictionariesToBuildDynamic,\n configuration,\n formats\n );\n }\n\n let fetchDictionaries: LocalizedDictionaryOutput | null = null;\n\n if (dynamicDictionaries && keysToBuildFetch.size > 0) {\n const dictionariesToBuildFetch: LocalizedDictionaryOutput = {};\n\n for (const key of keysToBuildFetch) {\n if (dynamicDictionaries[key]) {\n dictionariesToBuildFetch[key] = dynamicDictionaries[key];\n }\n }\n\n if (Object.keys(dictionariesToBuildFetch).length > 0) {\n fetchDictionaries = await writeFetchDictionary(\n dictionariesToBuildFetch,\n configuration,\n formats\n );\n }\n }\n\n return {\n unmergedDictionaries,\n mergedDictionaries,\n dynamicDictionaries,\n fetchDictionaries,\n };\n};\n"],"mappings":";;;;;;;;;;AAyBA,MAAM,iBAAiB;CACrB,SAAS;CACT,yBAAyB;CACzB,KAAK;AACP;;;;AAKA,MAAa,kBAAkB,OAC7B,0BACA,eACA,YACG;CACH,MAAM,aACJ,eAAe,OAAO,cACtB,eAAe,YAAY,cAC3B;CAEF,MAAM,EAAE,yBAAyB,KAAK,YAAY;EAChD,GAAG;EACH,GAAG;CACL;CAEA,MAAM,+BAA6C,CACjD,GAAG,wBACL;CAEA,IAAI,yBAAyB;EAC3B,MAAM,2BACJ,yBAAyB,cAAc,OAAO,uBAAuB;EAGvE,KAAK,MAAM,qBAAqB,0BAA0B;GACxD,MAAM,kCACJ,yBAAyB,kBAAkB;GAE7C,IAAI,iCAAiC,SAAS,GAAG;IAE/C,MAAM,4BACJ,iCAAiC,QAC9B,uBACC,mBAAmB,YAAY,kBAAkB,OACrD;IAEF,6BAA6B,KAAK,GAAI,6BAA6B,CAAC,CAAE;GACxE;EACF;CACF;CAEA,MAAM,uBAAuB,MAAM,0BACjC,8BACA,eACA,GACF;CAEA,MAAM,qBAAqB,MAAM,wBAC/B,sBACA,aACF;CAEA,MAAM,6BAA0D,CAAC;CACjE,MAAM,sCACJ,CAAC;CACH,MAAM,mCAAmB,IAAI,IAAY;CAEzC,KAAK,MAAM,CAAC,KAAK,iBAAiB,OAAO,QAAQ,kBAAkB,GAAG;EACpE,MAAM,aAAa,aAAa;EAEhC,IAAI,2BAA2B,UAAU,GAAG;GAI1C,MAAM,YAAY,WAAW,cAAc;GAE3C,IAAI,cAAc,WAChB,oCAAoC,OAAO;IACzC,gBAAgB,aAAa;IAC7B;GACF;QACK,IAAI,cAAc,SAEvB,AADkB,aAAa,aACvB,CAAC,CACP,cAAc,YAAY,GAAG,EAAE,kCAAkC,WAAW,eAAe,KAAK,IAAI,EAAE,oFACtG,EAAE,OAAO,OAAO,CAClB;GAGF;EACF;EAEA,MAAM,OAAO,WAAW,cAAc;EAEtC,IAAI,SAAS,aAAa,SAAS,SACjC,2BAA2B,OAAO;GAChC,gBAAgB,aAAa;GAC7B;EACF;EAGF,IAAI,SAAS,SACX,iBAAiB,IAAI,GAAG;CAE5B;CAEA,IAAI,sBAAwD;CAE5D,IAAI,OAAO,KAAK,0BAA0B,CAAC,CAAC,SAAS,GACnD,sBAAsB,MAAM,uBAC1B,4BACA,eACA,OACF;CAGF,IAAI,OAAO,KAAK,mCAAmC,CAAC,CAAC,SAAS,GAC5D,MAAM,kCACJ,qCACA,eACA,OACF;CAGF,IAAI,oBAAsD;CAE1D,IAAI,uBAAuB,iBAAiB,OAAO,GAAG;EACpD,MAAM,2BAAsD,CAAC;EAE7D,KAAK,MAAM,OAAO,kBAChB,IAAI,oBAAoB,MACtB,yBAAyB,OAAO,oBAAoB;EAIxD,IAAI,OAAO,KAAK,wBAAwB,CAAC,CAAC,SAAS,GACjD,oBAAoB,MAAM,qBACxB,0BACA,eACA,OACF;CAEJ;CAEA,OAAO;EACL;EACA;EACA;EACA;CACF;AACF"}
1
+ {"version":3,"file":"buildIntlayerDictionary.mjs","names":[],"sources":["../../../src/buildIntlayerDictionary/buildIntlayerDictionary.ts"],"sourcesContent":["import { IMPORT_MODE, OUTPUT_FORMAT } from '@intlayer/config/defaultValues';\nimport { colorizeKey, getAppLogger } from '@intlayer/config/logger';\nimport {\n getNestedDictionaryGraph,\n isQualifiedDictionaryGroup,\n} from '@intlayer/core/dictionaryManipulator';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\nimport { readDictionariesFromDisk } from '../utils/readDictionariesFromDisk';\nimport {\n type LocalizedDictionaryOutput,\n type QualifiedMergedDictionaryOutput,\n writeDynamicDictionary,\n writeDynamicQualifiedDictionaries,\n} from './writeDynamicDictionary';\nimport { writeFetchDictionary } from './writeFetchDictionary';\nimport {\n type PlainMergedDictionaryOutput,\n writeMergedDictionaries,\n} from './writeMergedDictionary';\nimport { writeNestedDictionaries } from './writeNestedDictionary';\nimport { writeUnmergedDictionaries } from './writeUnmergedDictionary';\n\nexport type BuildDictionariesOptions = Partial<{\n formats: typeof OUTPUT_FORMAT;\n importOtherDictionaries: boolean;\n env: 'prod' | 'dev';\n}>;\n\nconst defaultOptions = {\n formats: OUTPUT_FORMAT,\n importOtherDictionaries: true,\n env: 'dev',\n} as const satisfies BuildDictionariesOptions;\n\n/**\n * This function transpile the bundled code to to make dictionaries as JSON files\n */\nexport const buildDictionary = async (\n localDictionariesEntries: Dictionary[],\n configuration: IntlayerConfig,\n options?: BuildDictionariesOptions\n) => {\n const importMode =\n configuration?.build?.importMode ??\n configuration?.dictionary?.importMode ??\n IMPORT_MODE;\n\n const { importOtherDictionaries, env, formats } = {\n ...defaultOptions,\n ...options,\n };\n\n const unmergedDictionariesToUpdate: Dictionary[] = [\n ...localDictionariesEntries,\n ];\n\n if (importOtherDictionaries) {\n const prevUnmergedDictionaries: Record<string, Dictionary[]> =\n readDictionariesFromDisk(configuration.system.unmergedDictionariesDir);\n\n // Reinsert other dictionaries with the same key to avoid merging errors\n for (const dictionaryToWrite of localDictionariesEntries) {\n const allPrebuiltUnmergedDictionaries =\n prevUnmergedDictionaries[dictionaryToWrite.key]!;\n\n if (allPrebuiltUnmergedDictionaries?.length > 0) {\n // Do not add the same dictionary again by filtering out the one with the same localId\n const otherUnmergedDictionaries =\n allPrebuiltUnmergedDictionaries?.filter(\n (unmergedDictionary) =>\n unmergedDictionary.localId !== dictionaryToWrite.localId\n );\n\n unmergedDictionariesToUpdate.push(...(otherUnmergedDictionaries ?? []));\n }\n }\n }\n\n const unmergedDictionaries = await writeUnmergedDictionaries(\n unmergedDictionariesToUpdate,\n configuration,\n env\n );\n\n const mergedDictionaries = await writeMergedDictionaries(\n unmergedDictionaries,\n configuration\n );\n\n // `nest()` dependency graph, read back from disk so it stays complete on\n // incremental rebuilds where only a subset of dictionaries was regenerated.\n // Every artifact kind attaches its dependencies, letting `getNesting` resolve\n // them locally instead of through the registry the optimizer strips.\n const nestedDictionaryGraph = getNestedDictionaryGraph(\n Object.values(\n readDictionariesFromDisk<Record<string, Dictionary>>(\n configuration.system.dictionariesDir\n )\n )\n );\n\n await writeNestedDictionaries(nestedDictionaryGraph, configuration, formats);\n\n const dictionariesToBuildDynamic: PlainMergedDictionaryOutput = {};\n const qualifiedDictionariesToBuildDynamic: QualifiedMergedDictionaryOutput =\n {};\n const keysToBuildFetch = new Set<string>();\n\n for (const [key, mergedResult] of Object.entries(mergedDictionaries)) {\n const dictionary = mergedResult.dictionary;\n\n if (isQualifiedDictionaryGroup(dictionary)) {\n // Collections / variants resolve their qualifier at\n // runtime from a selector. Static mode keeps every entry in one JSON;\n // dynamic mode splits them into one chunk per (locale, qualifierId).\n const entryMode = dictionary.importMode ?? importMode;\n\n if (entryMode === 'dynamic') {\n qualifiedDictionariesToBuildDynamic[key] = {\n dictionaryPath: mergedResult.dictionaryPath,\n dictionary,\n };\n } else if (entryMode === 'fetch') {\n const appLogger = getAppLogger(configuration);\n appLogger(\n `Dictionary ${colorizeKey(key)} uses 'fetch' import mode with (${dictionary.qualifierTypes.join(', ')}) entries — fetch mode is not qualifier-aware yet, falling back to static import.`,\n { level: 'warn' }\n );\n }\n\n continue;\n }\n\n const mode = dictionary.importMode ?? importMode;\n\n if (mode === 'dynamic' || mode === 'fetch') {\n dictionariesToBuildDynamic[key] = {\n dictionaryPath: mergedResult.dictionaryPath,\n dictionary,\n };\n }\n\n if (mode === 'fetch') {\n keysToBuildFetch.add(key);\n }\n }\n\n // A lazily loaded dictionary resolves its `nest()` references from chunks\n // loaded by the same loader, so its nest targets need dynamic artifacts of\n // their own — whatever import mode they declare for their own call sites.\n for (const consumerKey of Object.keys(dictionariesToBuildDynamic)) {\n for (const nestedKey of nestedDictionaryGraph.get(consumerKey) ?? []) {\n if (dictionariesToBuildDynamic[nestedKey]) continue;\n\n const nestedResult = mergedDictionaries[nestedKey];\n if (\n !nestedResult ||\n isQualifiedDictionaryGroup(nestedResult.dictionary)\n ) {\n continue;\n }\n\n dictionariesToBuildDynamic[nestedKey] = {\n dictionaryPath: nestedResult.dictionaryPath,\n dictionary: nestedResult.dictionary,\n };\n }\n }\n\n let dynamicDictionaries: LocalizedDictionaryOutput | null = null;\n\n if (Object.keys(dictionariesToBuildDynamic).length > 0) {\n dynamicDictionaries = await writeDynamicDictionary(\n dictionariesToBuildDynamic,\n configuration,\n formats,\n nestedDictionaryGraph\n );\n }\n\n if (Object.keys(qualifiedDictionariesToBuildDynamic).length > 0) {\n await writeDynamicQualifiedDictionaries(\n qualifiedDictionariesToBuildDynamic,\n configuration,\n formats\n );\n }\n\n let fetchDictionaries: LocalizedDictionaryOutput | null = null;\n\n if (dynamicDictionaries && keysToBuildFetch.size > 0) {\n const dictionariesToBuildFetch: LocalizedDictionaryOutput = {};\n\n for (const key of keysToBuildFetch) {\n if (dynamicDictionaries[key]) {\n dictionariesToBuildFetch[key] = dynamicDictionaries[key];\n }\n }\n\n if (Object.keys(dictionariesToBuildFetch).length > 0) {\n fetchDictionaries = await writeFetchDictionary(\n dictionariesToBuildFetch,\n configuration,\n formats,\n nestedDictionaryGraph\n );\n }\n }\n\n return {\n unmergedDictionaries,\n mergedDictionaries,\n dynamicDictionaries,\n fetchDictionaries,\n };\n};\n"],"mappings":";;;;;;;;;;;AA6BA,MAAM,iBAAiB;CACrB,SAAS;CACT,yBAAyB;CACzB,KAAK;AACP;;;;AAKA,MAAa,kBAAkB,OAC7B,0BACA,eACA,YACG;CACH,MAAM,aACJ,eAAe,OAAO,cACtB,eAAe,YAAY,cAC3B;CAEF,MAAM,EAAE,yBAAyB,KAAK,YAAY;EAChD,GAAG;EACH,GAAG;CACL;CAEA,MAAM,+BAA6C,CACjD,GAAG,wBACL;CAEA,IAAI,yBAAyB;EAC3B,MAAM,2BACJ,yBAAyB,cAAc,OAAO,uBAAuB;EAGvE,KAAK,MAAM,qBAAqB,0BAA0B;GACxD,MAAM,kCACJ,yBAAyB,kBAAkB;GAE7C,IAAI,iCAAiC,SAAS,GAAG;IAE/C,MAAM,4BACJ,iCAAiC,QAC9B,uBACC,mBAAmB,YAAY,kBAAkB,OACrD;IAEF,6BAA6B,KAAK,GAAI,6BAA6B,CAAC,CAAE;GACxE;EACF;CACF;CAEA,MAAM,uBAAuB,MAAM,0BACjC,8BACA,eACA,GACF;CAEA,MAAM,qBAAqB,MAAM,wBAC/B,sBACA,aACF;CAMA,MAAM,wBAAwB,yBAC5B,OAAO,OACL,yBACE,cAAc,OAAO,eACvB,CACF,CACF;CAEA,MAAM,wBAAwB,uBAAuB,eAAe,OAAO;CAE3E,MAAM,6BAA0D,CAAC;CACjE,MAAM,sCACJ,CAAC;CACH,MAAM,mCAAmB,IAAI,IAAY;CAEzC,KAAK,MAAM,CAAC,KAAK,iBAAiB,OAAO,QAAQ,kBAAkB,GAAG;EACpE,MAAM,aAAa,aAAa;EAEhC,IAAI,2BAA2B,UAAU,GAAG;GAI1C,MAAM,YAAY,WAAW,cAAc;GAE3C,IAAI,cAAc,WAChB,oCAAoC,OAAO;IACzC,gBAAgB,aAAa;IAC7B;GACF;QACK,IAAI,cAAc,SAEvB,AADkB,aAAa,aACvB,CAAC,CACP,cAAc,YAAY,GAAG,EAAE,kCAAkC,WAAW,eAAe,KAAK,IAAI,EAAE,oFACtG,EAAE,OAAO,OAAO,CAClB;GAGF;EACF;EAEA,MAAM,OAAO,WAAW,cAAc;EAEtC,IAAI,SAAS,aAAa,SAAS,SACjC,2BAA2B,OAAO;GAChC,gBAAgB,aAAa;GAC7B;EACF;EAGF,IAAI,SAAS,SACX,iBAAiB,IAAI,GAAG;CAE5B;CAKA,KAAK,MAAM,eAAe,OAAO,KAAK,0BAA0B,GAC9D,KAAK,MAAM,aAAa,sBAAsB,IAAI,WAAW,KAAK,CAAC,GAAG;EACpE,IAAI,2BAA2B,YAAY;EAE3C,MAAM,eAAe,mBAAmB;EACxC,IACE,CAAC,gBACD,2BAA2B,aAAa,UAAU,GAElD;EAGF,2BAA2B,aAAa;GACtC,gBAAgB,aAAa;GAC7B,YAAY,aAAa;EAC3B;CACF;CAGF,IAAI,sBAAwD;CAE5D,IAAI,OAAO,KAAK,0BAA0B,CAAC,CAAC,SAAS,GACnD,sBAAsB,MAAM,uBAC1B,4BACA,eACA,SACA,qBACF;CAGF,IAAI,OAAO,KAAK,mCAAmC,CAAC,CAAC,SAAS,GAC5D,MAAM,kCACJ,qCACA,eACA,OACF;CAGF,IAAI,oBAAsD;CAE1D,IAAI,uBAAuB,iBAAiB,OAAO,GAAG;EACpD,MAAM,2BAAsD,CAAC;EAE7D,KAAK,MAAM,OAAO,kBAChB,IAAI,oBAAoB,MACtB,yBAAyB,OAAO,oBAAoB;EAIxD,IAAI,OAAO,KAAK,wBAAwB,CAAC,CAAC,SAAS,GACjD,oBAAoB,MAAM,qBACxB,0BACA,eACA,SACA,qBACF;CAEJ;CAEA,OAAO;EACL;EACA;EACA;EACA;CACF;AACF"}
@@ -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
- return format === "esm" ? ` '${safeLocale}': () => import('./${DICTIONARIES_SUBDIR}/${safeKey}/${safeLocale}.json').then(m => m.default)` : ` '${safeLocale}': () => Promise.resolve(require('./${DICTIONARIES_SUBDIR}/${safeKey}/${safeLocale}.json'))`;
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
- const generateDictionaryEntryPoint = (key, locales, relativePrefix, format = "esm") => {
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 localeEntries = locales.sort((a, b) => String(a).localeCompare(String(b))).map((locale) => ` '${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 }`).join(",\n");
13
- if (format === "esm") return `import { editor } from 'intlayer';\nimport dynContent from '${relativePrefix}/${key}.${extension}';\n\nconst content = {\n${localeEntries}\n};\n\nexport default content;\n`;
14
- return `const { editor } = require('intlayer');\nconst dynContent = require('${relativePrefix}/${key}.${extension}');\n\nmodule.exports = {\n${localeEntries}\n};\n`;
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 (locale) =>\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 .join(',\\n');\n\n if (format === 'esm') {\n return (\n `import { editor } from 'intlayer';\\n` +\n `import dynContent from '${relativePrefix}/${key}.${extension}';\\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` +\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): 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 );\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":";;;;;;;;;AAUA,MAAa,gCACX,KACA,SACA,gBACA,SAAwB,UACb;CACX,MAAM,YAAY,WAAW,QAAQ,QAAQ;CAE7C,MAAM,gBAAgB,QACnB,MAAM,GAAG,MAAM,OAAO,CAAC,CAAC,CAAC,cAAc,OAAO,CAAC,CAAC,CAAC,CAAC,CAClD,KACE,WACC,MAAM,OAAO,mGAE0D,IAAI,GAAG,OAAO,gFAGzD,OAAO,kBAGvC,CAAC,CACA,KAAK,KAAK;CAEb,IAAI,WAAW,OACb,OACE,+DAC2B,eAAe,GAAG,IAAI,GAAG,UAAU,2BACxC,cAAc;CAIxC,OACE,wEAC+B,eAAe,GAAG,IAAI,GAAG,UAAU,6BAC3C,cAAc;AAEzC;;;;;;;;;;;;;;;;;AAkBA,MAAa,uBAAuB,OAClC,qBACA,eACA,UAA6B,kBACU;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,MACF;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"}
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,12 +1,54 @@
1
1
  import { computeJaccardSimilarity } from "./computeSimilarity.mjs";
2
2
 
3
3
  //#region src/docReview/alignBlocks.ts
4
+ /** Cost of leaving a block unaligned (an insertion or a deletion). */
5
+ const GAP_PENALTY = -2;
6
+ /**
7
+ * Score of a pair that structural evidence rules out.
8
+ *
9
+ * Strictly below the cost of two gaps (`2 * GAP_PENALTY`) so the aligner always
10
+ * prefers reporting an insertion plus a deletion over such a pair. Every other
11
+ * score is positive, which means that without an explicit veto the aligner would
12
+ * rather pair two unrelated blocks than leave them unaligned — and a bogus pair
13
+ * is planned as `reuse`, which keeps the stale translation verbatim *and*
14
+ * inserts the freshly translated base block next to it, producing the duplicated
15
+ * headings this penalty exists to prevent.
16
+ */
17
+ const STRUCTURAL_MISMATCH_PENALTY = GAP_PENALTY * 2 - 1;
18
+ /** Reward for two blocks opened by a heading of the very same depth. */
19
+ const HEADING_DEPTH_MATCH_BONUS = 3;
20
+ /** Minimum length ratio for the (small) "comparable size" reward. */
21
+ const COMPARABLE_LENGTH_RATIO = .75;
22
+ /**
23
+ * Body length from which a section is considered to carry content of its own.
24
+ *
25
+ * Kept above a single sentence so a translator's short lead-in paragraph is not
26
+ * mistaken for a whole section body.
27
+ */
28
+ const SUBSTANTIAL_BODY_LENGTH = 80;
29
+ /**
30
+ * Length of the body a block carries below its opening heading.
31
+ *
32
+ * A section that only holds its heading (because subsections carry all of its
33
+ * content) is structurally different from one that holds a full body, and that
34
+ * difference survives translation.
35
+ *
36
+ * @param block - The block to measure.
37
+ * @returns The trimmed length of everything below the opening heading line.
38
+ */
39
+ const measureBodyLength = (block) => {
40
+ if (block.headingDepth === null) return block.content.trim().length;
41
+ const [, ...bodyLines] = block.content.split("\n");
42
+ return bodyLines.join("\n").trim().length;
43
+ };
4
44
  /**
5
45
  * Align the blocks of a base document with the blocks of its translation using a
6
- * Needleman–Wunsch global alignment over anchor similarity and block type.
46
+ * Needleman–Wunsch global alignment over heading depth, anchor similarity and
47
+ * block type.
7
48
  *
8
49
  * Because prose differs across languages, the score is weighted toward the
9
- * structural anchor (digits and symbols) rather than the words themselves.
50
+ * structural signals — heading depth first, then the anchor (digits and symbols)
51
+ * — rather than the words themselves.
10
52
  *
11
53
  * @param baseBlocks - Blocks of the base (source) document.
12
54
  * @param targetBlocks - Blocks of the target (translated) document.
@@ -17,26 +59,33 @@ const alignBaseAndTargetBlocks = (baseBlocks, targetBlocks) => {
17
59
  const targetLength = targetBlocks.length;
18
60
  const scoreMatrix = Array.from({ length: baseLength + 1 }, () => Array.from({ length: targetLength + 1 }, () => 0));
19
61
  const traceMatrix = Array.from({ length: baseLength + 1 }, () => Array.from({ length: targetLength + 1 }, () => "diagonal"));
20
- const gapPenalty = -2;
21
62
  const computeMatchScore = (baseIndex, targetIndex) => {
22
63
  const baseBlock = baseBlocks[baseIndex];
23
64
  const targetBlock = targetBlocks[targetIndex];
65
+ const hasComparableHeadingDepths = baseBlock.headingDepth !== null && targetBlock.headingDepth !== null;
66
+ if (hasComparableHeadingDepths && baseBlock.headingDepth !== targetBlock.headingDepth) return STRUCTURAL_MISMATCH_PENALTY;
67
+ const baseBodyLength = measureBodyLength(baseBlock);
68
+ const targetBodyLength = measureBodyLength(targetBlock);
69
+ if (baseBodyLength === 0 && targetBodyLength >= SUBSTANTIAL_BODY_LENGTH || targetBodyLength === 0 && baseBodyLength >= SUBSTANTIAL_BODY_LENGTH) return STRUCTURAL_MISMATCH_PENALTY;
70
+ const lengthRatio = Math.min(baseBlock.content.length, targetBlock.content.length) / Math.max(baseBlock.content.length, targetBlock.content.length);
71
+ const headingDepthBonus = hasComparableHeadingDepths ? HEADING_DEPTH_MATCH_BONUS : 0;
24
72
  const typeBonus = baseBlock.type === targetBlock.type ? 2 : 0;
25
73
  const anchorSimilarity = computeJaccardSimilarity(baseBlock.anchorText, targetBlock.anchorText, 3);
26
- return typeBonus + (Math.min(baseBlock.content.length, targetBlock.content.length) / Math.max(baseBlock.content.length, targetBlock.content.length) > .75 ? 1 : 0) + anchorSimilarity * 8;
74
+ const lengthBonus = lengthRatio > COMPARABLE_LENGTH_RATIO ? 1 : 0;
75
+ return headingDepthBonus + typeBonus + lengthBonus + anchorSimilarity * 8;
27
76
  };
28
77
  for (let i = 1; i <= baseLength; i += 1) {
29
- scoreMatrix[i][0] = scoreMatrix[i - 1][0] + gapPenalty;
78
+ scoreMatrix[i][0] = scoreMatrix[i - 1][0] + GAP_PENALTY;
30
79
  traceMatrix[i][0] = "up";
31
80
  }
32
81
  for (let j = 1; j <= targetLength; j += 1) {
33
- scoreMatrix[0][j] = scoreMatrix[0][j - 1] + gapPenalty;
82
+ scoreMatrix[0][j] = scoreMatrix[0][j - 1] + GAP_PENALTY;
34
83
  traceMatrix[0][j] = "left";
35
84
  }
36
85
  for (let i = 1; i <= baseLength; i += 1) for (let j = 1; j <= targetLength; j += 1) {
37
86
  const match = scoreMatrix[i - 1][j - 1] + computeMatchScore(i - 1, j - 1);
38
- const deleteGap = scoreMatrix[i - 1][j] + gapPenalty;
39
- const insertGap = scoreMatrix[i][j - 1] + gapPenalty;
87
+ const deleteGap = scoreMatrix[i - 1][j] + GAP_PENALTY;
88
+ const insertGap = scoreMatrix[i][j - 1] + GAP_PENALTY;
40
89
  const best = Math.max(match, deleteGap, insertGap);
41
90
  scoreMatrix[i][j] = best;
42
91
  traceMatrix[i][j] = best === match ? "diagonal" : best === deleteGap ? "up" : "left";