@intlayer/engine 9.5.10 → 9.5.11

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 (188) hide show
  1. package/dist/cjs/docReview/alignBlocks.cjs +53 -36
  2. package/dist/cjs/docReview/alignBlocks.cjs.map +1 -1
  3. package/dist/cjs/docReview/rebuildDocument.cjs +3 -1
  4. package/dist/cjs/docReview/rebuildDocument.cjs.map +1 -1
  5. package/dist/cjs/docReview/segmentDocument.cjs +8 -5
  6. package/dist/cjs/docReview/segmentDocument.cjs.map +1 -1
  7. package/dist/cjs/init/frameworkSetup/nextAppRouter/transforms.cjs +4 -1
  8. package/dist/cjs/init/frameworkSetup/nextAppRouter/transforms.cjs.map +1 -1
  9. package/dist/cjs/installMCP/installMCP.cjs +1 -1
  10. package/dist/cjs/installMCP/installMCP.cjs.map +1 -1
  11. package/dist/cjs/loadDictionaries/loadLocalDictionaries.cjs +3 -1
  12. package/dist/cjs/loadDictionaries/loadLocalDictionaries.cjs.map +1 -1
  13. package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs +1 -1
  14. package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs.map +1 -1
  15. package/dist/cjs/loadDictionaries/log.cjs +1 -1
  16. package/dist/cjs/loadDictionaries/log.cjs.map +1 -1
  17. package/dist/cjs/loadDictionaries/logTypeScriptErrors.cjs +19 -12
  18. package/dist/cjs/loadDictionaries/logTypeScriptErrors.cjs.map +1 -1
  19. package/dist/cjs/scan/analyzeBundleContent.cjs +26 -12
  20. package/dist/cjs/scan/analyzeBundleContent.cjs.map +1 -1
  21. package/dist/cjs/scan/calculateScore.cjs +4 -1
  22. package/dist/cjs/scan/calculateScore.cjs.map +1 -1
  23. package/dist/cjs/scan/checks.cjs +394 -221
  24. package/dist/cjs/scan/checks.cjs.map +1 -1
  25. package/dist/cjs/scan/detection/checkDetails.cjs +56 -0
  26. package/dist/cjs/scan/detection/checkDetails.cjs.map +1 -0
  27. package/dist/cjs/scan/detection/classifyInternalLinks.cjs +47 -0
  28. package/dist/cjs/scan/detection/classifyInternalLinks.cjs.map +1 -0
  29. package/dist/cjs/scan/detection/detectRoutingStrategy.cjs +143 -0
  30. package/dist/cjs/scan/detection/detectRoutingStrategy.cjs.map +1 -0
  31. package/dist/cjs/scan/detection/detectTechnologies.cjs +89 -0
  32. package/dist/cjs/scan/detection/detectTechnologies.cjs.map +1 -0
  33. package/dist/cjs/scan/detection/index.cjs +36 -0
  34. package/dist/cjs/scan/detection/localeCode.cjs +118 -0
  35. package/dist/cjs/scan/detection/localeCode.cjs.map +1 -0
  36. package/dist/cjs/scan/detection/localizedPages.cjs +48 -0
  37. package/dist/cjs/scan/detection/localizedPages.cjs.map +1 -0
  38. package/dist/cjs/scan/detection/technologySignatures.cjs +564 -0
  39. package/dist/cjs/scan/detection/technologySignatures.cjs.map +1 -0
  40. package/dist/cjs/scan/detection/url.cjs +53 -0
  41. package/dist/cjs/scan/detection/url.cjs.map +1 -0
  42. package/dist/cjs/scan/fetchText.cjs +56 -0
  43. package/dist/cjs/scan/fetchText.cjs.map +1 -0
  44. package/dist/cjs/scan/index.cjs +59 -3
  45. package/dist/cjs/scan/parseHtml.cjs +55 -10
  46. package/dist/cjs/scan/parseHtml.cjs.map +1 -1
  47. package/dist/cjs/scan/robots.cjs +65 -0
  48. package/dist/cjs/scan/robots.cjs.map +1 -0
  49. package/dist/cjs/scan/runScanChecks.cjs +89 -0
  50. package/dist/cjs/scan/runScanChecks.cjs.map +1 -0
  51. package/dist/cjs/scan/scanWebsite.cjs +58 -52
  52. package/dist/cjs/scan/scanWebsite.cjs.map +1 -1
  53. package/dist/cjs/scan/sitemap.cjs +109 -0
  54. package/dist/cjs/scan/sitemap.cjs.map +1 -0
  55. package/dist/cjs/utils/chunkJSON.cjs +24 -9
  56. package/dist/cjs/utils/chunkJSON.cjs.map +1 -1
  57. package/dist/cjs/utils/getChunk.cjs +2 -2
  58. package/dist/cjs/utils/getChunk.cjs.map +1 -1
  59. package/dist/cjs/utils/runParallel/index.cjs +1 -1
  60. package/dist/cjs/utils/runParallel/index.cjs.map +1 -1
  61. package/dist/cjs/utils/runParallel/pidTree.cjs +23 -11
  62. package/dist/cjs/utils/runParallel/pidTree.cjs.map +1 -1
  63. package/dist/cjs/utils/runParallel/ps.cjs +8 -3
  64. package/dist/cjs/utils/runParallel/ps.cjs.map +1 -1
  65. package/dist/cjs/utils/runParallel/runTask.cjs +2 -1
  66. package/dist/cjs/utils/runParallel/runTask.cjs.map +1 -1
  67. package/dist/cjs/utils/runParallel/wmic.cjs +8 -3
  68. package/dist/cjs/utils/runParallel/wmic.cjs.map +1 -1
  69. package/dist/cjs/writeContentDeclaration/detectExportedComponentName.cjs +7 -4
  70. package/dist/cjs/writeContentDeclaration/detectExportedComponentName.cjs.map +1 -1
  71. package/dist/cjs/writeContentDeclaration/writeMarkdownFile.cjs +2 -2
  72. package/dist/cjs/writeContentDeclaration/writeMarkdownFile.cjs.map +1 -1
  73. package/dist/esm/docReview/alignBlocks.mjs +53 -36
  74. package/dist/esm/docReview/alignBlocks.mjs.map +1 -1
  75. package/dist/esm/docReview/rebuildDocument.mjs +3 -1
  76. package/dist/esm/docReview/rebuildDocument.mjs.map +1 -1
  77. package/dist/esm/docReview/segmentDocument.mjs +8 -5
  78. package/dist/esm/docReview/segmentDocument.mjs.map +1 -1
  79. package/dist/esm/init/frameworkSetup/nextAppRouter/transforms.mjs +4 -1
  80. package/dist/esm/init/frameworkSetup/nextAppRouter/transforms.mjs.map +1 -1
  81. package/dist/esm/installMCP/installMCP.mjs +1 -1
  82. package/dist/esm/installMCP/installMCP.mjs.map +1 -1
  83. package/dist/esm/loadDictionaries/loadLocalDictionaries.mjs +3 -1
  84. package/dist/esm/loadDictionaries/loadLocalDictionaries.mjs.map +1 -1
  85. package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs +1 -1
  86. package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs.map +1 -1
  87. package/dist/esm/loadDictionaries/log.mjs +1 -1
  88. package/dist/esm/loadDictionaries/log.mjs.map +1 -1
  89. package/dist/esm/loadDictionaries/logTypeScriptErrors.mjs +19 -12
  90. package/dist/esm/loadDictionaries/logTypeScriptErrors.mjs.map +1 -1
  91. package/dist/esm/scan/analyzeBundleContent.mjs +26 -12
  92. package/dist/esm/scan/analyzeBundleContent.mjs.map +1 -1
  93. package/dist/esm/scan/calculateScore.mjs +4 -1
  94. package/dist/esm/scan/calculateScore.mjs.map +1 -1
  95. package/dist/esm/scan/checks.mjs +387 -220
  96. package/dist/esm/scan/checks.mjs.map +1 -1
  97. package/dist/esm/scan/detection/checkDetails.mjs +54 -0
  98. package/dist/esm/scan/detection/checkDetails.mjs.map +1 -0
  99. package/dist/esm/scan/detection/classifyInternalLinks.mjs +46 -0
  100. package/dist/esm/scan/detection/classifyInternalLinks.mjs.map +1 -0
  101. package/dist/esm/scan/detection/detectRoutingStrategy.mjs +141 -0
  102. package/dist/esm/scan/detection/detectRoutingStrategy.mjs.map +1 -0
  103. package/dist/esm/scan/detection/detectTechnologies.mjs +87 -0
  104. package/dist/esm/scan/detection/detectTechnologies.mjs.map +1 -0
  105. package/dist/esm/scan/detection/index.mjs +10 -0
  106. package/dist/esm/scan/detection/localeCode.mjs +108 -0
  107. package/dist/esm/scan/detection/localeCode.mjs.map +1 -0
  108. package/dist/esm/scan/detection/localizedPages.mjs +46 -0
  109. package/dist/esm/scan/detection/localizedPages.mjs.map +1 -0
  110. package/dist/esm/scan/detection/technologySignatures.mjs +563 -0
  111. package/dist/esm/scan/detection/technologySignatures.mjs.map +1 -0
  112. package/dist/esm/scan/detection/url.mjs +47 -0
  113. package/dist/esm/scan/detection/url.mjs.map +1 -0
  114. package/dist/esm/scan/fetchText.mjs +55 -0
  115. package/dist/esm/scan/fetchText.mjs.map +1 -0
  116. package/dist/esm/scan/index.mjs +15 -3
  117. package/dist/esm/scan/parseHtml.mjs +52 -11
  118. package/dist/esm/scan/parseHtml.mjs.map +1 -1
  119. package/dist/esm/scan/robots.mjs +63 -0
  120. package/dist/esm/scan/robots.mjs.map +1 -0
  121. package/dist/esm/scan/runScanChecks.mjs +88 -0
  122. package/dist/esm/scan/runScanChecks.mjs.map +1 -0
  123. package/dist/esm/scan/scanWebsite.mjs +59 -53
  124. package/dist/esm/scan/scanWebsite.mjs.map +1 -1
  125. package/dist/esm/scan/sitemap.mjs +105 -0
  126. package/dist/esm/scan/sitemap.mjs.map +1 -0
  127. package/dist/esm/utils/chunkJSON.mjs +24 -9
  128. package/dist/esm/utils/chunkJSON.mjs.map +1 -1
  129. package/dist/esm/utils/getChunk.mjs +2 -2
  130. package/dist/esm/utils/getChunk.mjs.map +1 -1
  131. package/dist/esm/utils/runParallel/index.mjs +1 -1
  132. package/dist/esm/utils/runParallel/index.mjs.map +1 -1
  133. package/dist/esm/utils/runParallel/pidTree.mjs +23 -11
  134. package/dist/esm/utils/runParallel/pidTree.mjs.map +1 -1
  135. package/dist/esm/utils/runParallel/ps.mjs +8 -3
  136. package/dist/esm/utils/runParallel/ps.mjs.map +1 -1
  137. package/dist/esm/utils/runParallel/runTask.mjs +2 -1
  138. package/dist/esm/utils/runParallel/runTask.mjs.map +1 -1
  139. package/dist/esm/utils/runParallel/wmic.mjs +8 -3
  140. package/dist/esm/utils/runParallel/wmic.mjs.map +1 -1
  141. package/dist/esm/writeContentDeclaration/detectExportedComponentName.mjs +7 -4
  142. package/dist/esm/writeContentDeclaration/detectExportedComponentName.mjs.map +1 -1
  143. package/dist/esm/writeContentDeclaration/writeMarkdownFile.mjs +2 -2
  144. package/dist/esm/writeContentDeclaration/writeMarkdownFile.mjs.map +1 -1
  145. package/dist/types/docReview/rebuildDocument.d.ts.map +1 -1
  146. package/dist/types/docReview/segmentDocument.d.ts.map +1 -1
  147. package/dist/types/init/frameworkSetup/nextAppRouter/transforms.d.ts.map +1 -1
  148. package/dist/types/loadDictionaries/logTypeScriptErrors.d.ts.map +1 -1
  149. package/dist/types/scan/analyzeBundleContent.d.ts.map +1 -1
  150. package/dist/types/scan/calculateScore.d.ts +4 -1
  151. package/dist/types/scan/calculateScore.d.ts.map +1 -1
  152. package/dist/types/scan/checks.d.ts +114 -21
  153. package/dist/types/scan/checks.d.ts.map +1 -1
  154. package/dist/types/scan/detection/checkDetails.d.ts +29 -0
  155. package/dist/types/scan/detection/checkDetails.d.ts.map +1 -0
  156. package/dist/types/scan/detection/classifyInternalLinks.d.ts +44 -0
  157. package/dist/types/scan/detection/classifyInternalLinks.d.ts.map +1 -0
  158. package/dist/types/scan/detection/detectRoutingStrategy.d.ts +60 -0
  159. package/dist/types/scan/detection/detectRoutingStrategy.d.ts.map +1 -0
  160. package/dist/types/scan/detection/detectTechnologies.d.ts +60 -0
  161. package/dist/types/scan/detection/detectTechnologies.d.ts.map +1 -0
  162. package/dist/types/scan/detection/index.d.ts +9 -0
  163. package/dist/types/scan/detection/localeCode.d.ts +49 -0
  164. package/dist/types/scan/detection/localeCode.d.ts.map +1 -0
  165. package/dist/types/scan/detection/localizedPages.d.ts +40 -0
  166. package/dist/types/scan/detection/localizedPages.d.ts.map +1 -0
  167. package/dist/types/scan/detection/technologySignatures.d.ts +48 -0
  168. package/dist/types/scan/detection/technologySignatures.d.ts.map +1 -0
  169. package/dist/types/scan/detection/url.d.ts +27 -0
  170. package/dist/types/scan/detection/url.d.ts.map +1 -0
  171. package/dist/types/scan/fetchText.d.ts +30 -0
  172. package/dist/types/scan/fetchText.d.ts.map +1 -0
  173. package/dist/types/scan/index.d.ts +16 -4
  174. package/dist/types/scan/parseHtml.d.ts +17 -0
  175. package/dist/types/scan/parseHtml.d.ts.map +1 -1
  176. package/dist/types/scan/robots.d.ts +23 -0
  177. package/dist/types/scan/robots.d.ts.map +1 -0
  178. package/dist/types/scan/runScanChecks.d.ts +43 -0
  179. package/dist/types/scan/runScanChecks.d.ts.map +1 -0
  180. package/dist/types/scan/scanWebsite.d.ts.map +1 -1
  181. package/dist/types/scan/sitemap.d.ts +49 -0
  182. package/dist/types/scan/sitemap.d.ts.map +1 -0
  183. package/dist/types/scan/types.d.ts +15 -0
  184. package/dist/types/scan/types.d.ts.map +1 -1
  185. package/dist/types/utils/chunkJSON.d.ts.map +1 -1
  186. package/dist/types/utils/runParallel/pidTree.d.ts.map +1 -1
  187. package/dist/types/writeContentDeclaration/writeMarkdownFile.d.ts.map +1 -1
  188. package/package.json +15 -7
@@ -63,6 +63,7 @@ const alignBaseAndTargetBlocks = (baseBlocks, targetBlocks) => {
63
63
  const computeMatchScore = (baseIndex, targetIndex) => {
64
64
  const baseBlock = baseBlocks[baseIndex];
65
65
  const targetBlock = targetBlocks[targetIndex];
66
+ if (!baseBlock || !targetBlock) return STRUCTURAL_MISMATCH_PENALTY;
66
67
  const hasComparableHeadingDepths = baseBlock.headingDepth !== null && targetBlock.headingDepth !== null;
67
68
  if (hasComparableHeadingDepths && baseBlock.headingDepth !== targetBlock.headingDepth) return STRUCTURAL_MISMATCH_PENALTY;
68
69
  const baseBodyLength = measureBodyLength(baseBlock);
@@ -76,49 +77,65 @@ const alignBaseAndTargetBlocks = (baseBlocks, targetBlocks) => {
76
77
  return headingDepthBonus + typeBonus + lengthBonus + anchorSimilarity * 8;
77
78
  };
78
79
  for (let i = 1; i <= baseLength; i += 1) {
79
- scoreMatrix[i][0] = scoreMatrix[i - 1][0] + GAP_PENALTY;
80
- traceMatrix[i][0] = "up";
80
+ const row = scoreMatrix[i];
81
+ const prevRow = scoreMatrix[i - 1];
82
+ const traceRow = traceMatrix[i];
83
+ if (row && prevRow) row[0] = (prevRow[0] ?? 0) + GAP_PENALTY;
84
+ if (traceRow) traceRow[0] = "up";
81
85
  }
86
+ const firstRow = scoreMatrix[0];
87
+ const firstTraceRow = traceMatrix[0];
82
88
  for (let j = 1; j <= targetLength; j += 1) {
83
- scoreMatrix[0][j] = scoreMatrix[0][j - 1] + GAP_PENALTY;
84
- traceMatrix[0][j] = "left";
89
+ if (firstRow) firstRow[j] = (firstRow[j - 1] ?? 0) + GAP_PENALTY;
90
+ if (firstTraceRow) firstTraceRow[j] = "left";
85
91
  }
86
- for (let i = 1; i <= baseLength; i += 1) for (let j = 1; j <= targetLength; j += 1) {
87
- const match = scoreMatrix[i - 1][j - 1] + computeMatchScore(i - 1, j - 1);
88
- const deleteGap = scoreMatrix[i - 1][j] + GAP_PENALTY;
89
- const insertGap = scoreMatrix[i][j - 1] + GAP_PENALTY;
90
- const best = Math.max(match, deleteGap, insertGap);
91
- scoreMatrix[i][j] = best;
92
- traceMatrix[i][j] = best === match ? "diagonal" : best === deleteGap ? "up" : "left";
92
+ for (let i = 1; i <= baseLength; i += 1) {
93
+ const currentRow = scoreMatrix[i];
94
+ const prevRow = scoreMatrix[i - 1];
95
+ const currentTraceRow = traceMatrix[i];
96
+ if (!currentRow || !prevRow || !currentTraceRow) continue;
97
+ for (let j = 1; j <= targetLength; j += 1) {
98
+ const match = (prevRow[j - 1] ?? 0) + computeMatchScore(i - 1, j - 1);
99
+ const deleteGap = (prevRow[j] ?? 0) + GAP_PENALTY;
100
+ const insertGap = (currentRow[j - 1] ?? 0) + GAP_PENALTY;
101
+ const best = Math.max(match, deleteGap, insertGap);
102
+ currentRow[j] = best;
103
+ currentTraceRow[j] = best === match ? "diagonal" : best === deleteGap ? "up" : "left";
104
+ }
93
105
  }
94
106
  const result = [];
95
107
  let i = baseLength;
96
108
  let j = targetLength;
97
- while (i > 0 || j > 0) if (i > 0 && j > 0 && traceMatrix[i][j] === "diagonal") {
98
- const baseIndex = i - 1;
99
- const targetIndex = j - 1;
100
- const similarityScore = require_docReview_computeSimilarity.computeJaccardSimilarity(baseBlocks[baseIndex].anchorText, targetBlocks[targetIndex].anchorText, 3);
101
- result.unshift({
102
- baseIndex,
103
- targetIndex,
104
- similarityScore
105
- });
106
- i -= 1;
107
- j -= 1;
108
- } else if (i > 0 && (j === 0 || traceMatrix[i][j] === "up")) {
109
- result.unshift({
110
- baseIndex: i - 1,
111
- targetIndex: null,
112
- similarityScore: 0
113
- });
114
- i -= 1;
115
- } else if (j > 0 && (i === 0 || traceMatrix[i][j] === "left")) {
116
- result.unshift({
117
- baseIndex: -1,
118
- targetIndex: j - 1,
119
- similarityScore: 0
120
- });
121
- j -= 1;
109
+ while (i > 0 || j > 0) {
110
+ const direction = traceMatrix[i]?.[j];
111
+ if (i > 0 && j > 0 && direction === "diagonal") {
112
+ const baseIndex = i - 1;
113
+ const targetIndex = j - 1;
114
+ const baseAnchor = baseBlocks[baseIndex]?.anchorText ?? "";
115
+ const targetAnchor = targetBlocks[targetIndex]?.anchorText ?? "";
116
+ const similarityScore = require_docReview_computeSimilarity.computeJaccardSimilarity(baseAnchor, targetAnchor, 3);
117
+ result.unshift({
118
+ baseIndex,
119
+ targetIndex,
120
+ similarityScore
121
+ });
122
+ i -= 1;
123
+ j -= 1;
124
+ } else if (i > 0 && (j === 0 || direction === "up")) {
125
+ result.unshift({
126
+ baseIndex: i - 1,
127
+ targetIndex: null,
128
+ similarityScore: 0
129
+ });
130
+ i -= 1;
131
+ } else if (j > 0 && (i === 0 || direction === "left")) {
132
+ result.unshift({
133
+ baseIndex: -1,
134
+ targetIndex: j - 1,
135
+ similarityScore: 0
136
+ });
137
+ j -= 1;
138
+ }
122
139
  }
123
140
  return result;
124
141
  };
@@ -1 +1 @@
1
- {"version":3,"file":"alignBlocks.cjs","names":["computeJaccardSimilarity"],"sources":["../../../src/docReview/alignBlocks.ts"],"sourcesContent":["import { computeJaccardSimilarity } from './computeSimilarity';\nimport type { AlignmentPair, FingerprintedBlock } from './types';\n\n/** Cost of leaving a block unaligned (an insertion or a deletion). */\nconst GAP_PENALTY = -2;\n\n/**\n * Score of a pair that structural evidence rules out.\n *\n * Strictly below the cost of two gaps (`2 * GAP_PENALTY`) so the aligner always\n * prefers reporting an insertion plus a deletion over such a pair. Every other\n * score is positive, which means that without an explicit veto the aligner would\n * rather pair two unrelated blocks than leave them unaligned — and a bogus pair\n * is planned as `reuse`, which keeps the stale translation verbatim *and*\n * inserts the freshly translated base block next to it, producing the duplicated\n * headings this penalty exists to prevent.\n */\nconst STRUCTURAL_MISMATCH_PENALTY = GAP_PENALTY * 2 - 1;\n\n/** Reward for two blocks opened by a heading of the very same depth. */\nconst HEADING_DEPTH_MATCH_BONUS = 3;\n\n/** Minimum length ratio for the (small) \"comparable size\" reward. */\nconst COMPARABLE_LENGTH_RATIO = 0.75;\n\n/**\n * Body length from which a section is considered to carry content of its own.\n *\n * Kept above a single sentence so a translator's short lead-in paragraph is not\n * mistaken for a whole section body.\n */\nconst SUBSTANTIAL_BODY_LENGTH = 80;\n\n/**\n * Length of the body a block carries below its opening heading.\n *\n * A section that only holds its heading (because subsections carry all of its\n * content) is structurally different from one that holds a full body, and that\n * difference survives translation.\n *\n * @param block - The block to measure.\n * @returns The trimmed length of everything below the opening heading line.\n */\nconst measureBodyLength = (block: FingerprintedBlock): number => {\n if (block.headingDepth === null) return block.content.trim().length;\n\n const [, ...bodyLines] = block.content.split('\\n');\n\n return bodyLines.join('\\n').trim().length;\n};\n\n/**\n * Align the blocks of a base document with the blocks of its translation using a\n * Needleman–Wunsch global alignment over heading depth, anchor similarity and\n * block type.\n *\n * Because prose differs across languages, the score is weighted toward the\n * structural signals — heading depth first, then the anchor (digits and symbols)\n * — rather than the words themselves.\n *\n * @param baseBlocks - Blocks of the base (source) document.\n * @param targetBlocks - Blocks of the target (translated) document.\n * @returns The ordered list of alignment pairs, including insertions and deletions.\n */\nexport const alignBaseAndTargetBlocks = (\n baseBlocks: FingerprintedBlock[],\n targetBlocks: FingerprintedBlock[]\n): AlignmentPair[] => {\n const baseLength = baseBlocks.length;\n const targetLength = targetBlocks.length;\n\n const scoreMatrix: number[][] = Array.from({ length: baseLength + 1 }, () =>\n Array.from({ length: targetLength + 1 }, () => 0)\n );\n const traceMatrix: ('diagonal' | 'up' | 'left')[][] = Array.from(\n { length: baseLength + 1 },\n () => Array.from({ length: targetLength + 1 }, () => 'diagonal')\n );\n\n const computeMatchScore = (\n baseIndex: number,\n targetIndex: number\n ): number => {\n const baseBlock = baseBlocks[baseIndex]!;\n const targetBlock = targetBlocks[targetIndex]!;\n\n // Translating a document never changes its heading depths, so two headings\n // of different depths cannot be counterparts, however similar their content.\n const hasComparableHeadingDepths =\n baseBlock.headingDepth !== null && targetBlock.headingDepth !== null;\n\n if (\n hasComparableHeadingDepths &&\n baseBlock.headingDepth !== targetBlock.headingDepth\n ) {\n return STRUCTURAL_MISMATCH_PENALTY;\n }\n\n // A section reduced to its bare heading (its content living in subsections)\n // is not the translation of a section holding a full body. Pairing them\n // reuses that whole body while the base heading is translated again right\n // next to it — which is exactly how a duplicated heading appears.\n const baseBodyLength = measureBodyLength(baseBlock);\n const targetBodyLength = measureBodyLength(targetBlock);\n const isBodyPresenceMismatched =\n (baseBodyLength === 0 && targetBodyLength >= SUBSTANTIAL_BODY_LENGTH) ||\n (targetBodyLength === 0 && baseBodyLength >= SUBSTANTIAL_BODY_LENGTH);\n\n if (isBodyPresenceMismatched) return STRUCTURAL_MISMATCH_PENALTY;\n\n const lengthRatio =\n Math.min(baseBlock.content.length, targetBlock.content.length) /\n Math.max(baseBlock.content.length, targetBlock.content.length);\n\n const headingDepthBonus = hasComparableHeadingDepths\n ? HEADING_DEPTH_MATCH_BONUS\n : 0;\n const typeBonus = baseBlock.type === targetBlock.type ? 2 : 0;\n const anchorSimilarity = computeJaccardSimilarity(\n baseBlock.anchorText,\n targetBlock.anchorText,\n 3\n );\n const lengthBonus = lengthRatio > COMPARABLE_LENGTH_RATIO ? 1 : 0;\n\n // weighted toward the structural signals (heading depth, then anchor)\n return headingDepthBonus + typeBonus + lengthBonus + anchorSimilarity * 8;\n };\n\n // initialize first row and column\n for (let i = 1; i <= baseLength; i += 1) {\n scoreMatrix[i][0] = scoreMatrix[i - 1][0] + GAP_PENALTY;\n traceMatrix[i][0] = 'up';\n }\n for (let j = 1; j <= targetLength; j += 1) {\n scoreMatrix[0][j] = scoreMatrix[0][j - 1] + GAP_PENALTY;\n traceMatrix[0][j] = 'left';\n }\n\n // fill\n for (let i = 1; i <= baseLength; i += 1) {\n for (let j = 1; j <= targetLength; j += 1) {\n const match = scoreMatrix[i - 1][j - 1] + computeMatchScore(i - 1, j - 1);\n const deleteGap = scoreMatrix[i - 1][j] + GAP_PENALTY;\n const insertGap = scoreMatrix[i][j - 1] + GAP_PENALTY;\n\n const best = Math.max(match, deleteGap, insertGap);\n scoreMatrix[i][j] = best;\n traceMatrix[i][j] =\n best === match ? 'diagonal' : best === deleteGap ? 'up' : 'left';\n }\n }\n\n // traceback\n const result: AlignmentPair[] = [];\n let i = baseLength;\n let j = targetLength;\n while (i > 0 || j > 0) {\n if (i > 0 && j > 0 && traceMatrix[i][j] === 'diagonal') {\n const baseIndex = i - 1;\n const targetIndex = j - 1;\n const similarityScore = computeJaccardSimilarity(\n baseBlocks[baseIndex].anchorText,\n targetBlocks[targetIndex].anchorText,\n 3\n );\n result.unshift({ baseIndex, targetIndex, similarityScore });\n i -= 1;\n j -= 1;\n } else if (i > 0 && (j === 0 || traceMatrix[i][j] === 'up')) {\n result.unshift({\n baseIndex: i - 1,\n targetIndex: null,\n similarityScore: 0,\n });\n i -= 1;\n } else if (j > 0 && (i === 0 || traceMatrix[i][j] === 'left')) {\n // target block has no corresponding base block (deleted)\n result.unshift({\n baseIndex: -1,\n targetIndex: j - 1,\n similarityScore: 0,\n });\n j -= 1;\n }\n }\n return result;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"alignBlocks.cjs","names":["computeJaccardSimilarity"],"sources":["../../../src/docReview/alignBlocks.ts"],"sourcesContent":["import { computeJaccardSimilarity } from './computeSimilarity';\nimport type { AlignmentPair, FingerprintedBlock } from './types';\n\n/** Cost of leaving a block unaligned (an insertion or a deletion). */\nconst GAP_PENALTY = -2;\n\n/**\n * Score of a pair that structural evidence rules out.\n *\n * Strictly below the cost of two gaps (`2 * GAP_PENALTY`) so the aligner always\n * prefers reporting an insertion plus a deletion over such a pair. Every other\n * score is positive, which means that without an explicit veto the aligner would\n * rather pair two unrelated blocks than leave them unaligned — and a bogus pair\n * is planned as `reuse`, which keeps the stale translation verbatim *and*\n * inserts the freshly translated base block next to it, producing the duplicated\n * headings this penalty exists to prevent.\n */\nconst STRUCTURAL_MISMATCH_PENALTY = GAP_PENALTY * 2 - 1;\n\n/** Reward for two blocks opened by a heading of the very same depth. */\nconst HEADING_DEPTH_MATCH_BONUS = 3;\n\n/** Minimum length ratio for the (small) \"comparable size\" reward. */\nconst COMPARABLE_LENGTH_RATIO = 0.75;\n\n/**\n * Body length from which a section is considered to carry content of its own.\n *\n * Kept above a single sentence so a translator's short lead-in paragraph is not\n * mistaken for a whole section body.\n */\nconst SUBSTANTIAL_BODY_LENGTH = 80;\n\n/**\n * Length of the body a block carries below its opening heading.\n *\n * A section that only holds its heading (because subsections carry all of its\n * content) is structurally different from one that holds a full body, and that\n * difference survives translation.\n *\n * @param block - The block to measure.\n * @returns The trimmed length of everything below the opening heading line.\n */\nconst measureBodyLength = (block: FingerprintedBlock): number => {\n if (block.headingDepth === null) return block.content.trim().length;\n\n const [, ...bodyLines] = block.content.split('\\n');\n\n return bodyLines.join('\\n').trim().length;\n};\n\n/**\n * Align the blocks of a base document with the blocks of its translation using a\n * Needleman–Wunsch global alignment over heading depth, anchor similarity and\n * block type.\n *\n * Because prose differs across languages, the score is weighted toward the\n * structural signals — heading depth first, then the anchor (digits and symbols)\n * — rather than the words themselves.\n *\n * @param baseBlocks - Blocks of the base (source) document.\n * @param targetBlocks - Blocks of the target (translated) document.\n * @returns The ordered list of alignment pairs, including insertions and deletions.\n */\nexport const alignBaseAndTargetBlocks = (\n baseBlocks: FingerprintedBlock[],\n targetBlocks: FingerprintedBlock[]\n): AlignmentPair[] => {\n const baseLength = baseBlocks.length;\n const targetLength = targetBlocks.length;\n\n const scoreMatrix: number[][] = Array.from({ length: baseLength + 1 }, () =>\n Array.from({ length: targetLength + 1 }, () => 0)\n );\n const traceMatrix: ('diagonal' | 'up' | 'left')[][] = Array.from(\n { length: baseLength + 1 },\n () => Array.from({ length: targetLength + 1 }, () => 'diagonal')\n );\n\n const computeMatchScore = (\n baseIndex: number,\n targetIndex: number\n ): number => {\n const baseBlock = baseBlocks[baseIndex];\n const targetBlock = targetBlocks[targetIndex];\n\n if (!baseBlock || !targetBlock) return STRUCTURAL_MISMATCH_PENALTY;\n\n // Translating a document never changes its heading depths, so two headings\n // of different depths cannot be counterparts, however similar their content.\n const hasComparableHeadingDepths =\n baseBlock.headingDepth !== null && targetBlock.headingDepth !== null;\n\n if (\n hasComparableHeadingDepths &&\n baseBlock.headingDepth !== targetBlock.headingDepth\n ) {\n return STRUCTURAL_MISMATCH_PENALTY;\n }\n\n // A section reduced to its bare heading (its content living in subsections)\n // is not the translation of a section holding a full body. Pairing them\n // reuses that whole body while the base heading is translated again right\n // next to it — which is exactly how a duplicated heading appears.\n const baseBodyLength = measureBodyLength(baseBlock);\n const targetBodyLength = measureBodyLength(targetBlock);\n const isBodyPresenceMismatched =\n (baseBodyLength === 0 && targetBodyLength >= SUBSTANTIAL_BODY_LENGTH) ||\n (targetBodyLength === 0 && baseBodyLength >= SUBSTANTIAL_BODY_LENGTH);\n\n if (isBodyPresenceMismatched) return STRUCTURAL_MISMATCH_PENALTY;\n\n const lengthRatio =\n Math.min(baseBlock.content.length, targetBlock.content.length) /\n Math.max(baseBlock.content.length, targetBlock.content.length);\n\n const headingDepthBonus = hasComparableHeadingDepths\n ? HEADING_DEPTH_MATCH_BONUS\n : 0;\n const typeBonus = baseBlock.type === targetBlock.type ? 2 : 0;\n const anchorSimilarity = computeJaccardSimilarity(\n baseBlock.anchorText,\n targetBlock.anchorText,\n 3\n );\n const lengthBonus = lengthRatio > COMPARABLE_LENGTH_RATIO ? 1 : 0;\n\n // weighted toward the structural signals (heading depth, then anchor)\n return headingDepthBonus + typeBonus + lengthBonus + anchorSimilarity * 8;\n };\n\n // initialize first row and column\n for (let i = 1; i <= baseLength; i += 1) {\n const row = scoreMatrix[i];\n const prevRow = scoreMatrix[i - 1];\n const traceRow = traceMatrix[i];\n\n if (row && prevRow) {\n row[0] = (prevRow[0] ?? 0) + GAP_PENALTY;\n }\n if (traceRow) {\n traceRow[0] = 'up';\n }\n }\n const firstRow = scoreMatrix[0];\n const firstTraceRow = traceMatrix[0];\n\n for (let j = 1; j <= targetLength; j += 1) {\n if (firstRow) {\n firstRow[j] = (firstRow[j - 1] ?? 0) + GAP_PENALTY;\n }\n if (firstTraceRow) {\n firstTraceRow[j] = 'left';\n }\n }\n\n // fill\n for (let i = 1; i <= baseLength; i += 1) {\n const currentRow = scoreMatrix[i];\n const prevRow = scoreMatrix[i - 1];\n const currentTraceRow = traceMatrix[i];\n\n if (!currentRow || !prevRow || !currentTraceRow) continue;\n\n for (let j = 1; j <= targetLength; j += 1) {\n const match = (prevRow[j - 1] ?? 0) + computeMatchScore(i - 1, j - 1);\n const deleteGap = (prevRow[j] ?? 0) + GAP_PENALTY;\n const insertGap = (currentRow[j - 1] ?? 0) + GAP_PENALTY;\n\n const best = Math.max(match, deleteGap, insertGap);\n currentRow[j] = best;\n currentTraceRow[j] =\n best === match ? 'diagonal' : best === deleteGap ? 'up' : 'left';\n }\n }\n\n // traceback\n const result: AlignmentPair[] = [];\n let i = baseLength;\n let j = targetLength;\n while (i > 0 || j > 0) {\n const direction = traceMatrix[i]?.[j];\n if (i > 0 && j > 0 && direction === 'diagonal') {\n const baseIndex = i - 1;\n const targetIndex = j - 1;\n const baseAnchor = baseBlocks[baseIndex]?.anchorText ?? '';\n const targetAnchor = targetBlocks[targetIndex]?.anchorText ?? '';\n const similarityScore = computeJaccardSimilarity(\n baseAnchor,\n targetAnchor,\n 3\n );\n result.unshift({ baseIndex, targetIndex, similarityScore });\n i -= 1;\n j -= 1;\n } else if (i > 0 && (j === 0 || direction === 'up')) {\n result.unshift({\n baseIndex: i - 1,\n targetIndex: null,\n similarityScore: 0,\n });\n i -= 1;\n } else if (j > 0 && (i === 0 || direction === 'left')) {\n // target block has no corresponding base block (deleted)\n result.unshift({\n baseIndex: -1,\n targetIndex: j - 1,\n similarityScore: 0,\n });\n j -= 1;\n }\n }\n return result;\n};\n"],"mappings":"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"}
@@ -13,7 +13,8 @@ const identifySegmentsToReview = ({ baseBlocks, targetBlocks, plan }) => {
13
13
  plan.actions.forEach((action, actionIndex) => {
14
14
  if (action.kind === "review") {
15
15
  const baseBlock = baseBlocks[action.baseIndex];
16
- const targetBlockText = action.targetIndex !== null ? targetBlocks[action.targetIndex].content : null;
16
+ if (!baseBlock) return;
17
+ const targetBlockText = action.targetIndex !== null ? targetBlocks[action.targetIndex]?.content ?? null : null;
17
18
  segmentsToReview.push({
18
19
  baseBlock,
19
20
  targetBlockText,
@@ -21,6 +22,7 @@ const identifySegmentsToReview = ({ baseBlocks, targetBlocks, plan }) => {
21
22
  });
22
23
  } else if (action.kind === "insert_new") {
23
24
  const baseBlock = baseBlocks[action.baseIndex];
25
+ if (!baseBlock) return;
24
26
  segmentsToReview.push({
25
27
  baseBlock,
26
28
  targetBlockText: null,
@@ -1 +1 @@
1
- {"version":3,"file":"rebuildDocument.cjs","names":[],"sources":["../../../src/docReview/rebuildDocument.ts"],"sourcesContent":["import type { AlignmentPlan, FingerprintedBlock } from './types';\n\n/**\n * A block that needs to be translated or re-translated by an external consumer\n * (an AI client, a human, or an agent).\n */\nexport type SegmentToReview = {\n /** The base block to translate. */\n baseBlock: FingerprintedBlock;\n /** Existing target translation, or `null` when the block is new. */\n targetBlockText: string | null;\n /** Index of the originating action within {@link AlignmentPlan.actions}. */\n actionIndex: number;\n};\n\nexport type RebuildInput = {\n baseBlocks: FingerprintedBlock[];\n targetBlocks: FingerprintedBlock[];\n plan: AlignmentPlan;\n};\n\nexport type RebuildResult = {\n segmentsToReview: SegmentToReview[];\n};\n\n/**\n * Analyze the alignment plan and return only the segments that need\n * review/translation. Does not generate output text - that is done by\n * {@link mergeReviewedSegments} once the translations are available.\n *\n * @param input - The base/target blocks and the alignment plan.\n * @returns The list of segments that require translation.\n */\nexport const identifySegmentsToReview = ({\n baseBlocks,\n targetBlocks,\n plan,\n}: RebuildInput): RebuildResult => {\n const segmentsToReview: SegmentToReview[] = [];\n\n plan.actions.forEach((action, actionIndex) => {\n if (action.kind === 'review') {\n const baseBlock = baseBlocks[action.baseIndex];\n const targetBlockText =\n action.targetIndex !== null\n ? targetBlocks[action.targetIndex].content\n : null;\n\n segmentsToReview.push({ baseBlock, targetBlockText, actionIndex });\n } else if (action.kind === 'insert_new') {\n const baseBlock = baseBlocks[action.baseIndex];\n\n segmentsToReview.push({\n baseBlock,\n targetBlockText: null,\n actionIndex,\n });\n }\n });\n\n return { segmentsToReview };\n};\n\n/** Markdown separates two blocks with a blank line. */\nconst endsWithBlockBoundary = (text: string): boolean =>\n /\\n[ \\t]*\\n$/.test(text);\n\n/**\n * The newlines missing at the end of `text` for it to close a markdown block.\n *\n * @param text - The document built so far.\n * @returns `''`, `'\\n'` or `'\\n\\n'` depending on how the text already ends.\n */\nconst buildBlockSeparator = (text: string): string => {\n if (text.length === 0 || endsWithBlockBoundary(text)) return '';\n\n return text.endsWith('\\n') ? '\\n' : '\\n\\n';\n};\n\n/**\n * Merge reviewed translations back into the final document following the\n * alignment plan, reusing untouched target blocks as-is.\n *\n * @param plan - The alignment plan.\n * @param targetBlocks - Blocks of the existing target document.\n * @param reviewedSegments - Map of action index to its reviewed translation.\n * @returns The rebuilt target document.\n */\nexport const mergeReviewedSegments = (\n plan: AlignmentPlan,\n targetBlocks: FingerprintedBlock[],\n reviewedSegments: Map<number, string>\n): string => {\n let mergedText = '';\n let previousPartWasGenerated = false;\n\n /**\n * Append one part, keeping a blank line around the content this run produced.\n *\n * Blocks own the blank lines that trail them, so two verbatim blocks already\n * separate themselves and are appended untouched — a document with nothing to\n * change merges back byte for byte. Generated content is the exception: the\n * block it lands after may be the one that used to end the document, which\n * carries no trailing blank line, and the two would be glued into a single\n * markdown block (a heading swallowed by the paragraph above it).\n */\n const appendPart = (content: string, isGenerated: boolean): void => {\n if (content.length === 0) return;\n\n if (isGenerated || previousPartWasGenerated) {\n mergedText += buildBlockSeparator(mergedText);\n }\n\n mergedText += content;\n previousPartWasGenerated = isGenerated;\n };\n\n plan.actions.forEach((action, actionIndex) => {\n if (action.kind === 'reuse') {\n appendPart(targetBlocks[action.targetIndex]!.content, false);\n } else if (action.kind === 'review' || action.kind === 'insert_new') {\n const reviewedContent = reviewedSegments.get(actionIndex);\n\n if (reviewedContent !== undefined) {\n appendPart(reviewedContent, true);\n } else {\n // Fallback: if review failed, use existing or blank\n if (action.kind === 'review' && action.targetIndex !== null) {\n appendPart(targetBlocks[action.targetIndex]!.content, false);\n } else {\n appendPart('\\n', false);\n }\n }\n } else if (action.kind === 'delete') {\n const reviewedContent = reviewedSegments.get(actionIndex);\n if (reviewedContent !== undefined) {\n // Caller explicitly resolved this block: empty string = actually delete,\n // non-empty string = replacement content.\n appendPart(reviewedContent, true);\n } else {\n // Default: keep verbatim. A target block with no base counterpart may\n // just be a section the aligner could not follow (reordering, split\n // prose) — keeping it prevents accidental data loss in log/read-only mode.\n appendPart(targetBlocks[action.targetIndex]!.content, false);\n }\n }\n });\n\n return mergedText;\n};\n"],"mappings":";;;;;;;;;;AAiCA,MAAa,4BAA4B,EACvC,YACA,cACA,WACiC;CACjC,MAAM,mBAAsC,CAAC;CAE7C,KAAK,QAAQ,SAAS,QAAQ,gBAAgB;EAC5C,IAAI,OAAO,SAAS,UAAU;GAC5B,MAAM,YAAY,WAAW,OAAO;GACpC,MAAM,kBACJ,OAAO,gBAAgB,OACnB,aAAa,OAAO,YAAY,CAAC,UACjC;GAEN,iBAAiB,KAAK;IAAE;IAAW;IAAiB;GAAY,CAAC;EACnE,OAAO,IAAI,OAAO,SAAS,cAAc;GACvC,MAAM,YAAY,WAAW,OAAO;GAEpC,iBAAiB,KAAK;IACpB;IACA,iBAAiB;IACjB;GACF,CAAC;EACH;CACF,CAAC;CAED,OAAO,EAAE,iBAAiB;AAC5B;;AAGA,MAAM,yBAAyB,SAC7B,cAAc,KAAK,IAAI;;;;;;;AAQzB,MAAM,uBAAuB,SAAyB;CACpD,IAAI,KAAK,WAAW,KAAK,sBAAsB,IAAI,GAAG,OAAO;CAE7D,OAAO,KAAK,SAAS,IAAI,IAAI,OAAO;AACtC;;;;;;;;;;AAWA,MAAa,yBACX,MACA,cACA,qBACW;CACX,IAAI,aAAa;CACjB,IAAI,2BAA2B;;;;;;;;;;;CAY/B,MAAM,cAAc,SAAiB,gBAA+B;EAClE,IAAI,QAAQ,WAAW,GAAG;EAE1B,IAAI,eAAe,0BACjB,cAAc,oBAAoB,UAAU;EAG9C,cAAc;EACd,2BAA2B;CAC7B;CAEA,KAAK,QAAQ,SAAS,QAAQ,gBAAgB;EAC5C,IAAI,OAAO,SAAS,SAClB,WAAW,aAAa,OAAO,YAAY,CAAE,SAAS,KAAK;OACtD,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,cAAc;GACnE,MAAM,kBAAkB,iBAAiB,IAAI,WAAW;GAExD,IAAI,oBAAoB,QACtB,WAAW,iBAAiB,IAAI;QAGhC,IAAI,OAAO,SAAS,YAAY,OAAO,gBAAgB,MACrD,WAAW,aAAa,OAAO,YAAY,CAAE,SAAS,KAAK;QAE3D,WAAW,MAAM,KAAK;EAG5B,OAAO,IAAI,OAAO,SAAS,UAAU;GACnC,MAAM,kBAAkB,iBAAiB,IAAI,WAAW;GACxD,IAAI,oBAAoB,QAGtB,WAAW,iBAAiB,IAAI;QAKhC,WAAW,aAAa,OAAO,YAAY,CAAE,SAAS,KAAK;EAE/D;CACF,CAAC;CAED,OAAO;AACT"}
1
+ {"version":3,"file":"rebuildDocument.cjs","names":[],"sources":["../../../src/docReview/rebuildDocument.ts"],"sourcesContent":["import type { AlignmentPlan, FingerprintedBlock } from './types';\n\n/**\n * A block that needs to be translated or re-translated by an external consumer\n * (an AI client, a human, or an agent).\n */\nexport type SegmentToReview = {\n /** The base block to translate. */\n baseBlock: FingerprintedBlock;\n /** Existing target translation, or `null` when the block is new. */\n targetBlockText: string | null;\n /** Index of the originating action within {@link AlignmentPlan.actions}. */\n actionIndex: number;\n};\n\nexport type RebuildInput = {\n baseBlocks: FingerprintedBlock[];\n targetBlocks: FingerprintedBlock[];\n plan: AlignmentPlan;\n};\n\nexport type RebuildResult = {\n segmentsToReview: SegmentToReview[];\n};\n\n/**\n * Analyze the alignment plan and return only the segments that need\n * review/translation. Does not generate output text - that is done by\n * {@link mergeReviewedSegments} once the translations are available.\n *\n * @param input - The base/target blocks and the alignment plan.\n * @returns The list of segments that require translation.\n */\nexport const identifySegmentsToReview = ({\n baseBlocks,\n targetBlocks,\n plan,\n}: RebuildInput): RebuildResult => {\n const segmentsToReview: SegmentToReview[] = [];\n\n plan.actions.forEach((action, actionIndex) => {\n if (action.kind === 'review') {\n const baseBlock = baseBlocks[action.baseIndex];\n\n if (!baseBlock) return;\n\n const targetBlockText =\n action.targetIndex !== null\n ? (targetBlocks[action.targetIndex]?.content ?? null)\n : null;\n\n segmentsToReview.push({ baseBlock, targetBlockText, actionIndex });\n } else if (action.kind === 'insert_new') {\n const baseBlock = baseBlocks[action.baseIndex];\n\n if (!baseBlock) return;\n\n segmentsToReview.push({\n baseBlock,\n targetBlockText: null,\n actionIndex,\n });\n }\n });\n\n return { segmentsToReview };\n};\n\n/** Markdown separates two blocks with a blank line. */\nconst endsWithBlockBoundary = (text: string): boolean =>\n /\\n[ \\t]*\\n$/.test(text);\n\n/**\n * The newlines missing at the end of `text` for it to close a markdown block.\n *\n * @param text - The document built so far.\n * @returns `''`, `'\\n'` or `'\\n\\n'` depending on how the text already ends.\n */\nconst buildBlockSeparator = (text: string): string => {\n if (text.length === 0 || endsWithBlockBoundary(text)) return '';\n\n return text.endsWith('\\n') ? '\\n' : '\\n\\n';\n};\n\n/**\n * Merge reviewed translations back into the final document following the\n * alignment plan, reusing untouched target blocks as-is.\n *\n * @param plan - The alignment plan.\n * @param targetBlocks - Blocks of the existing target document.\n * @param reviewedSegments - Map of action index to its reviewed translation.\n * @returns The rebuilt target document.\n */\nexport const mergeReviewedSegments = (\n plan: AlignmentPlan,\n targetBlocks: FingerprintedBlock[],\n reviewedSegments: Map<number, string>\n): string => {\n let mergedText = '';\n let previousPartWasGenerated = false;\n\n /**\n * Append one part, keeping a blank line around the content this run produced.\n *\n * Blocks own the blank lines that trail them, so two verbatim blocks already\n * separate themselves and are appended untouched — a document with nothing to\n * change merges back byte for byte. Generated content is the exception: the\n * block it lands after may be the one that used to end the document, which\n * carries no trailing blank line, and the two would be glued into a single\n * markdown block (a heading swallowed by the paragraph above it).\n */\n const appendPart = (content: string, isGenerated: boolean): void => {\n if (content.length === 0) return;\n\n if (isGenerated || previousPartWasGenerated) {\n mergedText += buildBlockSeparator(mergedText);\n }\n\n mergedText += content;\n previousPartWasGenerated = isGenerated;\n };\n\n plan.actions.forEach((action, actionIndex) => {\n if (action.kind === 'reuse') {\n appendPart(targetBlocks[action.targetIndex]!.content, false);\n } else if (action.kind === 'review' || action.kind === 'insert_new') {\n const reviewedContent = reviewedSegments.get(actionIndex);\n\n if (reviewedContent !== undefined) {\n appendPart(reviewedContent, true);\n } else {\n // Fallback: if review failed, use existing or blank\n if (action.kind === 'review' && action.targetIndex !== null) {\n appendPart(targetBlocks[action.targetIndex]!.content, false);\n } else {\n appendPart('\\n', false);\n }\n }\n } else if (action.kind === 'delete') {\n const reviewedContent = reviewedSegments.get(actionIndex);\n if (reviewedContent !== undefined) {\n // Caller explicitly resolved this block: empty string = actually delete,\n // non-empty string = replacement content.\n appendPart(reviewedContent, true);\n } else {\n // Default: keep verbatim. A target block with no base counterpart may\n // just be a section the aligner could not follow (reordering, split\n // prose) — keeping it prevents accidental data loss in log/read-only mode.\n appendPart(targetBlocks[action.targetIndex]!.content, false);\n }\n }\n });\n\n return mergedText;\n};\n"],"mappings":";;;;;;;;;;AAiCA,MAAa,4BAA4B,EACvC,YACA,cACA,WACiC;CACjC,MAAM,mBAAsC,CAAC;CAE7C,KAAK,QAAQ,SAAS,QAAQ,gBAAgB;EAC5C,IAAI,OAAO,SAAS,UAAU;GAC5B,MAAM,YAAY,WAAW,OAAO;GAEpC,IAAI,CAAC,WAAW;GAEhB,MAAM,kBACJ,OAAO,gBAAgB,OAClB,aAAa,OAAO,YAAY,EAAE,WAAW,OAC9C;GAEN,iBAAiB,KAAK;IAAE;IAAW;IAAiB;GAAY,CAAC;EACnE,OAAO,IAAI,OAAO,SAAS,cAAc;GACvC,MAAM,YAAY,WAAW,OAAO;GAEpC,IAAI,CAAC,WAAW;GAEhB,iBAAiB,KAAK;IACpB;IACA,iBAAiB;IACjB;GACF,CAAC;EACH;CACF,CAAC;CAED,OAAO,EAAE,iBAAiB;AAC5B;;AAGA,MAAM,yBAAyB,SAC7B,cAAc,KAAK,IAAI;;;;;;;AAQzB,MAAM,uBAAuB,SAAyB;CACpD,IAAI,KAAK,WAAW,KAAK,sBAAsB,IAAI,GAAG,OAAO;CAE7D,OAAO,KAAK,SAAS,IAAI,IAAI,OAAO;AACtC;;;;;;;;;;AAWA,MAAa,yBACX,MACA,cACA,qBACW;CACX,IAAI,aAAa;CACjB,IAAI,2BAA2B;;;;;;;;;;;CAY/B,MAAM,cAAc,SAAiB,gBAA+B;EAClE,IAAI,QAAQ,WAAW,GAAG;EAE1B,IAAI,eAAe,0BACjB,cAAc,oBAAoB,UAAU;EAG9C,cAAc;EACd,2BAA2B;CAC7B;CAEA,KAAK,QAAQ,SAAS,QAAQ,gBAAgB;EAC5C,IAAI,OAAO,SAAS,SAClB,WAAW,aAAa,OAAO,YAAY,CAAE,SAAS,KAAK;OACtD,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,cAAc;GACnE,MAAM,kBAAkB,iBAAiB,IAAI,WAAW;GAExD,IAAI,oBAAoB,QACtB,WAAW,iBAAiB,IAAI;QAGhC,IAAI,OAAO,SAAS,YAAY,OAAO,gBAAgB,MACrD,WAAW,aAAa,OAAO,YAAY,CAAE,SAAS,KAAK;QAE3D,WAAW,MAAM,KAAK;EAG5B,OAAO,IAAI,OAAO,SAAS,UAAU;GACnC,MAAM,kBAAkB,iBAAiB,IAAI,WAAW;GACxD,IAAI,oBAAoB,QAGtB,WAAW,iBAAiB,IAAI;QAKhC,WAAW,aAAa,OAAO,YAAY,CAAE,SAAS,KAAK;EAE/D;CACF,CAAC;CAED,OAAO;AACT"}
@@ -1,8 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  //#region src/docReview/segmentDocument.ts
3
3
  const HEADING_PATTERN = /^\s*(#{1,6})\s+/;
4
- const isBlankLine = (line) => line.trim().length === 0;
5
- const isFencedCodeDelimiter = (line) => /^\s*```/.test(line);
4
+ const isBlankLine = (line) => !line || line.trim().length === 0;
5
+ const isFencedCodeDelimiter = (line) => Boolean(line && /^\s*```/.test(line));
6
6
  /**
7
7
  * Read the depth of an ATX markdown heading (`#` → 1, `######` → 6).
8
8
  *
@@ -10,10 +10,11 @@ const isFencedCodeDelimiter = (line) => /^\s*```/.test(line);
10
10
  * @returns The heading depth, or `null` when the line is not a heading.
11
11
  */
12
12
  const parseHeadingDepth = (line) => {
13
+ if (!line) return null;
13
14
  return HEADING_PATTERN.exec(line)?.[1]?.length ?? null;
14
15
  };
15
16
  const isHeading = (line) => parseHeadingDepth(line) !== null;
16
- const isFrontmatterDelimiter = (line) => /^\s*---\s*$/.test(line);
17
+ const isFrontmatterDelimiter = (line) => Boolean(line && /^\s*---\s*$/.test(line));
17
18
  /**
18
19
  * Split a markdown document into fine-grained blocks.
19
20
  *
@@ -88,7 +89,8 @@ const segmentDocument = (text) => {
88
89
  if (units.length === 0) return [];
89
90
  return units.map((unit, unitIndex) => {
90
91
  const blockStartIndex = unitIndex === 0 ? 0 : unit.startIndex;
91
- const blockEndIndex = unitIndex === units.length - 1 ? lineCount - 1 : units[unitIndex + 1].startIndex - 1;
92
+ const nextUnit = units[unitIndex + 1];
93
+ const blockEndIndex = unitIndex === units.length - 1 || !nextUnit ? lineCount - 1 : nextUnit.startIndex - 1;
92
94
  const blockLines = lines.slice(blockStartIndex, blockEndIndex + 1);
93
95
  const content = blockEndIndex < lineCount - 1 ? `${blockLines.join("\n")}\n` : blockLines.join("\n");
94
96
  return {
@@ -124,8 +126,9 @@ const segmentSections = (text) => {
124
126
  let currentBlocks = [];
125
127
  const flushSection = () => {
126
128
  if (currentBlocks.length === 0) return;
127
- const [firstBlock] = currentBlocks;
129
+ const firstBlock = currentBlocks[0];
128
130
  const lastBlock = currentBlocks[currentBlocks.length - 1];
131
+ if (!firstBlock || !lastBlock) return;
129
132
  sections.push({
130
133
  type: firstBlock.type,
131
134
  content: currentBlocks.map((block) => block.content).join(""),
@@ -1 +1 @@
1
- {"version":3,"file":"segmentDocument.cjs","names":[],"sources":["../../../src/docReview/segmentDocument.ts"],"sourcesContent":["import type { Block, BlockType } from './types';\n\nconst HEADING_PATTERN = /^\\s*(#{1,6})\\s+/;\n\nconst isBlankLine = (line: string): boolean => line.trim().length === 0;\nconst isFencedCodeDelimiter = (line: string): boolean => /^\\s*```/.test(line);\n\n/**\n * Read the depth of an ATX markdown heading (`#` → 1, `######` → 6).\n *\n * @param line - The line to inspect.\n * @returns The heading depth, or `null` when the line is not a heading.\n */\nconst parseHeadingDepth = (line: string): number | null => {\n const match = HEADING_PATTERN.exec(line);\n\n return match?.[1]?.length ?? null;\n};\n\nconst isHeading = (line: string): boolean => parseHeadingDepth(line) !== null;\nconst isFrontmatterDelimiter = (line: string): boolean =>\n /^\\s*---\\s*$/.test(line);\n\n/**\n * A content unit (heading, paragraph, code block or frontmatter) spanning a\n * 0-based, inclusive line range. Blank-line runs are not units of their own;\n * they are folded into the preceding unit as trailing separators (see\n * {@link segmentDocument}) so that the concatenation of every block's content\n * reproduces the document byte-for-byte.\n */\ntype ContentUnit = {\n type: BlockType;\n /** Depth of the ATX heading opening the unit, `null` when it is not a heading. */\n headingDepth: number | null;\n startIndex: number;\n endIndex: number;\n};\n\n/**\n * Split a markdown document into fine-grained blocks.\n *\n * Boundaries are drawn at headings, blank lines (paragraph breaks) and fenced\n * code blocks, while frontmatter and the inside of code fences are kept intact.\n * Each blank-line run is appended to the block that precedes it, so the blocks\n * form an exact partition of the document: concatenating every `content` (in\n * order) yields the original text unchanged. This is what lets the block-aware\n * review re-translate only the paragraphs/snippets that actually changed instead\n * of the whole heading section.\n *\n * @param text - The full markdown document.\n * @returns The ordered list of blocks with their 1-based line ranges.\n */\nexport const segmentDocument = (text: string): Block[] => {\n const lines = text.split('\\n');\n const lineCount = lines.length;\n\n // 1. Tokenize into content units, skipping blank-line runs (folded in below).\n const units: ContentUnit[] = [];\n let index = 0;\n\n while (index < lineCount) {\n const currentLine = lines[index];\n\n if (isBlankLine(currentLine)) {\n index += 1;\n continue;\n }\n\n // Frontmatter: only when it opens the document.\n if (units.length === 0 && isFrontmatterDelimiter(currentLine)) {\n const startIndex = index;\n index += 1;\n while (index < lineCount && !isFrontmatterDelimiter(lines[index])) {\n index += 1;\n }\n // Include the closing delimiter when present.\n if (index < lineCount) index += 1;\n units.push({\n type: 'unknown',\n headingDepth: null,\n startIndex,\n endIndex: index - 1,\n });\n continue;\n }\n\n // Fenced code block: consumed whole so inner blank lines and `#` lines are\n // never treated as boundaries.\n if (isFencedCodeDelimiter(currentLine)) {\n const startIndex = index;\n index += 1;\n while (index < lineCount && !isFencedCodeDelimiter(lines[index])) {\n index += 1;\n }\n // Include the closing fence when present.\n if (index < lineCount) index += 1;\n units.push({\n type: 'code_block',\n headingDepth: null,\n startIndex,\n endIndex: index - 1,\n });\n continue;\n }\n\n // Heading: a single self-contained line.\n const headingDepth = parseHeadingDepth(currentLine);\n\n if (headingDepth !== null) {\n units.push({\n type: 'heading',\n headingDepth,\n startIndex: index,\n endIndex: index,\n });\n index += 1;\n continue;\n }\n\n // Paragraph: a run of consecutive lines until a blank line, a heading or a\n // code fence. Tables and tight lists stay together (no blank line between\n // their rows/items).\n const startIndex = index;\n while (\n index < lineCount &&\n !isBlankLine(lines[index]) &&\n !isHeading(lines[index]) &&\n !isFencedCodeDelimiter(lines[index])\n ) {\n index += 1;\n }\n units.push({\n type: 'paragraph',\n headingDepth: null,\n startIndex,\n endIndex: index - 1,\n });\n }\n\n if (units.length === 0) return [];\n\n // 2. Turn each unit into a block whose line range extends to just before the\n // next unit, so the trailing blank-line run is owned by it. The first block\n // also absorbs any leading blank lines, and the last block runs to EOF.\n return units.map((unit, unitIndex): Block => {\n const blockStartIndex = unitIndex === 0 ? 0 : unit.startIndex;\n const blockEndIndex =\n unitIndex === units.length - 1\n ? lineCount - 1\n : units[unitIndex + 1].startIndex - 1;\n\n const blockLines = lines.slice(blockStartIndex, blockEndIndex + 1);\n // Re-append the boundary newline dropped by `split` for every block but the\n // one ending at EOF, so concatenating all blocks rebuilds the document.\n const content =\n blockEndIndex < lineCount - 1\n ? `${blockLines.join('\\n')}\\n`\n : blockLines.join('\\n');\n\n return {\n type: unit.type,\n content,\n headingDepth: unit.headingDepth,\n lineStart: blockStartIndex + 1,\n lineEnd: blockEndIndex + 1,\n };\n });\n};\n\n/**\n * Split a markdown document into coarse, heading-anchored sections.\n *\n * Built by grouping the fine blocks of {@link segmentDocument}: frontmatter and\n * each heading open a new section, and the following non-heading blocks are\n * folded into it. Because it only concatenates adjacent fine blocks, the result\n * is still an exact partition of the document (sections concatenate back to the\n * source unchanged).\n *\n * Sections are the robust alignment unit between a base document and its\n * translation — both share the same heading structure, so they align almost\n * perfectly and a translation that splits its prose into a different number of\n * paragraphs never causes a section to be dropped. Fine-grained review happens\n * within a section once it is known to have changed.\n *\n * @param text - The full markdown document.\n * @returns The ordered list of sections with their 1-based line ranges.\n */\nexport const segmentSections = (text: string): Block[] => {\n const fineBlocks = segmentDocument(text);\n const sections: Block[] = [];\n let currentBlocks: Block[] = [];\n\n const flushSection = (): void => {\n if (currentBlocks.length === 0) return;\n\n const [firstBlock] = currentBlocks;\n const lastBlock = currentBlocks[currentBlocks.length - 1];\n\n sections.push({\n type: firstBlock.type,\n content: currentBlocks.map((block) => block.content).join(''),\n // A section is identified by the heading that opens it, so it inherits its\n // depth — this is what keeps a `##` section from aligning with a `###` one.\n headingDepth: firstBlock.headingDepth,\n lineStart: firstBlock.lineStart,\n lineEnd: lastBlock.lineEnd,\n });\n currentBlocks = [];\n };\n\n for (const block of fineBlocks) {\n // Frontmatter (a leading `unknown` block) and every heading open a section.\n const opensSection =\n block.type === 'heading' ||\n (block.type === 'unknown' && sections.length === 0);\n\n if (opensSection) flushSection();\n currentBlocks.push(block);\n if (block.type === 'unknown' && sections.length === 0) flushSection();\n }\n\n flushSection();\n\n return sections;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"segmentDocument.cjs","names":[],"sources":["../../../src/docReview/segmentDocument.ts"],"sourcesContent":["import type { Block, BlockType } from './types';\n\nconst HEADING_PATTERN = /^\\s*(#{1,6})\\s+/;\n\nconst isBlankLine = (line?: string): boolean =>\n !line || line.trim().length === 0;\nconst isFencedCodeDelimiter = (line?: string): boolean =>\n Boolean(line && /^\\s*```/.test(line));\n\n/**\n * Read the depth of an ATX markdown heading (`#` → 1, `######` → 6).\n *\n * @param line - The line to inspect.\n * @returns The heading depth, or `null` when the line is not a heading.\n */\nconst parseHeadingDepth = (line?: string): number | null => {\n if (!line) return null;\n const match = HEADING_PATTERN.exec(line);\n\n return match?.[1]?.length ?? null;\n};\n\nconst isHeading = (line?: string): boolean => parseHeadingDepth(line) !== null;\nconst isFrontmatterDelimiter = (line?: string): boolean =>\n Boolean(line && /^\\s*---\\s*$/.test(line));\n\n/**\n * A content unit (heading, paragraph, code block or frontmatter) spanning a\n * 0-based, inclusive line range. Blank-line runs are not units of their own;\n * they are folded into the preceding unit as trailing separators (see\n * {@link segmentDocument}) so that the concatenation of every block's content\n * reproduces the document byte-for-byte.\n */\ntype ContentUnit = {\n type: BlockType;\n /** Depth of the ATX heading opening the unit, `null` when it is not a heading. */\n headingDepth: number | null;\n startIndex: number;\n endIndex: number;\n};\n\n/**\n * Split a markdown document into fine-grained blocks.\n *\n * Boundaries are drawn at headings, blank lines (paragraph breaks) and fenced\n * code blocks, while frontmatter and the inside of code fences are kept intact.\n * Each blank-line run is appended to the block that precedes it, so the blocks\n * form an exact partition of the document: concatenating every `content` (in\n * order) yields the original text unchanged. This is what lets the block-aware\n * review re-translate only the paragraphs/snippets that actually changed instead\n * of the whole heading section.\n *\n * @param text - The full markdown document.\n * @returns The ordered list of blocks with their 1-based line ranges.\n */\nexport const segmentDocument = (text: string): Block[] => {\n const lines = text.split('\\n');\n const lineCount = lines.length;\n\n // 1. Tokenize into content units, skipping blank-line runs (folded in below).\n const units: ContentUnit[] = [];\n let index = 0;\n\n while (index < lineCount) {\n const currentLine = lines[index];\n\n if (isBlankLine(currentLine)) {\n index += 1;\n continue;\n }\n\n // Frontmatter: only when it opens the document.\n if (units.length === 0 && isFrontmatterDelimiter(currentLine)) {\n const startIndex = index;\n index += 1;\n while (index < lineCount && !isFrontmatterDelimiter(lines[index])) {\n index += 1;\n }\n // Include the closing delimiter when present.\n if (index < lineCount) index += 1;\n units.push({\n type: 'unknown',\n headingDepth: null,\n startIndex,\n endIndex: index - 1,\n });\n continue;\n }\n\n // Fenced code block: consumed whole so inner blank lines and `#` lines are\n // never treated as boundaries.\n if (isFencedCodeDelimiter(currentLine)) {\n const startIndex = index;\n index += 1;\n while (index < lineCount && !isFencedCodeDelimiter(lines[index])) {\n index += 1;\n }\n // Include the closing fence when present.\n if (index < lineCount) index += 1;\n units.push({\n type: 'code_block',\n headingDepth: null,\n startIndex,\n endIndex: index - 1,\n });\n continue;\n }\n\n // Heading: a single self-contained line.\n const headingDepth = parseHeadingDepth(currentLine);\n\n if (headingDepth !== null) {\n units.push({\n type: 'heading',\n headingDepth,\n startIndex: index,\n endIndex: index,\n });\n index += 1;\n continue;\n }\n\n // Paragraph: a run of consecutive lines until a blank line, a heading or a\n // code fence. Tables and tight lists stay together (no blank line between\n // their rows/items).\n const startIndex = index;\n while (\n index < lineCount &&\n !isBlankLine(lines[index]) &&\n !isHeading(lines[index]) &&\n !isFencedCodeDelimiter(lines[index])\n ) {\n index += 1;\n }\n units.push({\n type: 'paragraph',\n headingDepth: null,\n startIndex,\n endIndex: index - 1,\n });\n }\n\n if (units.length === 0) return [];\n\n // 2. Turn each unit into a block whose line range extends to just before the\n // next unit, so the trailing blank-line run is owned by it. The first block\n // also absorbs any leading blank lines, and the last block runs to EOF.\n return units.map((unit, unitIndex): Block => {\n const blockStartIndex = unitIndex === 0 ? 0 : unit.startIndex;\n const nextUnit = units[unitIndex + 1];\n const blockEndIndex =\n unitIndex === units.length - 1 || !nextUnit\n ? lineCount - 1\n : nextUnit.startIndex - 1;\n\n const blockLines = lines.slice(blockStartIndex, blockEndIndex + 1);\n // Re-append the boundary newline dropped by `split` for every block but the\n // one ending at EOF, so concatenating all blocks rebuilds the document.\n const content =\n blockEndIndex < lineCount - 1\n ? `${blockLines.join('\\n')}\\n`\n : blockLines.join('\\n');\n\n return {\n type: unit.type,\n content,\n headingDepth: unit.headingDepth,\n lineStart: blockStartIndex + 1,\n lineEnd: blockEndIndex + 1,\n };\n });\n};\n\n/**\n * Split a markdown document into coarse, heading-anchored sections.\n *\n * Built by grouping the fine blocks of {@link segmentDocument}: frontmatter and\n * each heading open a new section, and the following non-heading blocks are\n * folded into it. Because it only concatenates adjacent fine blocks, the result\n * is still an exact partition of the document (sections concatenate back to the\n * source unchanged).\n *\n * Sections are the robust alignment unit between a base document and its\n * translation — both share the same heading structure, so they align almost\n * perfectly and a translation that splits its prose into a different number of\n * paragraphs never causes a section to be dropped. Fine-grained review happens\n * within a section once it is known to have changed.\n *\n * @param text - The full markdown document.\n * @returns The ordered list of sections with their 1-based line ranges.\n */\nexport const segmentSections = (text: string): Block[] => {\n const fineBlocks = segmentDocument(text);\n const sections: Block[] = [];\n let currentBlocks: Block[] = [];\n\n const flushSection = (): void => {\n if (currentBlocks.length === 0) return;\n\n const firstBlock = currentBlocks[0];\n const lastBlock = currentBlocks[currentBlocks.length - 1];\n\n if (!firstBlock || !lastBlock) return;\n\n sections.push({\n type: firstBlock.type,\n content: currentBlocks.map((block) => block.content).join(''),\n // A section is identified by the heading that opens it, so it inherits its\n // depth — this is what keeps a `##` section from aligning with a `###` one.\n headingDepth: firstBlock.headingDepth,\n lineStart: firstBlock.lineStart,\n lineEnd: lastBlock.lineEnd,\n });\n currentBlocks = [];\n };\n\n for (const block of fineBlocks) {\n // Frontmatter (a leading `unknown` block) and every heading open a section.\n const opensSection =\n block.type === 'heading' ||\n (block.type === 'unknown' && sections.length === 0);\n\n if (opensSection) flushSection();\n currentBlocks.push(block);\n if (block.type === 'unknown' && sections.length === 0) flushSection();\n }\n\n flushSection();\n\n return sections;\n};\n"],"mappings":"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"}
@@ -74,7 +74,10 @@ const setHtmlLang = (ast) => {
74
74
  const langAttr = node.attributes?.find((attr) => attr.type === "JSXAttribute" && attr.name?.name === "lang");
75
75
  const localeExpression = b.jsxExpressionContainer(b.identifier("locale"));
76
76
  if (langAttr) langAttr.value = localeExpression;
77
- else node.attributes.push(b.jsxAttribute(b.jsxIdentifier("lang"), localeExpression));
77
+ else {
78
+ node.attributes = node.attributes ?? [];
79
+ node.attributes.push(b.jsxAttribute(b.jsxIdentifier("lang"), localeExpression));
80
+ }
78
81
  return false;
79
82
  }
80
83
  this.traverse(path);
@@ -1 +1 @@
1
- {"version":3,"file":"transforms.cjs","names":["babelTsParser","firstInsertIndex"],"sources":["../../../../../src/init/frameworkSetup/nextAppRouter/transforms.ts"],"sourcesContent":["import * as recast from 'recast';\nimport { babelTsParser } from '../../../utils/babelParser';\nimport { ensureNamedImport, firstInsertIndex } from '../../utils/astImports';\n\nconst { builders: b } = recast.types;\n\n/** babel-ts parser handles TypeScript *and* JSX (the `typescript` parser does not). */\nconst parseTsx = (code: string): any =>\n recast.parse(code, { parser: babelTsParser });\n\n/** Result of a source transform. `code` is unchanged for any non-`wrapped` status. */\nexport type TransformResult = {\n code: string;\n status: 'wrapped' | 'already' | 'skipped-client' | 'skipped';\n};\n\n/** Detects a top-level `'use client'` directive (client components can't be async server providers). */\nconst isClientComponent = (ast: any): boolean => {\n const directives = ast.program.directives ?? [];\n if (\n directives.some((directive: any) => directive.value?.value === 'use client')\n ) {\n return true;\n }\n return ast.program.body.some(\n (stmt: any) =>\n stmt.type === 'ExpressionStatement' &&\n stmt.expression?.type === 'StringLiteral' &&\n stmt.expression.value === 'use client'\n );\n};\n\n/** Finds the function node behind `export default`, following an identifier reference if needed. */\nconst findDefaultExportFunction = (ast: any): any => {\n const body = ast.program.body;\n\n const asFunction = (node: any): any =>\n node &&\n (node.type === 'ArrowFunctionExpression' ||\n node.type === 'FunctionExpression' ||\n node.type === 'FunctionDeclaration')\n ? node\n : null;\n\n for (const stmt of body) {\n if (stmt.type !== 'ExportDefaultDeclaration') continue;\n\n const direct = asFunction(stmt.declaration);\n if (direct) return direct;\n\n if (stmt.declaration?.type === 'Identifier') {\n const name = stmt.declaration.name;\n for (const candidate of body) {\n if (candidate.type === 'VariableDeclaration') {\n for (const declarator of candidate.declarations) {\n if (\n declarator.id?.type === 'Identifier' &&\n declarator.id.name === name\n ) {\n const fn = asFunction(declarator.init);\n if (fn) return fn;\n }\n }\n }\n if (\n candidate.type === 'FunctionDeclaration' &&\n candidate.id?.name === name\n ) {\n return candidate;\n }\n }\n }\n }\n\n return null;\n};\n\n/** Ensures `export { exportedName } from source`, skipping when already declared/re-exported. */\nconst ensureExportFrom = (\n ast: any,\n exportedName: string,\n source: string\n): void => {\n const body = ast.program.body;\n\n const alreadyPresent = body.some((stmt: any) => {\n if (\n stmt.type === 'ExportNamedDeclaration' &&\n stmt.source?.value === source\n ) {\n return stmt.specifiers.some(\n (spec: any) => spec.exported?.name === exportedName\n );\n }\n // Locally declared `export const/function generateStaticParams`\n if (stmt.type === 'ExportNamedDeclaration' && stmt.declaration) {\n const decl = stmt.declaration;\n if (decl.type === 'FunctionDeclaration' && decl.id?.name === exportedName)\n return true;\n if (decl.type === 'VariableDeclaration') {\n return decl.declarations.some(\n (d: any) => d.id?.type === 'Identifier' && d.id.name === exportedName\n );\n }\n }\n return false;\n });\n\n if (alreadyPresent) return;\n\n const exportNode = parseTsx(`export { ${exportedName} } from \"${source}\";`)\n .program.body[0];\n ast.program.body.splice(firstInsertIndex(ast), 0, exportNode);\n};\n\n/** Makes the function async and inserts `const locale = await getLocale();` once, at the top of its body. */\nconst ensureAwaitedLocale = (funcNode: any): void => {\n funcNode.async = true;\n\n if (funcNode.body.type !== 'BlockStatement') {\n funcNode.body = b.blockStatement([b.returnStatement(funcNode.body)]);\n }\n\n const hasLocale = funcNode.body.body.some(\n (stmt: any) =>\n stmt.type === 'VariableDeclaration' &&\n stmt.declarations.some(\n (d: any) =>\n // const locale = ...\n (d.id?.type === 'Identifier' && d.id.name === 'locale') ||\n // const { locale } = ... or const { locale: locale } = ...\n (d.id?.type === 'ObjectPattern' &&\n d.id.properties?.some(\n (prop: any) =>\n prop.value?.type === 'Identifier' &&\n prop.value?.name === 'locale'\n ))\n )\n );\n\n if (!hasLocale) {\n const localeStatement = parseTsx('const locale = await getLocale();')\n .program.body[0];\n funcNode.body.body.unshift(localeStatement);\n }\n};\n\n/** Builds `<providerName locale={locale}>{child}</providerName>` around an existing JSX child node. */\nconst buildProviderElement = (providerName: string, childNode: any): any => {\n const template = parseTsx(\n `const __wrap = <${providerName} locale={locale}>{__child__}</${providerName}>;`\n );\n const providerElement = template.program.body[0].declarations[0].init;\n providerElement.children = [childNode];\n return providerElement;\n};\n\n/** Sets `lang={locale}` on the first `<html>` element, if present. */\nconst setHtmlLang = (ast: any): void => {\n recast.visit(ast, {\n visitJSXOpeningElement(path) {\n const node = path.node;\n if (node.name?.type === 'JSXIdentifier' && node.name.name === 'html') {\n const langAttr = node.attributes?.find(\n (attr: any) =>\n attr.type === 'JSXAttribute' && attr.name?.name === 'lang'\n ) as any;\n const localeExpression = b.jsxExpressionContainer(\n b.identifier('locale')\n );\n if (langAttr) {\n langAttr.value = localeExpression;\n } else {\n node.attributes.push(\n b.jsxAttribute(b.jsxIdentifier('lang'), localeExpression)\n );\n }\n return false;\n }\n this.traverse(path);\n },\n });\n};\n\n/** Options of {@link wrapLayoutWithProvider}. */\nexport type WrapLayoutOptions = {\n /**\n * Re-export `generateStaticParams` from `next-intlayer`. Only meaningful for a\n * layout under a `[locale]` segment; a root layout of an unprefixed app has\n * no locale param to pre-render. Default: `true`.\n */\n exportStaticParams?: boolean;\n};\n\n/**\n * Wraps the `{children}` of a Next.js App Router **layout** with the unified\n * `IntlayerProvider`, deriving the locale via `getLocale()`. Safe and\n * idempotent: bails (returns the original code) for client components, when no\n * `{children}` placeholder is found, or when there is no default export.\n *\n * `IntlayerProvider` seeds both the server context and the client provider in\n * one mount, so this is the only provider a Next.js App Router app needs —\n * pages below the locale layout read locale/variant from it without wrapping\n * themselves individually (see the removed `wrapPageWithProvider`).\n */\nexport const wrapLayoutWithProvider = (\n code: string,\n { exportStaticParams = true }: WrapLayoutOptions = {}\n): TransformResult => {\n const ast = parseTsx(code);\n\n if (isClientComponent(ast)) return { code, status: 'skipped-client' };\n if (code.includes('IntlayerProvider')) return { code, status: 'already' };\n\n const funcNode = findDefaultExportFunction(ast);\n if (!funcNode) return { code, status: 'skipped' };\n\n let wrapped = false;\n recast.visit(funcNode, {\n visitJSXExpressionContainer(path) {\n if (wrapped) return false;\n const expression = path.node.expression;\n if (expression?.type === 'Identifier' && expression.name === 'children') {\n path.replace(buildProviderElement('IntlayerProvider', path.node));\n wrapped = true;\n return false;\n }\n this.traverse(path);\n },\n });\n\n if (!wrapped) return { code, status: 'skipped' };\n\n ensureNamedImport(ast, 'IntlayerProvider', 'next-intlayer/server');\n ensureNamedImport(ast, 'getLocale', 'next-intlayer/server');\n if (exportStaticParams) {\n ensureExportFrom(ast, 'generateStaticParams', 'next-intlayer');\n }\n ensureAwaitedLocale(funcNode);\n setHtmlLang(ast);\n\n return { code: recast.print(ast).code, status: 'wrapped' };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"transforms.cjs","names":["babelTsParser","firstInsertIndex"],"sources":["../../../../../src/init/frameworkSetup/nextAppRouter/transforms.ts"],"sourcesContent":["import * as recast from 'recast';\nimport { babelTsParser } from '../../../utils/babelParser';\nimport { ensureNamedImport, firstInsertIndex } from '../../utils/astImports';\n\nconst { builders: b } = recast.types;\n\n/** babel-ts parser handles TypeScript *and* JSX (the `typescript` parser does not). */\nconst parseTsx = (code: string): any =>\n recast.parse(code, { parser: babelTsParser });\n\n/** Result of a source transform. `code` is unchanged for any non-`wrapped` status. */\nexport type TransformResult = {\n code: string;\n status: 'wrapped' | 'already' | 'skipped-client' | 'skipped';\n};\n\n/** Detects a top-level `'use client'` directive (client components can't be async server providers). */\nconst isClientComponent = (ast: any): boolean => {\n const directives = ast.program.directives ?? [];\n if (\n directives.some((directive: any) => directive.value?.value === 'use client')\n ) {\n return true;\n }\n return ast.program.body.some(\n (stmt: any) =>\n stmt.type === 'ExpressionStatement' &&\n stmt.expression?.type === 'StringLiteral' &&\n stmt.expression.value === 'use client'\n );\n};\n\n/** Finds the function node behind `export default`, following an identifier reference if needed. */\nconst findDefaultExportFunction = (ast: any): any => {\n const body = ast.program.body;\n\n const asFunction = (node: any): any =>\n node &&\n (node.type === 'ArrowFunctionExpression' ||\n node.type === 'FunctionExpression' ||\n node.type === 'FunctionDeclaration')\n ? node\n : null;\n\n for (const stmt of body) {\n if (stmt.type !== 'ExportDefaultDeclaration') continue;\n\n const direct = asFunction(stmt.declaration);\n if (direct) return direct;\n\n if (stmt.declaration?.type === 'Identifier') {\n const name = stmt.declaration.name;\n for (const candidate of body) {\n if (candidate.type === 'VariableDeclaration') {\n for (const declarator of candidate.declarations) {\n if (\n declarator.id?.type === 'Identifier' &&\n declarator.id.name === name\n ) {\n const fn = asFunction(declarator.init);\n if (fn) return fn;\n }\n }\n }\n if (\n candidate.type === 'FunctionDeclaration' &&\n candidate.id?.name === name\n ) {\n return candidate;\n }\n }\n }\n }\n\n return null;\n};\n\n/** Ensures `export { exportedName } from source`, skipping when already declared/re-exported. */\nconst ensureExportFrom = (\n ast: any,\n exportedName: string,\n source: string\n): void => {\n const body = ast.program.body;\n\n const alreadyPresent = body.some((stmt: any) => {\n if (\n stmt.type === 'ExportNamedDeclaration' &&\n stmt.source?.value === source\n ) {\n return stmt.specifiers.some(\n (spec: any) => spec.exported?.name === exportedName\n );\n }\n // Locally declared `export const/function generateStaticParams`\n if (stmt.type === 'ExportNamedDeclaration' && stmt.declaration) {\n const decl = stmt.declaration;\n if (decl.type === 'FunctionDeclaration' && decl.id?.name === exportedName)\n return true;\n if (decl.type === 'VariableDeclaration') {\n return decl.declarations.some(\n (d: any) => d.id?.type === 'Identifier' && d.id.name === exportedName\n );\n }\n }\n return false;\n });\n\n if (alreadyPresent) return;\n\n const exportNode = parseTsx(`export { ${exportedName} } from \"${source}\";`)\n .program.body[0];\n ast.program.body.splice(firstInsertIndex(ast), 0, exportNode);\n};\n\n/** Makes the function async and inserts `const locale = await getLocale();` once, at the top of its body. */\nconst ensureAwaitedLocale = (funcNode: any): void => {\n funcNode.async = true;\n\n if (funcNode.body.type !== 'BlockStatement') {\n funcNode.body = b.blockStatement([b.returnStatement(funcNode.body)]);\n }\n\n const hasLocale = funcNode.body.body.some(\n (stmt: any) =>\n stmt.type === 'VariableDeclaration' &&\n stmt.declarations.some(\n (d: any) =>\n // const locale = ...\n (d.id?.type === 'Identifier' && d.id.name === 'locale') ||\n // const { locale } = ... or const { locale: locale } = ...\n (d.id?.type === 'ObjectPattern' &&\n d.id.properties?.some(\n (prop: any) =>\n prop.value?.type === 'Identifier' &&\n prop.value?.name === 'locale'\n ))\n )\n );\n\n if (!hasLocale) {\n const localeStatement = parseTsx('const locale = await getLocale();')\n .program.body[0];\n funcNode.body.body.unshift(localeStatement);\n }\n};\n\n/** Builds `<providerName locale={locale}>{child}</providerName>` around an existing JSX child node. */\nconst buildProviderElement = (providerName: string, childNode: any): any => {\n const template = parseTsx(\n `const __wrap = <${providerName} locale={locale}>{__child__}</${providerName}>;`\n );\n const providerElement = template.program.body[0].declarations[0].init;\n providerElement.children = [childNode];\n return providerElement;\n};\n\n/** Sets `lang={locale}` on the first `<html>` element, if present. */\nconst setHtmlLang = (ast: any): void => {\n recast.visit(ast, {\n visitJSXOpeningElement(path) {\n const node = path.node;\n if (node.name?.type === 'JSXIdentifier' && node.name.name === 'html') {\n const langAttr = node.attributes?.find(\n (attr: any) =>\n attr.type === 'JSXAttribute' && attr.name?.name === 'lang'\n ) as any;\n const localeExpression = b.jsxExpressionContainer(\n b.identifier('locale')\n );\n if (langAttr) {\n langAttr.value = localeExpression;\n } else {\n node.attributes = node.attributes ?? [];\n node.attributes.push(\n b.jsxAttribute(b.jsxIdentifier('lang'), localeExpression)\n );\n }\n return false;\n }\n this.traverse(path);\n },\n });\n};\n\n/** Options of {@link wrapLayoutWithProvider}. */\nexport type WrapLayoutOptions = {\n /**\n * Re-export `generateStaticParams` from `next-intlayer`. Only meaningful for a\n * layout under a `[locale]` segment; a root layout of an unprefixed app has\n * no locale param to pre-render. Default: `true`.\n */\n exportStaticParams?: boolean;\n};\n\n/**\n * Wraps the `{children}` of a Next.js App Router **layout** with the unified\n * `IntlayerProvider`, deriving the locale via `getLocale()`. Safe and\n * idempotent: bails (returns the original code) for client components, when no\n * `{children}` placeholder is found, or when there is no default export.\n *\n * `IntlayerProvider` seeds both the server context and the client provider in\n * one mount, so this is the only provider a Next.js App Router app needs —\n * pages below the locale layout read locale/variant from it without wrapping\n * themselves individually (see the removed `wrapPageWithProvider`).\n */\nexport const wrapLayoutWithProvider = (\n code: string,\n { exportStaticParams = true }: WrapLayoutOptions = {}\n): TransformResult => {\n const ast = parseTsx(code);\n\n if (isClientComponent(ast)) return { code, status: 'skipped-client' };\n if (code.includes('IntlayerProvider')) return { code, status: 'already' };\n\n const funcNode = findDefaultExportFunction(ast);\n if (!funcNode) return { code, status: 'skipped' };\n\n let wrapped = false;\n recast.visit(funcNode, {\n visitJSXExpressionContainer(path) {\n if (wrapped) return false;\n const expression = path.node.expression;\n if (expression?.type === 'Identifier' && expression.name === 'children') {\n path.replace(buildProviderElement('IntlayerProvider', path.node));\n wrapped = true;\n return false;\n }\n this.traverse(path);\n },\n });\n\n if (!wrapped) return { code, status: 'skipped' };\n\n ensureNamedImport(ast, 'IntlayerProvider', 'next-intlayer/server');\n ensureNamedImport(ast, 'getLocale', 'next-intlayer/server');\n if (exportStaticParams) {\n ensureExportFrom(ast, 'generateStaticParams', 'next-intlayer');\n }\n ensureAwaitedLocale(funcNode);\n setHtmlLang(ast);\n\n return { code: recast.print(ast).code, status: 'wrapped' };\n};\n"],"mappings":"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"}
@@ -18,7 +18,7 @@ const installMCP = async (projectRoot, platform, transport) => {
18
18
  let configKey = "mcpServers";
19
19
  if (platform === "Claude") configPath = CLAUDE_DESKTOP_CONFIG_PATH;
20
20
  else {
21
- const relativeDir = node_path.default.dirname(require_installSkills_index.PLATFORMS_METADATA[platform].dir);
21
+ const relativeDir = node_path.default.dirname(require_installSkills_index.PLATFORMS_METADATA[platform]?.dir ?? ".");
22
22
  configPath = node_path.default.join(projectRoot, relativeDir, MCP_CONFIG_FILENAME);
23
23
  if (platform === "VSCode") configKey = "servers";
24
24
  }
@@ -1 +1 @@
1
- {"version":3,"file":"installMCP.cjs","names":["path","os","PLATFORMS_METADATA","fs"],"sources":["../../../src/installMCP/installMCP.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { PLATFORMS_METADATA, type Platform } from '../installSkills';\n\nexport type MCPTransport = 'stdio' | 'sse';\n\nconst MCP_CONFIG_FILENAME = 'mcp.json';\n\nconst CLAUDE_DESKTOP_CONFIG_PATH =\n process.platform === 'win32'\n ? path.join(\n process.env.APPDATA || '',\n 'Claude',\n 'claude_desktop_config.json'\n )\n : path.join(\n os.homedir(),\n 'Library',\n 'Application Support',\n 'Claude',\n 'claude_desktop_config.json'\n );\n\n/**\n * Installs the Intlayer MCP server configuration for a specific platform.\n */\nexport const installMCP = async (\n projectRoot: string,\n platform: Platform,\n transport: MCPTransport\n): Promise<string> => {\n let configPath: string;\n let configKey = 'mcpServers';\n\n if (platform === 'Claude') {\n configPath = CLAUDE_DESKTOP_CONFIG_PATH;\n } else {\n const relativeDir = path.dirname(PLATFORMS_METADATA[platform].dir); // e.g. .cursor or .vscode\n configPath = path.join(projectRoot, relativeDir, MCP_CONFIG_FILENAME);\n\n if (platform === 'VSCode') {\n configKey = 'servers';\n }\n }\n\n // Ensure the configuration directory exists\n await fs.mkdir(path.dirname(configPath), { recursive: true });\n\n let config: any = {};\n try {\n const content = await fs.readFile(configPath, 'utf-8');\n config = JSON.parse(content);\n } catch {\n // File doesn't exist or is invalid JSON, start fresh\n }\n\n if (!config[configKey]) {\n config[configKey] = {};\n }\n\n if (transport === 'stdio') {\n const mcpConfig: any = {\n command: 'npx',\n args: ['-y', '@intlayer/mcp'],\n };\n\n if (platform === 'VSCode') {\n mcpConfig.type = 'stdio';\n }\n\n config[configKey].intlayer = mcpConfig;\n } else {\n const mcpConfig: any = {\n url: 'https://mcp.intlayer.org',\n };\n\n if (platform === 'VSCode') {\n mcpConfig.type = 'sse';\n } else {\n mcpConfig.transport = 'sse';\n }\n\n config[configKey].intlayer = mcpConfig;\n }\n\n await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');\n\n return `MCP server configuration updated in ${configPath}`;\n};\n"],"mappings":";;;;;;;;;;AAOA,MAAM,sBAAsB;AAE5B,MAAM,6BACJ,QAAQ,aAAa,UACjBA,kBAAK,KACH,QAAQ,IAAI,WAAW,IACvB,UACA,4BACF,IACAA,kBAAK,KACHC,gBAAG,QAAQ,GACX,WACA,uBACA,UACA,4BACF;;;;AAKN,MAAa,aAAa,OACxB,aACA,UACA,cACoB;CACpB,IAAI;CACJ,IAAI,YAAY;CAEhB,IAAI,aAAa,UACf,aAAa;MACR;EACL,MAAM,cAAcD,kBAAK,QAAQE,+CAAmB,SAAS,CAAC,GAAG;EACjE,aAAaF,kBAAK,KAAK,aAAa,aAAa,mBAAmB;EAEpE,IAAI,aAAa,UACf,YAAY;CAEhB;CAGA,MAAMG,iBAAG,MAAMH,kBAAK,QAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;CAE5D,IAAI,SAAc,CAAC;CACnB,IAAI;EACF,MAAM,UAAU,MAAMG,iBAAG,SAAS,YAAY,OAAO;EACrD,SAAS,KAAK,MAAM,OAAO;CAC7B,QAAQ,CAER;CAEA,IAAI,CAAC,OAAO,YACV,OAAO,aAAa,CAAC;CAGvB,IAAI,cAAc,SAAS;EACzB,MAAM,YAAiB;GACrB,SAAS;GACT,MAAM,CAAC,MAAM,eAAe;EAC9B;EAEA,IAAI,aAAa,UACf,UAAU,OAAO;EAGnB,OAAO,UAAU,CAAC,WAAW;CAC/B,OAAO;EACL,MAAM,YAAiB,EACrB,KAAK,2BACP;EAEA,IAAI,aAAa,UACf,UAAU,OAAO;OAEjB,UAAU,YAAY;EAGxB,OAAO,UAAU,CAAC,WAAW;CAC/B;CAEA,MAAMA,iBAAG,UAAU,YAAY,KAAK,UAAU,QAAQ,MAAM,CAAC,GAAG,OAAO;CAEvE,OAAO,uCAAuC;AAChD"}
1
+ {"version":3,"file":"installMCP.cjs","names":["path","os","PLATFORMS_METADATA","fs"],"sources":["../../../src/installMCP/installMCP.ts"],"sourcesContent":["import { promises as fs } from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { PLATFORMS_METADATA, type Platform } from '../installSkills';\n\nexport type MCPTransport = 'stdio' | 'sse';\n\nconst MCP_CONFIG_FILENAME = 'mcp.json';\n\nconst CLAUDE_DESKTOP_CONFIG_PATH =\n process.platform === 'win32'\n ? path.join(\n process.env.APPDATA || '',\n 'Claude',\n 'claude_desktop_config.json'\n )\n : path.join(\n os.homedir(),\n 'Library',\n 'Application Support',\n 'Claude',\n 'claude_desktop_config.json'\n );\n\n/**\n * Installs the Intlayer MCP server configuration for a specific platform.\n */\nexport const installMCP = async (\n projectRoot: string,\n platform: Platform,\n transport: MCPTransport\n): Promise<string> => {\n let configPath: string;\n let configKey = 'mcpServers';\n\n if (platform === 'Claude') {\n configPath = CLAUDE_DESKTOP_CONFIG_PATH;\n } else {\n const relativeDir = path.dirname(PLATFORMS_METADATA[platform]?.dir ?? '.'); // e.g. .cursor or .vscode\n configPath = path.join(projectRoot, relativeDir, MCP_CONFIG_FILENAME);\n\n if (platform === 'VSCode') {\n configKey = 'servers';\n }\n }\n\n // Ensure the configuration directory exists\n await fs.mkdir(path.dirname(configPath), { recursive: true });\n\n let config: any = {};\n try {\n const content = await fs.readFile(configPath, 'utf-8');\n config = JSON.parse(content);\n } catch {\n // File doesn't exist or is invalid JSON, start fresh\n }\n\n if (!config[configKey]) {\n config[configKey] = {};\n }\n\n if (transport === 'stdio') {\n const mcpConfig: any = {\n command: 'npx',\n args: ['-y', '@intlayer/mcp'],\n };\n\n if (platform === 'VSCode') {\n mcpConfig.type = 'stdio';\n }\n\n config[configKey].intlayer = mcpConfig;\n } else {\n const mcpConfig: any = {\n url: 'https://mcp.intlayer.org',\n };\n\n if (platform === 'VSCode') {\n mcpConfig.type = 'sse';\n } else {\n mcpConfig.transport = 'sse';\n }\n\n config[configKey].intlayer = mcpConfig;\n }\n\n await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');\n\n return `MCP server configuration updated in ${configPath}`;\n};\n"],"mappings":";;;;;;;;;;AAOA,MAAM,sBAAsB;AAE5B,MAAM,6BACJ,QAAQ,aAAa,UACjBA,kBAAK,KACH,QAAQ,IAAI,WAAW,IACvB,UACA,4BACF,IACAA,kBAAK,KACHC,gBAAG,QAAQ,GACX,WACA,uBACA,UACA,4BACF;;;;AAKN,MAAa,aAAa,OACxB,aACA,UACA,cACoB;CACpB,IAAI;CACJ,IAAI,YAAY;CAEhB,IAAI,aAAa,UACf,aAAa;MACR;EACL,MAAM,cAAcD,kBAAK,QAAQE,+CAAmB,SAAS,EAAE,OAAO,GAAG;EACzE,aAAaF,kBAAK,KAAK,aAAa,aAAa,mBAAmB;EAEpE,IAAI,aAAa,UACf,YAAY;CAEhB;CAGA,MAAMG,iBAAG,MAAMH,kBAAK,QAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;CAE5D,IAAI,SAAc,CAAC;CACnB,IAAI;EACF,MAAM,UAAU,MAAMG,iBAAG,SAAS,YAAY,OAAO;EACrD,SAAS,KAAK,MAAM,OAAO;CAC7B,QAAQ,CAER;CAEA,IAAI,CAAC,OAAO,YACV,OAAO,aAAa,CAAC;CAGvB,IAAI,cAAc,SAAS;EACzB,MAAM,YAAiB;GACrB,SAAS;GACT,MAAM,CAAC,MAAM,eAAe;EAC9B;EAEA,IAAI,aAAa,UACf,UAAU,OAAO;EAGnB,OAAO,UAAU,CAAC,WAAW;CAC/B,OAAO;EACL,MAAM,YAAiB,EACrB,KAAK,2BACP;EAEA,IAAI,aAAa,UACf,UAAU,OAAO;OAEjB,UAAU,YAAY;EAGxB,OAAO,UAAU,CAAC,WAAW;CAC/B;CAEA,MAAMA,iBAAG,UAAU,YAAY,KAAK,UAAU,QAAQ,MAAM,CAAC,GAAG,OAAO;CAEvE,OAAO,uCAAuC;AAChD"}
@@ -13,8 +13,10 @@ const loadLocalDictionaries = async (contentDeclarationsPaths, configuration) =>
13
13
  for await (const contentDeclarationPath of contentDeclarationsPaths) {
14
14
  const dictionary = await require_loadDictionaries_loadContentDeclaration.loadContentDeclarations([contentDeclarationPath], configuration);
15
15
  const relativeFilePath = (0, node_path.relative)(baseDir, contentDeclarationPath);
16
+ const firstDict = dictionary[0];
17
+ if (!firstDict) continue;
16
18
  const dictionaryWithPath = {
17
- ...dictionary[0],
19
+ ...firstDict,
18
20
  filePath: relativeFilePath
19
21
  };
20
22
  result.push(dictionaryWithPath);
@@ -1 +1 @@
1
- {"version":3,"file":"loadLocalDictionaries.cjs","names":["mkdir","resolve","loadContentDeclarations","relative"],"sources":["../../../src/loadDictionaries/loadLocalDictionaries.ts"],"sourcesContent":["import { mkdir } from 'node:fs/promises';\nimport { relative, resolve } from 'node:path';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\nimport { loadContentDeclarations } from './loadContentDeclaration';\n\nexport const loadLocalDictionaries = async (\n contentDeclarationsPaths: string[] | string,\n configuration: IntlayerConfig\n): Promise<Dictionary[]> => {\n const { system } = configuration;\n const { dictionariesDir, baseDir } = system;\n\n if (typeof contentDeclarationsPaths === 'string') {\n contentDeclarationsPaths = [contentDeclarationsPaths];\n }\n\n // Create the dictionaries folder if it doesn't exist\n await mkdir(resolve(dictionariesDir), { recursive: true });\n\n const result: Dictionary[] = [];\n\n for await (const contentDeclarationPath of contentDeclarationsPaths) {\n const dictionary = await loadContentDeclarations(\n [contentDeclarationPath],\n configuration\n );\n\n const relativeFilePath = relative(baseDir, contentDeclarationPath);\n\n const dictionaryWithPath: Dictionary = {\n ...dictionary[0],\n filePath: relativeFilePath,\n };\n\n result.push(dictionaryWithPath);\n }\n\n return result;\n};\n"],"mappings":";;;;;;AAMA,MAAa,wBAAwB,OACnC,0BACA,kBAC0B;CAC1B,MAAM,EAAE,WAAW;CACnB,MAAM,EAAE,iBAAiB,YAAY;CAErC,IAAI,OAAO,6BAA6B,UACtC,2BAA2B,CAAC,wBAAwB;CAItD,UAAMA,4BAAMC,mBAAQ,eAAe,GAAG,EAAE,WAAW,KAAK,CAAC;CAEzD,MAAM,SAAuB,CAAC;CAE9B,WAAW,MAAM,0BAA0B,0BAA0B;EACnE,MAAM,aAAa,MAAMC,wEACvB,CAAC,sBAAsB,GACvB,aACF;EAEA,MAAM,uBAAmBC,oBAAS,SAAS,sBAAsB;EAEjE,MAAM,qBAAiC;GACrC,GAAG,WAAW;GACd,UAAU;EACZ;EAEA,OAAO,KAAK,kBAAkB;CAChC;CAEA,OAAO;AACT"}
1
+ {"version":3,"file":"loadLocalDictionaries.cjs","names":["mkdir","resolve","loadContentDeclarations","relative"],"sources":["../../../src/loadDictionaries/loadLocalDictionaries.ts"],"sourcesContent":["import { mkdir } from 'node:fs/promises';\nimport { relative, resolve } from 'node:path';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport type { Dictionary } from '@intlayer/types/dictionary';\nimport { loadContentDeclarations } from './loadContentDeclaration';\n\nexport const loadLocalDictionaries = async (\n contentDeclarationsPaths: string[] | string,\n configuration: IntlayerConfig\n): Promise<Dictionary[]> => {\n const { system } = configuration;\n const { dictionariesDir, baseDir } = system;\n\n if (typeof contentDeclarationsPaths === 'string') {\n contentDeclarationsPaths = [contentDeclarationsPaths];\n }\n\n // Create the dictionaries folder if it doesn't exist\n await mkdir(resolve(dictionariesDir), { recursive: true });\n\n const result: Dictionary[] = [];\n\n for await (const contentDeclarationPath of contentDeclarationsPaths) {\n const dictionary = await loadContentDeclarations(\n [contentDeclarationPath],\n configuration\n );\n\n const relativeFilePath = relative(baseDir, contentDeclarationPath);\n\n const firstDict = dictionary[0];\n if (!firstDict) continue;\n\n const dictionaryWithPath: Dictionary = {\n ...firstDict,\n filePath: relativeFilePath,\n };\n\n result.push(dictionaryWithPath);\n }\n\n return result;\n};\n"],"mappings":";;;;;;AAMA,MAAa,wBAAwB,OACnC,0BACA,kBAC0B;CAC1B,MAAM,EAAE,WAAW;CACnB,MAAM,EAAE,iBAAiB,YAAY;CAErC,IAAI,OAAO,6BAA6B,UACtC,2BAA2B,CAAC,wBAAwB;CAItD,UAAMA,4BAAMC,mBAAQ,eAAe,GAAG,EAAE,WAAW,KAAK,CAAC;CAEzD,MAAM,SAAuB,CAAC;CAE9B,WAAW,MAAM,0BAA0B,0BAA0B;EACnE,MAAM,aAAa,MAAMC,wEACvB,CAAC,sBAAsB,GACvB,aACF;EAEA,MAAM,uBAAmBC,oBAAS,SAAS,sBAAsB;EAEjE,MAAM,YAAY,WAAW;EAC7B,IAAI,CAAC,WAAW;EAEhB,MAAM,qBAAiC;GACrC,GAAG;GACH,UAAU;EACZ;EAEA,OAAO,KAAK,kBAAkB;CAChC;CAEA,OAAO;AACT"}
@@ -30,7 +30,7 @@ const loadRemoteDictionaries = async (configuration = (0, _intlayer_config_node.
30
30
  return data.updatedAt > localUpdatedAt;
31
31
  });
32
32
  const cachedDictionaries = Object.values(remoteDictionariesRecord).flat().filter((dictionary) => {
33
- const remoteUpdatedAt = distantDictionaryUpdateTimeStamp[dictionary.id].updatedAt;
33
+ const remoteUpdatedAt = dictionary.id ? distantDictionaryUpdateTimeStamp[dictionary.id]?.updatedAt : void 0;
34
34
  const localUpdatedAtRaw = dictionary.updatedAt;
35
35
  const localUpdatedAt = typeof localUpdatedAtRaw === "number" ? localUpdatedAtRaw : localUpdatedAtRaw ? new Date(localUpdatedAtRaw).getTime() : void 0;
36
36
  return typeof localUpdatedAt === "number" && typeof remoteUpdatedAt === "number" && localUpdatedAt >= remoteUpdatedAt;
@@ -1 +1 @@
1
- {"version":3,"file":"loadRemoteDictionaries.cjs","names":["getConfiguration","getRemoteDictionaries","dictionaryEndpoint","createIntlayerCMS","sortAlphabetically","fetchDistantDictionaries"],"sources":["../../../src/loadDictionaries/loadRemoteDictionaries.ts"],"sourcesContent":["import { createIntlayerCMS } from '@intlayer/api';\nimport { dictionaryEndpoint } from '@intlayer/api/dictionary';\n// @ts-ignore @intlayer/backend is not build yet\nimport type { DictionaryAPI } from '@intlayer/backend';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { getRemoteDictionaries } from '@intlayer/dictionaries-entry/remote';\nimport type {\n Dictionary,\n DictionaryId,\n DictionaryKey,\n} from '@intlayer/types/dictionary';\nimport { fetchDistantDictionaries } from '../fetchDistantDictionaries';\nimport { sortAlphabetically } from '../utils/sortAlphabetically';\nimport type { DictionariesStatus } from './loadDictionaries';\n\nexport const formatDistantDictionaries = (\n dictionaries: (DictionaryAPI | Dictionary)[]\n): Dictionary[] =>\n dictionaries.map((dict) => ({\n ...dict,\n localId: `${dict.key}::remote::${dict.id}`,\n location: 'remote' as const,\n }));\n\nexport const loadRemoteDictionaries = async (\n configuration = getConfiguration(),\n onStatusUpdate?: (status: DictionariesStatus[]) => void,\n options?: {\n onStartRemoteCheck?: () => void;\n onStopRemoteCheck?: () => void;\n onError?: (error: Error) => void;\n }\n): Promise<Dictionary[]> => {\n const { editor } = configuration;\n const remoteDictionariesRecord = getRemoteDictionaries(configuration);\n\n const hasRemoteDictionaries = Boolean(editor.clientId && editor.clientSecret);\n\n if (!hasRemoteDictionaries) return [];\n\n try {\n options?.onStartRemoteCheck?.();\n\n const dictionary = dictionaryEndpoint(createIntlayerCMS(configuration));\n\n // Get the list of dictionary keys\n const getDictionariesKeysResult =\n await dictionary.getDictionariesUpdateTimestamp();\n\n const distantDictionaryUpdateTimeStamp: Record<\n DictionaryId,\n { key: DictionaryKey; updatedAt: number }\n > | null = getDictionariesKeysResult.data;\n\n if (!distantDictionaryUpdateTimeStamp) {\n throw new Error('No distant dictionaries found');\n }\n\n const dictionariesIdToFetch = Object.entries(\n distantDictionaryUpdateTimeStamp\n ).filter(([dictionaryId, data]) => {\n // If remote doesn't provide updatedAt, fetch to be safe\n if (!data.updatedAt) return true;\n\n // If no local cache exists, fetch\n const local: Dictionary | undefined = remoteDictionariesRecord[\n data.key\n ]?.find((dictionary) => dictionary.id === dictionaryId);\n if (!local) return true;\n\n const localUpdatedAtRaw = (local as any)?.updatedAt as\n | number\n | string\n | undefined;\n\n const localUpdatedAt =\n typeof localUpdatedAtRaw === 'number'\n ? localUpdatedAtRaw\n : localUpdatedAtRaw\n ? new Date(localUpdatedAtRaw).getTime()\n : undefined;\n\n // If local timestamp missing or older than remote, fetch\n if (typeof localUpdatedAt !== 'number') return true;\n\n return data.updatedAt > localUpdatedAt;\n });\n\n const flatRemoteDictionariesRecord: DictionaryAPI[] = Object.values(\n remoteDictionariesRecord\n ).flat();\n\n const cachedDictionaries: Dictionary[] =\n flatRemoteDictionariesRecord.filter((dictionary) => {\n const remoteUpdatedAt =\n distantDictionaryUpdateTimeStamp[dictionary.id!].updatedAt;\n\n const localUpdatedAtRaw = dictionary.updatedAt;\n\n const localUpdatedAt =\n typeof localUpdatedAtRaw === 'number'\n ? localUpdatedAtRaw\n : localUpdatedAtRaw\n ? new Date(localUpdatedAtRaw).getTime()\n : undefined;\n\n // Consider as cached/imported when local exists and is up-to-date or newer\n return (\n typeof localUpdatedAt === 'number' &&\n typeof remoteUpdatedAt === 'number' &&\n localUpdatedAt >= remoteUpdatedAt\n );\n });\n\n // Report cached as already imported\n if (cachedDictionaries.length > 0) {\n onStatusUpdate?.(\n cachedDictionaries.map((dictionary) => ({\n dictionaryKey: dictionary.key,\n type: 'remote',\n status: 'imported',\n }))\n );\n }\n\n // Several qualified dictionaries can share a key, so keys are deduplicated:\n // one request per key already returns all of its siblings.\n const orderedDistantDictionaryKeys = [\n ...new Set(dictionariesIdToFetch.map(([, data]) => data.key)),\n ].sort(sortAlphabetically);\n\n // Report pending for keys to be fetched so totals are visible immediately\n if (orderedDistantDictionaryKeys.length > 0) {\n onStatusUpdate?.(\n orderedDistantDictionaryKeys.map((key) => ({\n dictionaryKey: key,\n type: 'remote',\n status: 'pending',\n }))\n );\n }\n\n const distantDictionariesData = await fetchDistantDictionaries(\n {\n dictionaryKeys: orderedDistantDictionaryKeys,\n },\n onStatusUpdate\n );\n\n const distantDictionaries: Dictionary[] = formatDistantDictionaries(\n distantDictionariesData\n );\n\n // Fetching by key also returns the up-to-date siblings of a stale\n // dictionary, so drop the cached copies the remote just sent back.\n const fetchedDictionaryIds = new Set(\n distantDictionaries.map((dictionary) => dictionary.id)\n );\n\n const notRefetchedCachedDictionaries = cachedDictionaries.filter(\n (dictionary) => !fetchedDictionaryIds.has(dictionary.id)\n );\n\n return [...notRefetchedCachedDictionaries, ...distantDictionaries];\n } catch (error) {\n options?.onError?.(error as Error);\n return [];\n } finally {\n options?.onStopRemoteCheck?.();\n }\n};\n"],"mappings":";;;;;;;;;AAeA,MAAa,6BACX,iBAEA,aAAa,KAAK,UAAU;CAC1B,GAAG;CACH,SAAS,GAAG,KAAK,IAAI,YAAY,KAAK;CACtC,UAAU;AACZ,EAAE;AAEJ,MAAa,yBAAyB,OACpC,oBAAgBA,wCAAiB,GACjC,gBACA,YAK0B;CAC1B,MAAM,EAAE,WAAW;CACnB,MAAM,+BAA2BC,2DAAsB,aAAa;CAIpE,IAAI,CAF0B,QAAQ,OAAO,YAAY,OAAO,YAEvC,GAAG,OAAO,CAAC;CAEpC,IAAI;EACF,SAAS,qBAAqB;EAQ9B,MAAM,oCAGK,UATQC,iDAAmBC,iCAAkB,aAAa,CAIpD,CAAC,CAAC,+BAA+B,EAKd,CAAC;EAErC,IAAI,CAAC,kCACH,MAAM,IAAI,MAAM,+BAA+B;EAGjD,MAAM,wBAAwB,OAAO,QACnC,gCACF,CAAC,CAAC,QAAQ,CAAC,cAAc,UAAU;GAEjC,IAAI,CAAC,KAAK,WAAW,OAAO;GAG5B,MAAM,QAAgC,yBACpC,KAAK,IACN,EAAE,MAAM,eAAe,WAAW,OAAO,YAAY;GACtD,IAAI,CAAC,OAAO,OAAO;GAEnB,MAAM,oBAAqB,OAAe;GAK1C,MAAM,iBACJ,OAAO,sBAAsB,WACzB,oBACA,oBACE,IAAI,KAAK,iBAAiB,CAAC,CAAC,QAAQ,IACpC;GAGR,IAAI,OAAO,mBAAmB,UAAU,OAAO;GAE/C,OAAO,KAAK,YAAY;EAC1B,CAAC;EAMD,MAAM,qBAJgD,OAAO,OAC3D,wBACF,CAAC,CAAC,KAG2B,CAAC,CAAC,QAAQ,eAAe;GAClD,MAAM,kBACJ,iCAAiC,WAAW,GAAI,CAAC;GAEnD,MAAM,oBAAoB,WAAW;GAErC,MAAM,iBACJ,OAAO,sBAAsB,WACzB,oBACA,oBACE,IAAI,KAAK,iBAAiB,CAAC,CAAC,QAAQ,IACpC;GAGR,OACE,OAAO,mBAAmB,YAC1B,OAAO,oBAAoB,YAC3B,kBAAkB;EAEtB,CAAC;EAGH,IAAI,mBAAmB,SAAS,GAC9B,iBACE,mBAAmB,KAAK,gBAAgB;GACtC,eAAe,WAAW;GAC1B,MAAM;GACN,QAAQ;EACV,EAAE,CACJ;EAKF,MAAM,+BAA+B,CACnC,GAAG,IAAI,IAAI,sBAAsB,KAAK,GAAG,UAAU,KAAK,GAAG,CAAC,CAC9D,CAAC,CAAC,KAAKC,mDAAkB;EAGzB,IAAI,6BAA6B,SAAS,GACxC,iBACE,6BAA6B,KAAK,SAAS;GACzC,eAAe;GACf,MAAM;GACN,QAAQ;EACV,EAAE,CACJ;EAGF,MAAM,0BAA0B,MAAMC,0DACpC,EACE,gBAAgB,6BAClB,GACA,cACF;EAEA,MAAM,sBAAoC,0BACxC,uBACF;EAIA,MAAM,uBAAuB,IAAI,IAC/B,oBAAoB,KAAK,eAAe,WAAW,EAAE,CACvD;EAMA,OAAO,CAAC,GAJ+B,mBAAmB,QACvD,eAAe,CAAC,qBAAqB,IAAI,WAAW,EAAE,CAGjB,GAAG,GAAG,mBAAmB;CACnE,SAAS,OAAO;EACd,SAAS,UAAU,KAAc;EACjC,OAAO,CAAC;CACV,UAAU;EACR,SAAS,oBAAoB;CAC/B;AACF"}
1
+ {"version":3,"file":"loadRemoteDictionaries.cjs","names":["getConfiguration","getRemoteDictionaries","dictionaryEndpoint","createIntlayerCMS","sortAlphabetically","fetchDistantDictionaries"],"sources":["../../../src/loadDictionaries/loadRemoteDictionaries.ts"],"sourcesContent":["import { createIntlayerCMS } from '@intlayer/api';\nimport { dictionaryEndpoint } from '@intlayer/api/dictionary';\n// @ts-ignore @intlayer/backend is not build yet\nimport type { DictionaryAPI } from '@intlayer/backend';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { getRemoteDictionaries } from '@intlayer/dictionaries-entry/remote';\nimport type {\n Dictionary,\n DictionaryId,\n DictionaryKey,\n} from '@intlayer/types/dictionary';\nimport { fetchDistantDictionaries } from '../fetchDistantDictionaries';\nimport { sortAlphabetically } from '../utils/sortAlphabetically';\nimport type { DictionariesStatus } from './loadDictionaries';\n\nexport const formatDistantDictionaries = (\n dictionaries: (DictionaryAPI | Dictionary)[]\n): Dictionary[] =>\n dictionaries.map((dict) => ({\n ...dict,\n localId: `${dict.key}::remote::${dict.id}`,\n location: 'remote' as const,\n }));\n\nexport const loadRemoteDictionaries = async (\n configuration = getConfiguration(),\n onStatusUpdate?: (status: DictionariesStatus[]) => void,\n options?: {\n onStartRemoteCheck?: () => void;\n onStopRemoteCheck?: () => void;\n onError?: (error: Error) => void;\n }\n): Promise<Dictionary[]> => {\n const { editor } = configuration;\n const remoteDictionariesRecord = getRemoteDictionaries(configuration);\n\n const hasRemoteDictionaries = Boolean(editor.clientId && editor.clientSecret);\n\n if (!hasRemoteDictionaries) return [];\n\n try {\n options?.onStartRemoteCheck?.();\n\n const dictionary = dictionaryEndpoint(createIntlayerCMS(configuration));\n\n // Get the list of dictionary keys\n const getDictionariesKeysResult =\n await dictionary.getDictionariesUpdateTimestamp();\n\n const distantDictionaryUpdateTimeStamp: Record<\n DictionaryId,\n { key: DictionaryKey; updatedAt: number }\n > | null = getDictionariesKeysResult.data;\n\n if (!distantDictionaryUpdateTimeStamp) {\n throw new Error('No distant dictionaries found');\n }\n\n const dictionariesIdToFetch = Object.entries(\n distantDictionaryUpdateTimeStamp\n ).filter(([dictionaryId, data]) => {\n // If remote doesn't provide updatedAt, fetch to be safe\n if (!data.updatedAt) return true;\n\n // If no local cache exists, fetch\n const local: Dictionary | undefined = remoteDictionariesRecord[\n data.key\n ]?.find((dictionary) => dictionary.id === dictionaryId);\n if (!local) return true;\n\n const localUpdatedAtRaw = (local as any)?.updatedAt as\n | number\n | string\n | undefined;\n\n const localUpdatedAt =\n typeof localUpdatedAtRaw === 'number'\n ? localUpdatedAtRaw\n : localUpdatedAtRaw\n ? new Date(localUpdatedAtRaw).getTime()\n : undefined;\n\n // If local timestamp missing or older than remote, fetch\n if (typeof localUpdatedAt !== 'number') return true;\n\n return data.updatedAt > localUpdatedAt;\n });\n\n const flatRemoteDictionariesRecord: DictionaryAPI[] = Object.values(\n remoteDictionariesRecord\n ).flat();\n\n const cachedDictionaries: Dictionary[] =\n flatRemoteDictionariesRecord.filter((dictionary) => {\n const remoteUpdatedAt = dictionary.id\n ? distantDictionaryUpdateTimeStamp[dictionary.id]?.updatedAt\n : undefined;\n\n const localUpdatedAtRaw = dictionary.updatedAt;\n\n const localUpdatedAt =\n typeof localUpdatedAtRaw === 'number'\n ? localUpdatedAtRaw\n : localUpdatedAtRaw\n ? new Date(localUpdatedAtRaw).getTime()\n : undefined;\n\n // Consider as cached/imported when local exists and is up-to-date or newer\n return (\n typeof localUpdatedAt === 'number' &&\n typeof remoteUpdatedAt === 'number' &&\n localUpdatedAt >= remoteUpdatedAt\n );\n });\n\n // Report cached as already imported\n if (cachedDictionaries.length > 0) {\n onStatusUpdate?.(\n cachedDictionaries.map((dictionary) => ({\n dictionaryKey: dictionary.key,\n type: 'remote',\n status: 'imported',\n }))\n );\n }\n\n // Several qualified dictionaries can share a key, so keys are deduplicated:\n // one request per key already returns all of its siblings.\n const orderedDistantDictionaryKeys = [\n ...new Set(dictionariesIdToFetch.map(([, data]) => data.key)),\n ].sort(sortAlphabetically);\n\n // Report pending for keys to be fetched so totals are visible immediately\n if (orderedDistantDictionaryKeys.length > 0) {\n onStatusUpdate?.(\n orderedDistantDictionaryKeys.map((key) => ({\n dictionaryKey: key,\n type: 'remote',\n status: 'pending',\n }))\n );\n }\n\n const distantDictionariesData = await fetchDistantDictionaries(\n {\n dictionaryKeys: orderedDistantDictionaryKeys,\n },\n onStatusUpdate\n );\n\n const distantDictionaries: Dictionary[] = formatDistantDictionaries(\n distantDictionariesData\n );\n\n // Fetching by key also returns the up-to-date siblings of a stale\n // dictionary, so drop the cached copies the remote just sent back.\n const fetchedDictionaryIds = new Set(\n distantDictionaries.map((dictionary) => dictionary.id)\n );\n\n const notRefetchedCachedDictionaries = cachedDictionaries.filter(\n (dictionary) => !fetchedDictionaryIds.has(dictionary.id)\n );\n\n return [...notRefetchedCachedDictionaries, ...distantDictionaries];\n } catch (error) {\n options?.onError?.(error as Error);\n return [];\n } finally {\n options?.onStopRemoteCheck?.();\n }\n};\n"],"mappings":";;;;;;;;;AAeA,MAAa,6BACX,iBAEA,aAAa,KAAK,UAAU;CAC1B,GAAG;CACH,SAAS,GAAG,KAAK,IAAI,YAAY,KAAK;CACtC,UAAU;AACZ,EAAE;AAEJ,MAAa,yBAAyB,OACpC,oBAAgBA,wCAAiB,GACjC,gBACA,YAK0B;CAC1B,MAAM,EAAE,WAAW;CACnB,MAAM,+BAA2BC,2DAAsB,aAAa;CAIpE,IAAI,CAF0B,QAAQ,OAAO,YAAY,OAAO,YAEvC,GAAG,OAAO,CAAC;CAEpC,IAAI;EACF,SAAS,qBAAqB;EAQ9B,MAAM,oCAGK,UATQC,iDAAmBC,iCAAkB,aAAa,CAIpD,CAAC,CAAC,+BAA+B,EAKd,CAAC;EAErC,IAAI,CAAC,kCACH,MAAM,IAAI,MAAM,+BAA+B;EAGjD,MAAM,wBAAwB,OAAO,QACnC,gCACF,CAAC,CAAC,QAAQ,CAAC,cAAc,UAAU;GAEjC,IAAI,CAAC,KAAK,WAAW,OAAO;GAG5B,MAAM,QAAgC,yBACpC,KAAK,IACN,EAAE,MAAM,eAAe,WAAW,OAAO,YAAY;GACtD,IAAI,CAAC,OAAO,OAAO;GAEnB,MAAM,oBAAqB,OAAe;GAK1C,MAAM,iBACJ,OAAO,sBAAsB,WACzB,oBACA,oBACE,IAAI,KAAK,iBAAiB,CAAC,CAAC,QAAQ,IACpC;GAGR,IAAI,OAAO,mBAAmB,UAAU,OAAO;GAE/C,OAAO,KAAK,YAAY;EAC1B,CAAC;EAMD,MAAM,qBAJgD,OAAO,OAC3D,wBACF,CAAC,CAAC,KAG2B,CAAC,CAAC,QAAQ,eAAe;GAClD,MAAM,kBAAkB,WAAW,KAC/B,iCAAiC,WAAW,GAAG,EAAE,YACjD;GAEJ,MAAM,oBAAoB,WAAW;GAErC,MAAM,iBACJ,OAAO,sBAAsB,WACzB,oBACA,oBACE,IAAI,KAAK,iBAAiB,CAAC,CAAC,QAAQ,IACpC;GAGR,OACE,OAAO,mBAAmB,YAC1B,OAAO,oBAAoB,YAC3B,kBAAkB;EAEtB,CAAC;EAGH,IAAI,mBAAmB,SAAS,GAC9B,iBACE,mBAAmB,KAAK,gBAAgB;GACtC,eAAe,WAAW;GAC1B,MAAM;GACN,QAAQ;EACV,EAAE,CACJ;EAKF,MAAM,+BAA+B,CACnC,GAAG,IAAI,IAAI,sBAAsB,KAAK,GAAG,UAAU,KAAK,GAAG,CAAC,CAC9D,CAAC,CAAC,KAAKC,mDAAkB;EAGzB,IAAI,6BAA6B,SAAS,GACxC,iBACE,6BAA6B,KAAK,SAAS;GACzC,eAAe;GACf,MAAM;GACN,QAAQ;EACV,EAAE,CACJ;EAGF,MAAM,0BAA0B,MAAMC,0DACpC,EACE,gBAAgB,6BAClB,GACA,cACF;EAEA,MAAM,sBAAoC,0BACxC,uBACF;EAIA,MAAM,uBAAuB,IAAI,IAC/B,oBAAoB,KAAK,eAAe,WAAW,EAAE,CACvD;EAMA,OAAO,CAAC,GAJ+B,mBAAmB,QACvD,eAAe,CAAC,qBAAqB,IAAI,WAAW,EAAE,CAGjB,GAAG,GAAG,mBAAmB;CACnE,SAAS,OAAO;EACd,SAAS,UAAU,KAAc;EACjC,OAAO,CAAC;CACV,UAAU;EACR,SAAS,oBAAoB;CAC/B;AACF"}
@@ -109,7 +109,7 @@ var DictionariesLogger = class {
109
109
  }
110
110
  render() {
111
111
  const { localTotal, localDone, remoteTotal, remoteDone, pluginTotal, pluginDone } = this.computeProgress();
112
- const frame = this.spinnerFrames[this.spinnerIndex];
112
+ const frame = this.spinnerFrames[this.spinnerIndex] ?? "";
113
113
  const clock = (0, _intlayer_config_logger.colorize)(frame, _intlayer_config_colors.BLUE);
114
114
  const lines = [];
115
115
  const isLocalDone = localDone === localTotal;
@@ -1 +1 @@
1
- {"version":3,"file":"log.cjs","names":["spinnerFrames","getPrefix","log","extractErrorMessage","colorize","ANSIColors","v","x"],"sources":["../../../src/loadDictionaries/log.ts"],"sourcesContent":["import { log } from '@intlayer/config/built';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n getPrefix,\n spinnerFrames,\n v,\n x,\n} from '@intlayer/config/logger';\nimport { extractErrorMessage } from '@intlayer/config/utils';\nimport type { DictionariesStatus } from './loadDictionaries';\n\nexport class DictionariesLogger {\n private statuses: DictionariesStatus[] = [];\n private spinnerTimer: NodeJS.Timeout | null = null;\n private spinnerIndex = 0;\n private renderedLines = 0;\n private readonly spinnerFrames = spinnerFrames;\n private isFinished = false;\n private readonly prefix: string;\n private lastRenderedState: string = '';\n private remoteCheckInProgress = false;\n private expectRemote = false;\n private remoteError: string | undefined;\n private pluginTotal = 0;\n private pluginDone = 0;\n private pluginNames: string[] = [];\n private pluginError: string | undefined;\n\n constructor() {\n this.prefix = getPrefix(log?.prefix) ?? '';\n }\n\n setExpectRemote(expect: boolean) {\n this.expectRemote = expect;\n }\n\n startRemoteCheck() {\n if (this.isFinished) return;\n this.remoteCheckInProgress = true;\n this.startSpinner();\n this.render();\n }\n\n stopRemoteCheck() {\n this.remoteCheckInProgress = false;\n }\n\n update(newStatuses: DictionariesStatus[]) {\n if (this.isFinished) return;\n for (const status of newStatuses) {\n const index = this.statuses.findIndex(\n (s) =>\n s.dictionaryKey === status.dictionaryKey && s.type === status.type\n );\n if (index >= 0) {\n this.statuses[index] = status;\n } else {\n this.statuses.push(status);\n }\n }\n\n // If we expect remote fetch later, avoid rendering a local-only line first\n const { remoteTotal } = this.computeProgress();\n if (this.expectRemote && !this.remoteCheckInProgress && remoteTotal === 0) {\n // Do not start spinner or render yet; wait until remote check starts\n return;\n }\n\n this.startSpinner();\n this.render();\n }\n\n finish() {\n this.isFinished = true;\n this.stopSpinner();\n // Render final state and keep it visible\n this.render();\n }\n\n private startSpinner() {\n if (this.spinnerTimer || this.isFinished) return;\n this.spinnerTimer = setInterval(() => {\n this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerFrames.length;\n this.render();\n }, 100);\n }\n\n private stopSpinner() {\n if (!this.spinnerTimer) return;\n clearInterval(this.spinnerTimer);\n this.spinnerTimer = null;\n }\n\n public setRemoteError = (error?: Error) => {\n this.remoteError = extractErrorMessage(error);\n // Avoid rendering a transient remote-only line while the remote check flag is still true\n // Ensure local + remote are rendered together after a failure\n this.stopRemoteCheck();\n this.render();\n };\n\n /**\n * Register the names of the plugins providing dictionaries.\n * They are displayed next to the plugin progress counters, e.g.\n * `Plugin content: 6/6 (sync-json, load-po)`.\n *\n * @param names - Names of the plugins exposing a `loadDictionaries` hook\n */\n setPluginNames(names: string[]) {\n if (this.isFinished) return;\n // Guard against plugins declared without a name at runtime\n this.pluginNames = names.filter((name) => Boolean(name));\n this.render();\n }\n\n setPluginTotal(total: number) {\n if (this.isFinished) return;\n this.pluginTotal = total;\n if (total > 0) {\n this.startSpinner();\n }\n this.render();\n }\n\n setPluginDone(done: number) {\n if (this.isFinished) return;\n this.pluginDone = done;\n this.render();\n }\n\n /**\n * Flag the plugin line as errored.\n *\n * @param error - Error thrown while loading the plugin dictionaries\n * @param pluginName - Name of the plugin that threw, prefixed to the message\n */\n setPluginError(error?: Error, pluginName?: string) {\n if (this.isFinished) return;\n const errorMessage = extractErrorMessage(error);\n this.pluginError = pluginName\n ? `${pluginName}: ${errorMessage}`\n : errorMessage;\n this.render();\n }\n\n private render() {\n const {\n localTotal,\n localDone,\n remoteTotal,\n remoteDone,\n pluginTotal,\n pluginDone,\n } = this.computeProgress();\n\n const frame = this.spinnerFrames[this.spinnerIndex];\n const clock = colorize(frame, ANSIColors.BLUE);\n const lines: string[] = [];\n\n const isLocalDone = localDone === localTotal;\n const isRemoteDone = remoteDone === remoteTotal;\n const isPluginDone = pluginDone === pluginTotal;\n\n const suppressLocalWhileCheckingRemote =\n this.expectRemote && this.remoteCheckInProgress && remoteTotal === 0;\n\n if (!suppressLocalWhileCheckingRemote) {\n if (isLocalDone) {\n lines.push(\n `${this.prefix} ${v} Local content: ${colorize(`${localDone}`, ANSIColors.GREEN)}${colorize(`/${localTotal}`, ANSIColors.GREY)}`\n );\n } else {\n lines.push(\n `${this.prefix} ${clock} Local content: ${colorize(`${localDone}`, ANSIColors.BLUE)}${colorize(`/${localTotal}`, ANSIColors.GREY)}`\n );\n }\n }\n\n // Single remote line: show error, check, or progress counts\n if (remoteTotal > 0 || this.remoteCheckInProgress || this.remoteError) {\n if (this.remoteError) {\n lines.push(\n `${this.prefix} ${x} Remote content: ${colorize(\n this.remoteError,\n ANSIColors.RED\n )}`\n );\n } else if (remoteTotal === 0) {\n lines.push(\n `${this.prefix} ${clock} Remote content: ${colorize('Check server', ANSIColors.BLUE)}`\n );\n } else if (isRemoteDone) {\n lines.push(\n `${this.prefix} ${v} Remote content: ${colorize(`${remoteDone}`, ANSIColors.GREEN)}${colorize(`/${remoteTotal}`, ANSIColors.GREY)}`\n );\n } else {\n lines.push(\n `${this.prefix} ${clock} Remote content: ${colorize(`${remoteDone}`, ANSIColors.BLUE)}${colorize(`/${remoteTotal}`, ANSIColors.GREY)}`\n );\n }\n }\n\n // Plugin line: show error or progress counts, suffixed by the plugin names\n if (pluginTotal > 0 || this.pluginError) {\n const pluginNamesSuffix =\n this.pluginNames.length > 0\n ? colorize(` (${this.pluginNames.join(', ')})`, ANSIColors.GREY)\n : '';\n\n if (this.pluginError) {\n lines.push(\n `${this.prefix} ${x} Plugin content: ${colorize(\n this.pluginError,\n ANSIColors.RED\n )}`\n );\n } else if (isPluginDone) {\n lines.push(\n `${this.prefix} ${v} Plugin content: ${colorize(`${pluginDone}`, ANSIColors.GREEN)}${colorize(`/${pluginTotal}`, ANSIColors.GREY)}${pluginNamesSuffix}`\n );\n } else {\n lines.push(\n `${this.prefix} ${clock} Plugin content: ${colorize(`${pluginDone}`, ANSIColors.BLUE)}${colorize(`/${pluginTotal}`, ANSIColors.GREY)}${pluginNamesSuffix}`\n );\n }\n }\n\n // Check if the state has changed to avoid duplicate rendering\n const currentState = lines.join('\\n');\n if (currentState === this.lastRenderedState) {\n return;\n }\n this.lastRenderedState = currentState;\n\n if (this.renderedLines > 0) {\n process.stdout.write(`\\x1b[${this.renderedLines}F`);\n }\n\n const totalLinesToClear = Math.max(this.renderedLines, lines.length);\n for (let i = 0; i < totalLinesToClear; i++) {\n process.stdout.write('\\x1b[2K');\n const line = lines[i];\n if (line !== undefined) {\n process.stdout.write(line);\n }\n process.stdout.write('\\n');\n }\n\n this.renderedLines = lines.length;\n }\n\n private computeProgress() {\n const localKeys = new Set(\n this.statuses\n .filter((s) => s.type === 'local')\n .map((s) => s.dictionaryKey)\n );\n\n const localDoneKeys = new Set(\n this.statuses\n .filter(\n (s) =>\n s.type === 'local' && (s.status === 'built' || s.status === 'error')\n )\n .map((s) => s.dictionaryKey)\n );\n\n const remoteKeys = new Set(\n this.statuses\n .filter((s) => s.type === 'remote')\n .map((s) => s.dictionaryKey)\n );\n\n const remoteDoneKeys = new Set(\n this.statuses\n .filter(\n (s) =>\n s.type === 'remote' &&\n (s.status === 'fetched' ||\n s.status === 'imported' ||\n s.status === 'error')\n )\n .map((s) => s.dictionaryKey)\n );\n\n return {\n localTotal: localKeys.size,\n localDone: localDoneKeys.size,\n remoteTotal: remoteKeys.size,\n remoteDone: remoteDoneKeys.size,\n pluginTotal: this.pluginTotal,\n pluginDone: this.pluginDone,\n } as const;\n }\n}\n"],"mappings":";;;;;;;;;AAYA,IAAa,qBAAb,MAAgC;CAC9B,AAAQ,WAAiC,CAAC;CAC1C,AAAQ,eAAsC;CAC9C,AAAQ,eAAe;CACvB,AAAQ,gBAAgB;CACxB,AAAiB,gBAAgBA;CACjC,AAAQ,aAAa;CACrB,AAAiB;CACjB,AAAQ,oBAA4B;CACpC,AAAQ,wBAAwB;CAChC,AAAQ,eAAe;CACvB,AAAQ;CACR,AAAQ,cAAc;CACtB,AAAQ,aAAa;CACrB,AAAQ,cAAwB,CAAC;CACjC,AAAQ;CAER,cAAc;EACZ,KAAK,aAASC,mCAAUC,4BAAK,MAAM,KAAK;CAC1C;CAEA,gBAAgB,QAAiB;EAC/B,KAAK,eAAe;CACtB;CAEA,mBAAmB;EACjB,IAAI,KAAK,YAAY;EACrB,KAAK,wBAAwB;EAC7B,KAAK,aAAa;EAClB,KAAK,OAAO;CACd;CAEA,kBAAkB;EAChB,KAAK,wBAAwB;CAC/B;CAEA,OAAO,aAAmC;EACxC,IAAI,KAAK,YAAY;EACrB,KAAK,MAAM,UAAU,aAAa;GAChC,MAAM,QAAQ,KAAK,SAAS,WACzB,MACC,EAAE,kBAAkB,OAAO,iBAAiB,EAAE,SAAS,OAAO,IAClE;GACA,IAAI,SAAS,GACX,KAAK,SAAS,SAAS;QAEvB,KAAK,SAAS,KAAK,MAAM;EAE7B;EAGA,MAAM,EAAE,gBAAgB,KAAK,gBAAgB;EAC7C,IAAI,KAAK,gBAAgB,CAAC,KAAK,yBAAyB,gBAAgB,GAEtE;EAGF,KAAK,aAAa;EAClB,KAAK,OAAO;CACd;CAEA,SAAS;EACP,KAAK,aAAa;EAClB,KAAK,YAAY;EAEjB,KAAK,OAAO;CACd;CAEA,AAAQ,eAAe;EACrB,IAAI,KAAK,gBAAgB,KAAK,YAAY;EAC1C,KAAK,eAAe,kBAAkB;GACpC,KAAK,gBAAgB,KAAK,eAAe,KAAK,KAAK,cAAc;GACjE,KAAK,OAAO;EACd,GAAG,GAAG;CACR;CAEA,AAAQ,cAAc;EACpB,IAAI,CAAC,KAAK,cAAc;EACxB,cAAc,KAAK,YAAY;EAC/B,KAAK,eAAe;CACtB;CAEA,AAAO,kBAAkB,UAAkB;EACzC,KAAK,kBAAcC,4CAAoB,KAAK;EAG5C,KAAK,gBAAgB;EACrB,KAAK,OAAO;CACd;;;;;;;;CASA,eAAe,OAAiB;EAC9B,IAAI,KAAK,YAAY;EAErB,KAAK,cAAc,MAAM,QAAQ,SAAS,QAAQ,IAAI,CAAC;EACvD,KAAK,OAAO;CACd;CAEA,eAAe,OAAe;EAC5B,IAAI,KAAK,YAAY;EACrB,KAAK,cAAc;EACnB,IAAI,QAAQ,GACV,KAAK,aAAa;EAEpB,KAAK,OAAO;CACd;CAEA,cAAc,MAAc;EAC1B,IAAI,KAAK,YAAY;EACrB,KAAK,aAAa;EAClB,KAAK,OAAO;CACd;;;;;;;CAQA,eAAe,OAAe,YAAqB;EACjD,IAAI,KAAK,YAAY;EACrB,MAAM,mBAAeA,4CAAoB,KAAK;EAC9C,KAAK,cAAc,aACf,GAAG,WAAW,IAAI,iBAClB;EACJ,KAAK,OAAO;CACd;CAEA,AAAQ,SAAS;EACf,MAAM,EACJ,YACA,WACA,aACA,YACA,aACA,eACE,KAAK,gBAAgB;EAEzB,MAAM,QAAQ,KAAK,cAAc,KAAK;EACtC,MAAM,YAAQC,kCAAS,OAAOC,wBAAW,IAAI;EAC7C,MAAM,QAAkB,CAAC;EAEzB,MAAM,cAAc,cAAc;EAClC,MAAM,eAAe,eAAe;EACpC,MAAM,eAAe,eAAe;EAKpC,IAAI,EAFF,KAAK,gBAAgB,KAAK,yBAAyB,gBAAgB,IAE9B;GACrC,IAAI,aACF,MAAM,KACJ,GAAG,KAAK,OAAO,GAAGC,0BAAE,sBAAkBF,kCAAS,GAAG,aAAaC,wBAAW,KAAK,QAAID,kCAAS,IAAI,cAAcC,wBAAW,IAAI,GAC/H;QAEA,MAAM,KACJ,GAAG,KAAK,OAAO,GAAG,MAAM,sBAAkBD,kCAAS,GAAG,aAAaC,wBAAW,IAAI,QAAID,kCAAS,IAAI,cAAcC,wBAAW,IAAI,GAClI;EAEJ;EAGA,IAAI,cAAc,KAAK,KAAK,yBAAyB,KAAK,aAAa;GACrE,IAAI,KAAK,aACP,MAAM,KACJ,GAAG,KAAK,OAAO,GAAGE,0BAAE,uBAAmBH,kCACrC,KAAK,aACLC,wBAAW,GACb,GACF;QACK,IAAI,gBAAgB,GACzB,MAAM,KACJ,GAAG,KAAK,OAAO,GAAG,MAAM,uBAAmBD,kCAAS,gBAAgBC,wBAAW,IAAI,GACrF;QACK,IAAI,cACT,MAAM,KACJ,GAAG,KAAK,OAAO,GAAGC,0BAAE,uBAAmBF,kCAAS,GAAG,cAAcC,wBAAW,KAAK,QAAID,kCAAS,IAAI,eAAeC,wBAAW,IAAI,GAClI;QAEA,MAAM,KACJ,GAAG,KAAK,OAAO,GAAG,MAAM,uBAAmBD,kCAAS,GAAG,cAAcC,wBAAW,IAAI,QAAID,kCAAS,IAAI,eAAeC,wBAAW,IAAI,GACrI;EAEJ;EAGA,IAAI,cAAc,KAAK,KAAK,aAAa;GACvC,MAAM,oBACJ,KAAK,YAAY,SAAS,QACtBD,kCAAS,KAAK,KAAK,YAAY,KAAK,IAAI,EAAE,IAAIC,wBAAW,IAAI,IAC7D;GAEN,IAAI,KAAK,aACP,MAAM,KACJ,GAAG,KAAK,OAAO,GAAGE,0BAAE,uBAAmBH,kCACrC,KAAK,aACLC,wBAAW,GACb,GACF;QACK,IAAI,cACT,MAAM,KACJ,GAAG,KAAK,OAAO,GAAGC,0BAAE,uBAAmBF,kCAAS,GAAG,cAAcC,wBAAW,KAAK,QAAID,kCAAS,IAAI,eAAeC,wBAAW,IAAI,IAAI,mBACtI;QAEA,MAAM,KACJ,GAAG,KAAK,OAAO,GAAG,MAAM,uBAAmBD,kCAAS,GAAG,cAAcC,wBAAW,IAAI,QAAID,kCAAS,IAAI,eAAeC,wBAAW,IAAI,IAAI,mBACzI;EAEJ;EAGA,MAAM,eAAe,MAAM,KAAK,IAAI;EACpC,IAAI,iBAAiB,KAAK,mBACxB;EAEF,KAAK,oBAAoB;EAEzB,IAAI,KAAK,gBAAgB,GACvB,QAAQ,OAAO,MAAM,QAAQ,KAAK,cAAc,EAAE;EAGpD,MAAM,oBAAoB,KAAK,IAAI,KAAK,eAAe,MAAM,MAAM;EACnE,KAAK,IAAI,IAAI,GAAG,IAAI,mBAAmB,KAAK;GAC1C,QAAQ,OAAO,MAAM,SAAS;GAC9B,MAAM,OAAO,MAAM;GACnB,IAAI,SAAS,QACX,QAAQ,OAAO,MAAM,IAAI;GAE3B,QAAQ,OAAO,MAAM,IAAI;EAC3B;EAEA,KAAK,gBAAgB,MAAM;CAC7B;CAEA,AAAQ,kBAAkB;EACxB,MAAM,YAAY,IAAI,IACpB,KAAK,SACF,QAAQ,MAAM,EAAE,SAAS,OAAO,CAAC,CACjC,KAAK,MAAM,EAAE,aAAa,CAC/B;EAEA,MAAM,gBAAgB,IAAI,IACxB,KAAK,SACF,QACE,MACC,EAAE,SAAS,YAAY,EAAE,WAAW,WAAW,EAAE,WAAW,QAChE,CAAC,CACA,KAAK,MAAM,EAAE,aAAa,CAC/B;EAEA,MAAM,aAAa,IAAI,IACrB,KAAK,SACF,QAAQ,MAAM,EAAE,SAAS,QAAQ,CAAC,CAClC,KAAK,MAAM,EAAE,aAAa,CAC/B;EAEA,MAAM,iBAAiB,IAAI,IACzB,KAAK,SACF,QACE,MACC,EAAE,SAAS,aACV,EAAE,WAAW,aACZ,EAAE,WAAW,cACb,EAAE,WAAW,QACnB,CAAC,CACA,KAAK,MAAM,EAAE,aAAa,CAC/B;EAEA,OAAO;GACL,YAAY,UAAU;GACtB,WAAW,cAAc;GACzB,aAAa,WAAW;GACxB,YAAY,eAAe;GAC3B,aAAa,KAAK;GAClB,YAAY,KAAK;EACnB;CACF;AACF"}
1
+ {"version":3,"file":"log.cjs","names":["spinnerFrames","getPrefix","log","extractErrorMessage","colorize","ANSIColors","v","x"],"sources":["../../../src/loadDictionaries/log.ts"],"sourcesContent":["import { log } from '@intlayer/config/built';\nimport * as ANSIColors from '@intlayer/config/colors';\nimport {\n colorize,\n getPrefix,\n spinnerFrames,\n v,\n x,\n} from '@intlayer/config/logger';\nimport { extractErrorMessage } from '@intlayer/config/utils';\nimport type { DictionariesStatus } from './loadDictionaries';\n\nexport class DictionariesLogger {\n private statuses: DictionariesStatus[] = [];\n private spinnerTimer: NodeJS.Timeout | null = null;\n private spinnerIndex = 0;\n private renderedLines = 0;\n private readonly spinnerFrames = spinnerFrames;\n private isFinished = false;\n private readonly prefix: string;\n private lastRenderedState: string = '';\n private remoteCheckInProgress = false;\n private expectRemote = false;\n private remoteError: string | undefined;\n private pluginTotal = 0;\n private pluginDone = 0;\n private pluginNames: string[] = [];\n private pluginError: string | undefined;\n\n constructor() {\n this.prefix = getPrefix(log?.prefix) ?? '';\n }\n\n setExpectRemote(expect: boolean) {\n this.expectRemote = expect;\n }\n\n startRemoteCheck() {\n if (this.isFinished) return;\n this.remoteCheckInProgress = true;\n this.startSpinner();\n this.render();\n }\n\n stopRemoteCheck() {\n this.remoteCheckInProgress = false;\n }\n\n update(newStatuses: DictionariesStatus[]) {\n if (this.isFinished) return;\n for (const status of newStatuses) {\n const index = this.statuses.findIndex(\n (s) =>\n s.dictionaryKey === status.dictionaryKey && s.type === status.type\n );\n if (index >= 0) {\n this.statuses[index] = status;\n } else {\n this.statuses.push(status);\n }\n }\n\n // If we expect remote fetch later, avoid rendering a local-only line first\n const { remoteTotal } = this.computeProgress();\n if (this.expectRemote && !this.remoteCheckInProgress && remoteTotal === 0) {\n // Do not start spinner or render yet; wait until remote check starts\n return;\n }\n\n this.startSpinner();\n this.render();\n }\n\n finish() {\n this.isFinished = true;\n this.stopSpinner();\n // Render final state and keep it visible\n this.render();\n }\n\n private startSpinner() {\n if (this.spinnerTimer || this.isFinished) return;\n this.spinnerTimer = setInterval(() => {\n this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerFrames.length;\n this.render();\n }, 100);\n }\n\n private stopSpinner() {\n if (!this.spinnerTimer) return;\n clearInterval(this.spinnerTimer);\n this.spinnerTimer = null;\n }\n\n public setRemoteError = (error?: Error) => {\n this.remoteError = extractErrorMessage(error);\n // Avoid rendering a transient remote-only line while the remote check flag is still true\n // Ensure local + remote are rendered together after a failure\n this.stopRemoteCheck();\n this.render();\n };\n\n /**\n * Register the names of the plugins providing dictionaries.\n * They are displayed next to the plugin progress counters, e.g.\n * `Plugin content: 6/6 (sync-json, load-po)`.\n *\n * @param names - Names of the plugins exposing a `loadDictionaries` hook\n */\n setPluginNames(names: string[]) {\n if (this.isFinished) return;\n // Guard against plugins declared without a name at runtime\n this.pluginNames = names.filter((name) => Boolean(name));\n this.render();\n }\n\n setPluginTotal(total: number) {\n if (this.isFinished) return;\n this.pluginTotal = total;\n if (total > 0) {\n this.startSpinner();\n }\n this.render();\n }\n\n setPluginDone(done: number) {\n if (this.isFinished) return;\n this.pluginDone = done;\n this.render();\n }\n\n /**\n * Flag the plugin line as errored.\n *\n * @param error - Error thrown while loading the plugin dictionaries\n * @param pluginName - Name of the plugin that threw, prefixed to the message\n */\n setPluginError(error?: Error, pluginName?: string) {\n if (this.isFinished) return;\n const errorMessage = extractErrorMessage(error);\n this.pluginError = pluginName\n ? `${pluginName}: ${errorMessage}`\n : errorMessage;\n this.render();\n }\n\n private render() {\n const {\n localTotal,\n localDone,\n remoteTotal,\n remoteDone,\n pluginTotal,\n pluginDone,\n } = this.computeProgress();\n\n const frame = this.spinnerFrames[this.spinnerIndex] ?? '';\n const clock = colorize(frame, ANSIColors.BLUE);\n const lines: string[] = [];\n\n const isLocalDone = localDone === localTotal;\n const isRemoteDone = remoteDone === remoteTotal;\n const isPluginDone = pluginDone === pluginTotal;\n\n const suppressLocalWhileCheckingRemote =\n this.expectRemote && this.remoteCheckInProgress && remoteTotal === 0;\n\n if (!suppressLocalWhileCheckingRemote) {\n if (isLocalDone) {\n lines.push(\n `${this.prefix} ${v} Local content: ${colorize(`${localDone}`, ANSIColors.GREEN)}${colorize(`/${localTotal}`, ANSIColors.GREY)}`\n );\n } else {\n lines.push(\n `${this.prefix} ${clock} Local content: ${colorize(`${localDone}`, ANSIColors.BLUE)}${colorize(`/${localTotal}`, ANSIColors.GREY)}`\n );\n }\n }\n\n // Single remote line: show error, check, or progress counts\n if (remoteTotal > 0 || this.remoteCheckInProgress || this.remoteError) {\n if (this.remoteError) {\n lines.push(\n `${this.prefix} ${x} Remote content: ${colorize(\n this.remoteError,\n ANSIColors.RED\n )}`\n );\n } else if (remoteTotal === 0) {\n lines.push(\n `${this.prefix} ${clock} Remote content: ${colorize('Check server', ANSIColors.BLUE)}`\n );\n } else if (isRemoteDone) {\n lines.push(\n `${this.prefix} ${v} Remote content: ${colorize(`${remoteDone}`, ANSIColors.GREEN)}${colorize(`/${remoteTotal}`, ANSIColors.GREY)}`\n );\n } else {\n lines.push(\n `${this.prefix} ${clock} Remote content: ${colorize(`${remoteDone}`, ANSIColors.BLUE)}${colorize(`/${remoteTotal}`, ANSIColors.GREY)}`\n );\n }\n }\n\n // Plugin line: show error or progress counts, suffixed by the plugin names\n if (pluginTotal > 0 || this.pluginError) {\n const pluginNamesSuffix =\n this.pluginNames.length > 0\n ? colorize(` (${this.pluginNames.join(', ')})`, ANSIColors.GREY)\n : '';\n\n if (this.pluginError) {\n lines.push(\n `${this.prefix} ${x} Plugin content: ${colorize(\n this.pluginError,\n ANSIColors.RED\n )}`\n );\n } else if (isPluginDone) {\n lines.push(\n `${this.prefix} ${v} Plugin content: ${colorize(`${pluginDone}`, ANSIColors.GREEN)}${colorize(`/${pluginTotal}`, ANSIColors.GREY)}${pluginNamesSuffix}`\n );\n } else {\n lines.push(\n `${this.prefix} ${clock} Plugin content: ${colorize(`${pluginDone}`, ANSIColors.BLUE)}${colorize(`/${pluginTotal}`, ANSIColors.GREY)}${pluginNamesSuffix}`\n );\n }\n }\n\n // Check if the state has changed to avoid duplicate rendering\n const currentState = lines.join('\\n');\n if (currentState === this.lastRenderedState) {\n return;\n }\n this.lastRenderedState = currentState;\n\n if (this.renderedLines > 0) {\n process.stdout.write(`\\x1b[${this.renderedLines}F`);\n }\n\n const totalLinesToClear = Math.max(this.renderedLines, lines.length);\n for (let i = 0; i < totalLinesToClear; i++) {\n process.stdout.write('\\x1b[2K');\n const line = lines[i];\n if (line !== undefined) {\n process.stdout.write(line);\n }\n process.stdout.write('\\n');\n }\n\n this.renderedLines = lines.length;\n }\n\n private computeProgress() {\n const localKeys = new Set(\n this.statuses\n .filter((s) => s.type === 'local')\n .map((s) => s.dictionaryKey)\n );\n\n const localDoneKeys = new Set(\n this.statuses\n .filter(\n (s) =>\n s.type === 'local' && (s.status === 'built' || s.status === 'error')\n )\n .map((s) => s.dictionaryKey)\n );\n\n const remoteKeys = new Set(\n this.statuses\n .filter((s) => s.type === 'remote')\n .map((s) => s.dictionaryKey)\n );\n\n const remoteDoneKeys = new Set(\n this.statuses\n .filter(\n (s) =>\n s.type === 'remote' &&\n (s.status === 'fetched' ||\n s.status === 'imported' ||\n s.status === 'error')\n )\n .map((s) => s.dictionaryKey)\n );\n\n return {\n localTotal: localKeys.size,\n localDone: localDoneKeys.size,\n remoteTotal: remoteKeys.size,\n remoteDone: remoteDoneKeys.size,\n pluginTotal: this.pluginTotal,\n pluginDone: this.pluginDone,\n } as const;\n }\n}\n"],"mappings":"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"}