ai-i18n-tools 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -0
  3. package/dist/api/openrouter.d.ts +115 -0
  4. package/dist/api/openrouter.d.ts.map +1 -0
  5. package/dist/api/openrouter.js +399 -0
  6. package/dist/api/openrouter.js.map +1 -0
  7. package/dist/cli/doc-translate.d.ts +90 -0
  8. package/dist/cli/doc-translate.d.ts.map +1 -0
  9. package/dist/cli/doc-translate.js +1153 -0
  10. package/dist/cli/doc-translate.js.map +1 -0
  11. package/dist/cli/export-ui-xliff.d.ts +32 -0
  12. package/dist/cli/export-ui-xliff.d.ts.map +1 -0
  13. package/dist/cli/export-ui-xliff.js +153 -0
  14. package/dist/cli/export-ui-xliff.js.map +1 -0
  15. package/dist/cli/extract-strings.d.ts +12 -0
  16. package/dist/cli/extract-strings.d.ts.map +1 -0
  17. package/dist/cli/extract-strings.js +80 -0
  18. package/dist/cli/extract-strings.js.map +1 -0
  19. package/dist/cli/file-utils.d.ts +10 -0
  20. package/dist/cli/file-utils.d.ts.map +1 -0
  21. package/dist/cli/file-utils.js +77 -0
  22. package/dist/cli/file-utils.js.map +1 -0
  23. package/dist/cli/format.d.ts +21 -0
  24. package/dist/cli/format.d.ts.map +1 -0
  25. package/dist/cli/format.js +75 -0
  26. package/dist/cli/format.js.map +1 -0
  27. package/dist/cli/helpers.d.ts +27 -0
  28. package/dist/cli/helpers.d.ts.map +1 -0
  29. package/dist/cli/helpers.js +84 -0
  30. package/dist/cli/helpers.js.map +1 -0
  31. package/dist/cli/index.d.ts +3 -0
  32. package/dist/cli/index.d.ts.map +1 -0
  33. package/dist/cli/index.js +772 -0
  34. package/dist/cli/index.js.map +1 -0
  35. package/dist/cli/log-output.d.ts +13 -0
  36. package/dist/cli/log-output.d.ts.map +1 -0
  37. package/dist/cli/log-output.js +75 -0
  38. package/dist/cli/log-output.js.map +1 -0
  39. package/dist/cli/translate-svg.d.ts +7 -0
  40. package/dist/cli/translate-svg.d.ts.map +1 -0
  41. package/dist/cli/translate-svg.js +167 -0
  42. package/dist/cli/translate-svg.js.map +1 -0
  43. package/dist/cli/translate-ui-strings.d.ts +27 -0
  44. package/dist/cli/translate-ui-strings.d.ts.map +1 -0
  45. package/dist/cli/translate-ui-strings.js +357 -0
  46. package/dist/cli/translate-ui-strings.js.map +1 -0
  47. package/dist/core/cache-tracking-keys.d.ts +12 -0
  48. package/dist/core/cache-tracking-keys.d.ts.map +1 -0
  49. package/dist/core/cache-tracking-keys.js +20 -0
  50. package/dist/core/cache-tracking-keys.js.map +1 -0
  51. package/dist/core/cache.d.ts +153 -0
  52. package/dist/core/cache.d.ts.map +1 -0
  53. package/dist/core/cache.js +546 -0
  54. package/dist/core/cache.js.map +1 -0
  55. package/dist/core/config.d.ts +58 -0
  56. package/dist/core/config.d.ts.map +1 -0
  57. package/dist/core/config.js +392 -0
  58. package/dist/core/config.js.map +1 -0
  59. package/dist/core/doc-file-tracking.d.ts +8 -0
  60. package/dist/core/doc-file-tracking.d.ts.map +1 -0
  61. package/dist/core/doc-file-tracking.js +27 -0
  62. package/dist/core/doc-file-tracking.js.map +1 -0
  63. package/dist/core/errors.d.ts +19 -0
  64. package/dist/core/errors.d.ts.map +1 -0
  65. package/dist/core/errors.js +23 -0
  66. package/dist/core/errors.js.map +1 -0
  67. package/dist/core/locale-utils.d.ts +20 -0
  68. package/dist/core/locale-utils.d.ts.map +1 -0
  69. package/dist/core/locale-utils.js +75 -0
  70. package/dist/core/locale-utils.js.map +1 -0
  71. package/dist/core/output-paths.d.ts +22 -0
  72. package/dist/core/output-paths.d.ts.map +1 -0
  73. package/dist/core/output-paths.js +130 -0
  74. package/dist/core/output-paths.js.map +1 -0
  75. package/dist/core/prompt-builder.d.ts +62 -0
  76. package/dist/core/prompt-builder.d.ts.map +1 -0
  77. package/dist/core/prompt-builder.js +232 -0
  78. package/dist/core/prompt-builder.js.map +1 -0
  79. package/dist/core/prompts.d.ts +27 -0
  80. package/dist/core/prompts.d.ts.map +1 -0
  81. package/dist/core/prompts.js +57 -0
  82. package/dist/core/prompts.js.map +1 -0
  83. package/dist/core/svg-asset-paths.d.ts +40 -0
  84. package/dist/core/svg-asset-paths.d.ts.map +1 -0
  85. package/dist/core/svg-asset-paths.js +107 -0
  86. package/dist/core/svg-asset-paths.js.map +1 -0
  87. package/dist/core/types.d.ts +388 -0
  88. package/dist/core/types.d.ts.map +1 -0
  89. package/dist/core/types.js +265 -0
  90. package/dist/core/types.js.map +1 -0
  91. package/dist/core/ui-languages.d.ts +66 -0
  92. package/dist/core/ui-languages.d.ts.map +1 -0
  93. package/dist/core/ui-languages.js +277 -0
  94. package/dist/core/ui-languages.js.map +1 -0
  95. package/dist/core/user-edited-model.d.ts +3 -0
  96. package/dist/core/user-edited-model.d.ts.map +1 -0
  97. package/dist/core/user-edited-model.js +3 -0
  98. package/dist/core/user-edited-model.js.map +1 -0
  99. package/dist/edit-cache-app/app.js +1326 -0
  100. package/dist/edit-cache-app/index.html +287 -0
  101. package/dist/edit-cache-app/styles.css +664 -0
  102. package/dist/extractors/base-extractor.d.ts +15 -0
  103. package/dist/extractors/base-extractor.d.ts.map +1 -0
  104. package/dist/extractors/base-extractor.js +23 -0
  105. package/dist/extractors/base-extractor.js.map +1 -0
  106. package/dist/extractors/classify-segment.d.ts +6 -0
  107. package/dist/extractors/classify-segment.d.ts.map +1 -0
  108. package/dist/extractors/classify-segment.js +20 -0
  109. package/dist/extractors/classify-segment.js.map +1 -0
  110. package/dist/extractors/json-extractor.d.ts +16 -0
  111. package/dist/extractors/json-extractor.d.ts.map +1 -0
  112. package/dist/extractors/json-extractor.js +128 -0
  113. package/dist/extractors/json-extractor.js.map +1 -0
  114. package/dist/extractors/markdown-extractor.d.ts +15 -0
  115. package/dist/extractors/markdown-extractor.d.ts.map +1 -0
  116. package/dist/extractors/markdown-extractor.js +205 -0
  117. package/dist/extractors/markdown-extractor.js.map +1 -0
  118. package/dist/extractors/svg-extractor.d.ts +19 -0
  119. package/dist/extractors/svg-extractor.d.ts.map +1 -0
  120. package/dist/extractors/svg-extractor.js +132 -0
  121. package/dist/extractors/svg-extractor.js.map +1 -0
  122. package/dist/extractors/ui-string-extractor.d.ts +40 -0
  123. package/dist/extractors/ui-string-extractor.d.ts.map +1 -0
  124. package/dist/extractors/ui-string-extractor.js +146 -0
  125. package/dist/extractors/ui-string-extractor.js.map +1 -0
  126. package/dist/extractors/ui-string-locations.d.ts +23 -0
  127. package/dist/extractors/ui-string-locations.d.ts.map +1 -0
  128. package/dist/extractors/ui-string-locations.js +138 -0
  129. package/dist/extractors/ui-string-locations.js.map +1 -0
  130. package/dist/glossary/glossary.d.ts +34 -0
  131. package/dist/glossary/glossary.d.ts.map +1 -0
  132. package/dist/glossary/glossary.js +260 -0
  133. package/dist/glossary/glossary.js.map +1 -0
  134. package/dist/glossary/matcher.d.ts +10 -0
  135. package/dist/glossary/matcher.d.ts.map +1 -0
  136. package/dist/glossary/matcher.js +12 -0
  137. package/dist/glossary/matcher.js.map +1 -0
  138. package/dist/index.d.ts +44 -0
  139. package/dist/index.d.ts.map +1 -0
  140. package/dist/index.js +44 -0
  141. package/dist/index.js.map +1 -0
  142. package/dist/processors/admonition-placeholders.d.ts +8 -0
  143. package/dist/processors/admonition-placeholders.d.ts.map +1 -0
  144. package/dist/processors/admonition-placeholders.js +59 -0
  145. package/dist/processors/admonition-placeholders.js.map +1 -0
  146. package/dist/processors/anchor-placeholders.d.ts +8 -0
  147. package/dist/processors/anchor-placeholders.d.ts.map +1 -0
  148. package/dist/processors/anchor-placeholders.js +37 -0
  149. package/dist/processors/anchor-placeholders.js.map +1 -0
  150. package/dist/processors/batch-processor.d.ts +10 -0
  151. package/dist/processors/batch-processor.d.ts.map +1 -0
  152. package/dist/processors/batch-processor.js +33 -0
  153. package/dist/processors/batch-processor.js.map +1 -0
  154. package/dist/processors/bold-code-placeholders.d.ts +14 -0
  155. package/dist/processors/bold-code-placeholders.d.ts.map +1 -0
  156. package/dist/processors/bold-code-placeholders.js +116 -0
  157. package/dist/processors/bold-code-placeholders.js.map +1 -0
  158. package/dist/processors/doc-postprocess.d.ts +51 -0
  159. package/dist/processors/doc-postprocess.d.ts.map +1 -0
  160. package/dist/processors/doc-postprocess.js +215 -0
  161. package/dist/processors/doc-postprocess.js.map +1 -0
  162. package/dist/processors/emphasis-placeholders.d.ts +6 -0
  163. package/dist/processors/emphasis-placeholders.d.ts.map +1 -0
  164. package/dist/processors/emphasis-placeholders.js +262 -0
  165. package/dist/processors/emphasis-placeholders.js.map +1 -0
  166. package/dist/processors/flat-link-rewrite.d.ts +32 -0
  167. package/dist/processors/flat-link-rewrite.d.ts.map +1 -0
  168. package/dist/processors/flat-link-rewrite.js +90 -0
  169. package/dist/processors/flat-link-rewrite.js.map +1 -0
  170. package/dist/processors/glossary-force-placeholders.d.ts +12 -0
  171. package/dist/processors/glossary-force-placeholders.d.ts.map +1 -0
  172. package/dist/processors/glossary-force-placeholders.js +58 -0
  173. package/dist/processors/glossary-force-placeholders.js.map +1 -0
  174. package/dist/processors/inline-code-placeholders.d.ts +11 -0
  175. package/dist/processors/inline-code-placeholders.d.ts.map +1 -0
  176. package/dist/processors/inline-code-placeholders.js +87 -0
  177. package/dist/processors/inline-code-placeholders.js.map +1 -0
  178. package/dist/processors/placeholder-handler.d.ts +38 -0
  179. package/dist/processors/placeholder-handler.d.ts.map +1 -0
  180. package/dist/processors/placeholder-handler.js +55 -0
  181. package/dist/processors/placeholder-handler.js.map +1 -0
  182. package/dist/processors/translation-placeholder-leaks.d.ts +2 -0
  183. package/dist/processors/translation-placeholder-leaks.d.ts.map +1 -0
  184. package/dist/processors/translation-placeholder-leaks.js +9 -0
  185. package/dist/processors/translation-placeholder-leaks.js.map +1 -0
  186. package/dist/processors/url-placeholders.d.ts +10 -0
  187. package/dist/processors/url-placeholders.d.ts.map +1 -0
  188. package/dist/processors/url-placeholders.js +29 -0
  189. package/dist/processors/url-placeholders.js.map +1 -0
  190. package/dist/processors/validator.d.ts +23 -0
  191. package/dist/processors/validator.d.ts.map +1 -0
  192. package/dist/processors/validator.js +186 -0
  193. package/dist/processors/validator.js.map +1 -0
  194. package/dist/runtime/i18next-helpers.d.ts +146 -0
  195. package/dist/runtime/i18next-helpers.d.ts.map +1 -0
  196. package/dist/runtime/i18next-helpers.js +192 -0
  197. package/dist/runtime/i18next-helpers.js.map +1 -0
  198. package/dist/runtime/index.d.ts +4 -0
  199. package/dist/runtime/index.d.ts.map +1 -0
  200. package/dist/runtime/index.js +4 -0
  201. package/dist/runtime/index.js.map +1 -0
  202. package/dist/runtime/template.d.ts +21 -0
  203. package/dist/runtime/template.d.ts.map +1 -0
  204. package/dist/runtime/template.js +28 -0
  205. package/dist/runtime/template.js.map +1 -0
  206. package/dist/runtime/ui-language-display.d.ts +16 -0
  207. package/dist/runtime/ui-language-display.d.ts.map +1 -0
  208. package/dist/runtime/ui-language-display.js +26 -0
  209. package/dist/runtime/ui-language-display.js.map +1 -0
  210. package/dist/server/translation-editor.d.ts +25 -0
  211. package/dist/server/translation-editor.d.ts.map +1 -0
  212. package/dist/server/translation-editor.js +583 -0
  213. package/dist/server/translation-editor.js.map +1 -0
  214. package/dist/utils/concurrency.d.ts +31 -0
  215. package/dist/utils/concurrency.d.ts.map +1 -0
  216. package/dist/utils/concurrency.js +103 -0
  217. package/dist/utils/concurrency.js.map +1 -0
  218. package/dist/utils/hash.d.ts +5 -0
  219. package/dist/utils/hash.d.ts.map +1 -0
  220. package/dist/utils/hash.js +9 -0
  221. package/dist/utils/hash.js.map +1 -0
  222. package/dist/utils/ignore-parser.d.ts +7 -0
  223. package/dist/utils/ignore-parser.d.ts.map +1 -0
  224. package/dist/utils/ignore-parser.js +26 -0
  225. package/dist/utils/ignore-parser.js.map +1 -0
  226. package/dist/utils/logger.d.ts +45 -0
  227. package/dist/utils/logger.d.ts.map +1 -0
  228. package/dist/utils/logger.js +158 -0
  229. package/dist/utils/logger.js.map +1 -0
  230. package/docs/GETTING_STARTED.md +697 -0
  231. package/docs/PACKAGE_OVERVIEW.md +427 -0
  232. package/docs/ai-i18n-tools-context.md +481 -0
  233. package/package.json +117 -0
  234. package/translated-docs/README.de.md +157 -0
  235. package/translated-docs/README.es.md +157 -0
  236. package/translated-docs/README.fr.md +157 -0
  237. package/translated-docs/README.hi.md +157 -0
  238. package/translated-docs/README.ja.md +157 -0
  239. package/translated-docs/README.ko.md +157 -0
  240. package/translated-docs/README.pt-BR.md +157 -0
  241. package/translated-docs/README.zh-CN.md +157 -0
  242. package/translated-docs/README.zh-TW.md +157 -0
  243. package/translated-docs/docs/GETTING_STARTED.de.md +682 -0
  244. package/translated-docs/docs/GETTING_STARTED.es.md +682 -0
  245. package/translated-docs/docs/GETTING_STARTED.fr.md +682 -0
  246. package/translated-docs/docs/GETTING_STARTED.hi.md +682 -0
  247. package/translated-docs/docs/GETTING_STARTED.ja.md +682 -0
  248. package/translated-docs/docs/GETTING_STARTED.ko.md +682 -0
  249. package/translated-docs/docs/GETTING_STARTED.pt-BR.md +682 -0
  250. package/translated-docs/docs/GETTING_STARTED.zh-CN.md +682 -0
  251. package/translated-docs/docs/GETTING_STARTED.zh-TW.md +682 -0
  252. package/translated-docs/docs/PACKAGE_OVERVIEW.de.md +428 -0
  253. package/translated-docs/docs/PACKAGE_OVERVIEW.es.md +428 -0
  254. package/translated-docs/docs/PACKAGE_OVERVIEW.fr.md +428 -0
  255. package/translated-docs/docs/PACKAGE_OVERVIEW.hi.md +428 -0
  256. package/translated-docs/docs/PACKAGE_OVERVIEW.ja.md +428 -0
  257. package/translated-docs/docs/PACKAGE_OVERVIEW.ko.md +428 -0
  258. package/translated-docs/docs/PACKAGE_OVERVIEW.pt-BR.md +428 -0
  259. package/translated-docs/docs/PACKAGE_OVERVIEW.zh-CN.md +428 -0
  260. package/translated-docs/docs/PACKAGE_OVERVIEW.zh-TW.md +428 -0
@@ -0,0 +1,772 @@
1
+ #!/usr/bin/env node
2
+ import { Command, InvalidArgumentError } from "commander";
3
+ // node:sqlite emits an ExperimentalWarning on every startup even on Node 24+.
4
+ // The module is unflagged but not yet formally stable (nodejs/node#57445).
5
+ // Node.js installs its own 'warning' listener during bootstrap that writes to
6
+ // stderr unconditionally, so we must replace it rather than just add to it.
7
+ process.removeAllListeners("warning");
8
+ process.on("warning", (w) => {
9
+ if (w.name === "ExperimentalWarning" && w.message.includes("SQLite"))
10
+ return;
11
+ process.stderr.write(`${w.name}: ${w.message}\n`);
12
+ });
13
+ import express from "express";
14
+ import fs from "fs";
15
+ import path from "path";
16
+ import { fileURLToPath } from "node:url";
17
+ import http from "http";
18
+ import { exec, execFile } from "node:child_process";
19
+ import chalk from "chalk";
20
+ import { DEFAULT_CONFIG_FILENAME, writeInitConfigFile, toDocTranslateConfig, } from "../core/config.js";
21
+ import { documentationFileTrackingKey } from "../core/doc-file-tracking.js";
22
+ import { resolveCacheTrackingKeyToAbs } from "../core/cache-tracking-keys.js";
23
+ import { getDocumentationTargetLocaleCodes, resolveLocalesForDocumentation, resolveLocalesForSvg, resolveLocalesForUI, } from "../core/ui-languages.js";
24
+ import { loadConfigOrExit, resolveStringsJsonPath, hashFileContent, resolveTranslatedOutputPath, writeAtomicUtf8, } from "./helpers.js";
25
+ import { normalizeLocale } from "../core/config.js";
26
+ import { collectFilesByExtension, collectFilesRelativeToRoot } from "./file-utils.js";
27
+ import { loadTranslateIgnore, isIgnored } from "../utils/ignore-parser.js";
28
+ import { runExtract } from "./extract-strings.js";
29
+ import { runTranslate, shouldRunJson } from "./doc-translate.js";
30
+ import { runTranslateSvg } from "./translate-svg.js";
31
+ import { runTranslateUI } from "./translate-ui-strings.js";
32
+ import { runExportUIXliff } from "./export-ui-xliff.js";
33
+ import { TranslationCache } from "../core/cache.js";
34
+ import { setupLogOutput } from "./log-output.js";
35
+ import { stripAnsi } from "../utils/logger.js";
36
+ import { createTranslationEditorApp, resolveEditCacheStaticDir, } from "../server/translation-editor.js";
37
+ function openBrowser(url) {
38
+ const onErr = (err) => {
39
+ if (err)
40
+ console.warn(`[editor] Failed to open browser: ${err.message}`);
41
+ };
42
+ if (process.platform === "darwin") {
43
+ execFile("open", [url], onErr);
44
+ }
45
+ else if (process.platform === "win32") {
46
+ execFile("cmd", ["/c", "start", "", url], { windowsHide: true }, onErr);
47
+ }
48
+ else {
49
+ const browser = process.env.BROWSER;
50
+ if (browser) {
51
+ exec(`${browser} ${JSON.stringify(url)}`, onErr);
52
+ }
53
+ else {
54
+ execFile("xdg-open", [url], onErr);
55
+ }
56
+ }
57
+ }
58
+ const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
59
+ let version = "0.0.0";
60
+ try {
61
+ const raw = fs.readFileSync(pkgPath, "utf8");
62
+ const pkg = JSON.parse(raw);
63
+ if (pkg.version) {
64
+ version = pkg.version;
65
+ }
66
+ }
67
+ catch {
68
+ /* keep default */
69
+ }
70
+ function filterIgnored(files, cwd) {
71
+ const ig = loadTranslateIgnore(".translate-ignore", cwd);
72
+ return files.filter((f) => !isIgnored(ig, path.join(cwd, f), cwd));
73
+ }
74
+ function withConfig(cmd) {
75
+ const o = cmd.optsWithGlobals();
76
+ return { configFlag: o.config, cwd: process.cwd() };
77
+ }
78
+ const program = new Command();
79
+ program
80
+ .name("ai-i18n-tools")
81
+ .description(`Unified i18n toolkit for Node.js apps and documentation with AI translation (v${version})`)
82
+ .version(version)
83
+ .option("-c, --config <path>", "Config file path", DEFAULT_CONFIG_FILENAME)
84
+ .option("-v, --verbose", "Verbose logging", false)
85
+ .option("-w, --write-logs [path]", "Tee console output to a .log file (default path: under cacheDir)");
86
+ program
87
+ .command("init")
88
+ .description("Write starter ai-i18n-tools JSON config")
89
+ .option("-o, --output <path>", "config file path", DEFAULT_CONFIG_FILENAME)
90
+ .option("-t, --template <name>", "ui-markdown (UI + app markdown) | ui-docusaurus (UI + Docusaurus docs)", "ui-markdown")
91
+ .option("--with-translate-ignore", "Create a starter .translate-ignore", false)
92
+ .action((opts) => {
93
+ const t = opts.template.toLowerCase();
94
+ if (t !== "ui-markdown" && t !== "ui-docusaurus") {
95
+ console.error('Template must be "ui-markdown" or "ui-docusaurus".');
96
+ process.exitCode = 1;
97
+ return;
98
+ }
99
+ const key = t === "ui-markdown" ? "uiMarkdown" : "uiDocusaurus";
100
+ writeInitConfigFile(opts.output, key);
101
+ console.log(`Wrote ${opts.output} (${key})`);
102
+ if (opts.withTranslateIgnore) {
103
+ const ignorePath = path.join(process.cwd(), ".translate-ignore");
104
+ if (!fs.existsSync(ignorePath)) {
105
+ fs.writeFileSync(ignorePath, ["node_modules/", ".git/", "*.min.js", "dist/", ""].join("\n"), "utf8");
106
+ console.log("Wrote .translate-ignore");
107
+ }
108
+ }
109
+ });
110
+ program
111
+ .command("extract")
112
+ .description("Extract UI strings (t(...) / i18next-scanner) to strings.json")
113
+ .action(async (_opts, cmd) => {
114
+ const { configFlag, cwd } = withConfig(cmd);
115
+ const { config, projectRoot } = loadConfigOrExit(configFlag, cwd);
116
+ if (!config.features.extractUIStrings) {
117
+ console.error("[extract] Enable features.extractUIStrings in config.");
118
+ process.exit(1);
119
+ }
120
+ try {
121
+ const s = runExtract(config, projectRoot);
122
+ console.log(chalk.green(`✅ Extracted ${s.found} strings (${s.added} new, ${s.updated} updated) → ${s.outPath}`));
123
+ }
124
+ catch (e) {
125
+ console.error(chalk.red(`❌ [extract] ${e instanceof Error ? e.message : String(e)}`));
126
+ process.exit(1);
127
+ }
128
+ });
129
+ function parsePositiveInt(optionLabel, value) {
130
+ const n = Number.parseInt(value, 10);
131
+ if (!Number.isFinite(n) || n < 1) {
132
+ throw new InvalidArgumentError(`${optionLabel} must be a positive integer (got "${value}")`);
133
+ }
134
+ return n;
135
+ }
136
+ function activateWriteLogs(writeLogs, cacheDir, prefix) {
137
+ if (!writeLogs)
138
+ return undefined;
139
+ const explicitPath = typeof writeLogs === "string" ? writeLogs : undefined;
140
+ const { logPath } = setupLogOutput({ cacheDir, prefix, logPath: explicitPath });
141
+ return logPath;
142
+ }
143
+ function buildTranslateOpts(cmd, config, projectRoot, logPath) {
144
+ const g = cmd.optsWithGlobals();
145
+ const o = cmd.opts();
146
+ const locales = resolveLocalesForDocumentation(config, projectRoot, o.locale ?? null);
147
+ const uiLocales = resolveLocalesForUI(config, projectRoot, o.locale ?? null);
148
+ const translateOpts = {
149
+ cwd: projectRoot,
150
+ locales,
151
+ dryRun: Boolean(o.dryRun),
152
+ force: Boolean(o.force),
153
+ forceUpdate: Boolean(o.forceUpdate),
154
+ noCache: Boolean(o.noCache),
155
+ verbose: Boolean(g.verbose),
156
+ pathFilter: o.path,
157
+ typeFilter: o.type,
158
+ jsonOnly: Boolean(o.jsonOnly),
159
+ noJson: Boolean(o.noJson),
160
+ logPath,
161
+ concurrency: o.concurrency !== undefined ? parsePositiveInt("Concurrency (-j)", o.concurrency) : undefined,
162
+ batchConcurrency: o.batchConcurrency !== undefined
163
+ ? parsePositiveInt("Batch concurrency (-b)", o.batchConcurrency)
164
+ : undefined,
165
+ promptFormat: parseTranslatePromptFormat(o.promptFormat),
166
+ };
167
+ return { locales, uiLocales, translateOpts };
168
+ }
169
+ function parseTranslatePromptFormat(raw) {
170
+ if (raw === undefined || raw === "") {
171
+ return "xml";
172
+ }
173
+ if (raw === "xml" || raw === "json-array" || raw === "json-object") {
174
+ return raw;
175
+ }
176
+ throw new InvalidArgumentError(`Invalid --prompt-format: ${raw} (expected xml, json-array, or json-object)`);
177
+ }
178
+ /** Same as `sync --force-update` (extract + UI + SVG + docs with force-update semantics). */
179
+ function buildCleanupSyncTranslateOpts(config, projectRoot, logPath, g, dryRun) {
180
+ const locales = resolveLocalesForDocumentation(config, projectRoot, null);
181
+ const uiLocales = resolveLocalesForUI(config, projectRoot, null);
182
+ const translateOpts = {
183
+ cwd: projectRoot,
184
+ locales,
185
+ dryRun,
186
+ force: false,
187
+ forceUpdate: true,
188
+ noCache: false,
189
+ verbose: Boolean(g.verbose),
190
+ pathFilter: undefined,
191
+ typeFilter: undefined,
192
+ jsonOnly: false,
193
+ noJson: false,
194
+ logPath,
195
+ concurrency: undefined,
196
+ batchConcurrency: undefined,
197
+ promptFormat: "xml",
198
+ };
199
+ return { uiLocales, translateOpts };
200
+ }
201
+ async function runSyncPipeline(args) {
202
+ const { config, projectRoot, uiLocales, svgLocales, translateOpts, noUi, noSvg, noDocs } = args;
203
+ if (config.features.extractUIStrings) {
204
+ try {
205
+ const s = runExtract(config, projectRoot);
206
+ console.log(chalk.green(`✅ Extracted ${s.found} strings (${s.added} new, ${s.updated} updated) → ${s.outPath}`));
207
+ }
208
+ catch (e) {
209
+ console.error(chalk.red(`❌ [sync][extract] ${e instanceof Error ? e.message : String(e)}`));
210
+ throw e;
211
+ }
212
+ }
213
+ if (config.features.translateUIStrings && !noUi) {
214
+ try {
215
+ await runTranslateUI(config, {
216
+ cwd: projectRoot,
217
+ locales: uiLocales,
218
+ force: translateOpts.force,
219
+ dryRun: translateOpts.dryRun,
220
+ verbose: translateOpts.verbose,
221
+ logPath: translateOpts.logPath,
222
+ concurrency: translateOpts.concurrency,
223
+ });
224
+ }
225
+ catch (e) {
226
+ console.error(chalk.red(`❌ [sync][ui] ${e instanceof Error ? e.message : String(e)}`));
227
+ throw e;
228
+ }
229
+ }
230
+ if (config.features.translateSVG && config.svg && !noSvg) {
231
+ try {
232
+ const svgOpts = {
233
+ ...translateOpts,
234
+ locales: svgLocales,
235
+ };
236
+ await runTranslateSvg(config, svgOpts);
237
+ }
238
+ catch (e) {
239
+ console.error(chalk.red(`❌ [sync][svg] ${e instanceof Error ? e.message : String(e)}`));
240
+ throw e;
241
+ }
242
+ }
243
+ if (!noDocs) {
244
+ try {
245
+ for (let bi = 0; bi < config.documentations.length; bi++) {
246
+ const block = config.documentations[bi];
247
+ const view = toDocTranslateConfig(config, block);
248
+ const md = filterIgnored(collectFilesByExtension(block.contentPaths, [".md", ".mdx"], projectRoot), projectRoot);
249
+ const jsonRoot = block.jsonSource
250
+ ? path.resolve(projectRoot, block.jsonSource)
251
+ : path.resolve(projectRoot, ".");
252
+ const jsonFiles = block.jsonSource?.trim() && shouldRunJson(translateOpts, view)
253
+ ? collectFilesRelativeToRoot(jsonRoot, [".json"])
254
+ : [];
255
+ if (md.length === 0 && jsonFiles.length === 0) {
256
+ continue;
257
+ }
258
+ await runTranslate(view, { ...translateOpts, documentationBlockIndex: bi }, { markdown: md, json: jsonFiles }, jsonRoot);
259
+ }
260
+ }
261
+ catch (e) {
262
+ console.error(chalk.red(`❌ [sync][translate] ${e instanceof Error ? e.message : String(e)}`));
263
+ throw e;
264
+ }
265
+ }
266
+ }
267
+ program
268
+ .command("translate-docs")
269
+ .description("Translate documentation files (markdown, JSON) per config features")
270
+ .option("-l, --locale <codes>", "Target locales (comma-separated); default: documentation.targetLocales if set, else targetLocales")
271
+ .option("-p, --path <path>", "Limit to file or directory prefix")
272
+ .option("--dry-run", "No writes, no API calls", false)
273
+ .option("--force", "Re-translate: clear file tracking for each file and ignore segment cache (not combinable with --force-update)", false)
274
+ .option("--force-update", "Re-process files even when file tracking matches; still use segment cache (not combinable with --force)", false)
275
+ .option("--stats", "Show cache statistics and exit", false)
276
+ .option("--clear-cache [locale]", "Clear translation cache (all locales, or one locale)")
277
+ .option("--type <kind>", "markdown | json")
278
+ .option("--json-only", "JSON only", false)
279
+ .option("--no-json", "Skip JSON", false)
280
+ .option("-j, --concurrency <n>", "Max parallel target locales (default: config or 3)")
281
+ .option("-b, --batch-concurrency <n>", "Max parallel batch API calls per file (default: config or 4)")
282
+ .option("--prompt-format <mode>", "Batch segment prompt/response: xml (<seg>/<t>), json-array, or json-object", "xml")
283
+ .action(async (_a, cmd) => {
284
+ const { configFlag, cwd } = withConfig(cmd);
285
+ const { config, projectRoot } = loadConfigOrExit(configFlag, cwd);
286
+ const g = cmd.optsWithGlobals();
287
+ const cacheDir = path.join(projectRoot, config.cacheDir);
288
+ const raw = cmd.opts();
289
+ if (raw.force && raw.forceUpdate) {
290
+ console.error(chalk.red(`\n❌ Use either --force or --force-update, not both.\n` +
291
+ ` --force: ignore segment cache and clear file tracking for processed files.\n` +
292
+ ` --force-update: re-run outputs when file tracking would skip; segment cache still applies.\n`));
293
+ cmd.outputHelp();
294
+ process.exit(1);
295
+ }
296
+ if (raw.stats) {
297
+ const cache = new TranslationCache(cacheDir);
298
+ const cacheStats = cache.getStats();
299
+ console.log(chalk.bold("\n📊 Cache statistics:"));
300
+ console.log(` Cached segments: ${cacheStats.totalSegments}`);
301
+ console.log(` Tracked files: ${cacheStats.totalFiles}`);
302
+ console.log(` By locale:`);
303
+ for (const [loc, count] of Object.entries(cacheStats.byLocale)) {
304
+ console.log(` - ${loc}: ${count}`);
305
+ }
306
+ cache.close();
307
+ return;
308
+ }
309
+ if (raw.clearCache !== undefined) {
310
+ const cache = new TranslationCache(cacheDir);
311
+ let locale;
312
+ if (typeof raw.clearCache === "string" && raw.clearCache.trim() !== "") {
313
+ locale = normalizeLocale(raw.clearCache);
314
+ const allowed = new Set(getDocumentationTargetLocaleCodes(config).map((c) => normalizeLocale(c)));
315
+ if (allowed.size > 0 && !allowed.has(locale)) {
316
+ console.error(chalk.red(`\n❌ Locale "${locale}" is not in documentation target locales.\n` +
317
+ ` Configured: ${[...allowed].join(", ")}\n`));
318
+ cache.close();
319
+ process.exit(1);
320
+ }
321
+ }
322
+ cache.clear(locale);
323
+ console.log(chalk.blue(`✅ Cache cleared${locale ? ` for ${locale}` : ""}`));
324
+ cache.close();
325
+ return;
326
+ }
327
+ const logPath = activateWriteLogs(g.writeLogs, cacheDir, "translate-docs");
328
+ const { translateOpts } = buildTranslateOpts(cmd, config, projectRoot, logPath);
329
+ if (config.features.translateJSON && !config.documentations.some((b) => b.jsonSource?.trim())) {
330
+ console.warn(chalk.yellow("\n⚠️ translateJSON is enabled but no documentations[].jsonSource is set. " +
331
+ "JSON translation (e.g. Docusaurus UI strings from write-translations) is skipped. " +
332
+ 'Set jsonSource on at least one block to your default-locale catalog (e.g. "docs-site/i18n/en").\n'));
333
+ }
334
+ let totalSkipped = 0;
335
+ let totalWritten = 0;
336
+ try {
337
+ for (let bi = 0; bi < config.documentations.length; bi++) {
338
+ const block = config.documentations[bi];
339
+ const view = toDocTranslateConfig(config, block);
340
+ const md = filterIgnored(collectFilesByExtension(block.contentPaths, [".md", ".mdx"], projectRoot), projectRoot);
341
+ const jsonRoot = block.jsonSource
342
+ ? path.resolve(projectRoot, block.jsonSource)
343
+ : path.resolve(projectRoot, ".");
344
+ const jsonFiles = block.jsonSource?.trim() && shouldRunJson(translateOpts, view)
345
+ ? collectFilesRelativeToRoot(jsonRoot, [".json"])
346
+ : [];
347
+ if (md.length === 0 && jsonFiles.length === 0) {
348
+ continue;
349
+ }
350
+ const desc = typeof block.description === "string" && block.description.trim()
351
+ ? ` — ${block.description.trim()}`
352
+ : "";
353
+ console.log(chalk.gray(`\n--- documentations[${bi}]${desc} → ${path.resolve(projectRoot, block.outputDir)} (${md.length} md, ${jsonFiles.length} json) ---\n`));
354
+ const sum = await runTranslate(view, { ...translateOpts, documentationBlockIndex: bi }, { markdown: md, json: jsonFiles }, jsonRoot);
355
+ totalSkipped += sum.filesSkipped;
356
+ totalWritten += sum.filesWritten;
357
+ }
358
+ if (totalSkipped > 0 &&
359
+ totalWritten === 0 &&
360
+ !translateOpts.dryRun &&
361
+ !translateOpts.force &&
362
+ !translateOpts.forceUpdate) {
363
+ console.log(chalk.cyan(`💡 All files were skipped (cache matches output). Use --force-update to re-process files (using translationcache) or --force to retranslate files`));
364
+ }
365
+ }
366
+ catch (e) {
367
+ console.error(chalk.red(`❌ [translate-docs] ${e instanceof Error ? e.message : String(e)}`));
368
+ process.exit(1);
369
+ }
370
+ });
371
+ program
372
+ .command("translate-svg")
373
+ .description("Translate standalone SVG assets per config.svg (requires features.translateSVG)")
374
+ .option("-l, --locale <codes>", "Target locales (comma-separated); default: documentation.targetLocales if set, else targetLocales")
375
+ .option("-p, --path <path>", "Limit to file or directory prefix")
376
+ .option("--dry-run", "No writes, no API calls", false)
377
+ .option("--force", "Re-translate: clear file tracking for each file and ignore segment cache (not combinable with --force-update)", false)
378
+ .option("--force-update", "Re-process files even when file tracking matches; still use segment cache (not combinable with --force)", false)
379
+ .option("--no-cache", "Bypass SQLite cache", false)
380
+ .option("-j, --concurrency <n>", "Max parallel target locales (default: config or 3)")
381
+ .option("-b, --batch-concurrency <n>", "Max parallel batch API calls per file (default: config or 4)")
382
+ .action(async (_a, cmd) => {
383
+ const { configFlag, cwd } = withConfig(cmd);
384
+ const { config, projectRoot } = loadConfigOrExit(configFlag, cwd);
385
+ const g = cmd.optsWithGlobals();
386
+ const cacheDir = path.join(projectRoot, config.cacheDir);
387
+ const raw = cmd.opts();
388
+ if (raw.force && raw.forceUpdate) {
389
+ console.error(chalk.red(`\n❌ Use either --force or --force-update, not both.\n` +
390
+ ` --force: ignore segment cache and clear file tracking for processed files.\n` +
391
+ ` --force-update: re-run outputs when file tracking would skip; segment cache still applies.\n`));
392
+ cmd.outputHelp();
393
+ process.exit(1);
394
+ }
395
+ const logPath = activateWriteLogs(g.writeLogs, cacheDir, "translate-svg");
396
+ const { translateOpts } = buildTranslateOpts(cmd, config, projectRoot, logPath);
397
+ const localeOpt = cmd.opts();
398
+ translateOpts.locales = resolveLocalesForSvg(config, projectRoot, localeOpt.locale ?? null);
399
+ if (!config.features.translateSVG) {
400
+ console.error(chalk.red("❌ [translate-svg] Enable features.translateSVG in config."));
401
+ process.exit(1);
402
+ }
403
+ try {
404
+ await runTranslateSvg(config, translateOpts);
405
+ }
406
+ catch (e) {
407
+ console.error(chalk.red(`❌ [translate-svg] ${e instanceof Error ? e.message : String(e)}`));
408
+ process.exit(1);
409
+ }
410
+ });
411
+ program
412
+ .command("translate-ui")
413
+ .description("Translate UI strings (strings.json → locale JSON via OpenRouter)")
414
+ .option("-l, --locale <codes>", "Target locales (comma-separated); default: ui-languages.json or config.targetLocales")
415
+ .option("--dry-run", "No writes, no API calls", false)
416
+ .option("--force", "Re-translate all entries per locale", false)
417
+ .option("-j, --concurrency <n>", "Max parallel target locales (default: config or 4)")
418
+ .action(async (_a, cmd) => {
419
+ const { configFlag, cwd } = withConfig(cmd);
420
+ const { config, projectRoot } = loadConfigOrExit(configFlag, cwd);
421
+ const g = cmd.optsWithGlobals();
422
+ const o = cmd.opts();
423
+ const locales = resolveLocalesForUI(config, projectRoot, o.locale ?? null);
424
+ if (!config.features.translateUIStrings) {
425
+ console.error(chalk.red("❌ [translate-ui] Enable features.translateUIStrings in config."));
426
+ process.exit(1);
427
+ }
428
+ const cacheDir = path.join(projectRoot, config.cacheDir);
429
+ const logPath = activateWriteLogs(g.writeLogs, cacheDir, "translate-ui");
430
+ try {
431
+ await runTranslateUI(config, {
432
+ cwd: projectRoot,
433
+ locales,
434
+ force: Boolean(o.force),
435
+ dryRun: Boolean(o.dryRun),
436
+ verbose: Boolean(g.verbose),
437
+ logPath,
438
+ concurrency: o.concurrency !== undefined
439
+ ? parsePositiveInt("Concurrency (-j)", o.concurrency)
440
+ : undefined,
441
+ });
442
+ }
443
+ catch (e) {
444
+ console.error(chalk.red(`❌ [translate-ui] ${e instanceof Error ? e.message : String(e)}`));
445
+ process.exit(1);
446
+ }
447
+ });
448
+ program
449
+ .command("export-ui-xliff")
450
+ .description("Export UI strings from strings.json to XLIFF 2.0 (one .xliff file per target locale)")
451
+ .option("-l, --locale <codes>", "Target locales (comma-separated); default: ui-languages.json or config.targetLocales")
452
+ .option("-o, --output-dir <path>", "Output directory for .xliff files (default: same directory as strings.json)")
453
+ .option("--untranslated-only", "Include only units that have no translation for the target locale", false)
454
+ .option("--dry-run", "Print paths that would be written without writing files", false)
455
+ .action((_a, cmd) => {
456
+ const { configFlag, cwd } = withConfig(cmd);
457
+ const { config, projectRoot } = loadConfigOrExit(configFlag, cwd);
458
+ const o = cmd.opts();
459
+ try {
460
+ runExportUIXliff(config, {
461
+ cwd: projectRoot,
462
+ locales: o.locale ?? null,
463
+ outputDir: o.outputDir,
464
+ untranslatedOnly: Boolean(o.untranslatedOnly),
465
+ dryRun: Boolean(o.dryRun),
466
+ });
467
+ }
468
+ catch (e) {
469
+ console.error(chalk.red(`❌ [export-ui-xliff] ${e instanceof Error ? e.message : String(e)}`));
470
+ process.exit(1);
471
+ }
472
+ });
473
+ program
474
+ .command("sync")
475
+ .description("Extract UI strings (if enabled), then translate UI / SVG (if features.translateSVG + svg) / docs unless skipped with --no-*")
476
+ .option("-l, --locale <codes>", "Target locales for translate step")
477
+ .option("-p, --path <path>", "Limit translate to path prefix")
478
+ .option("--dry-run", "No writes / no API", false)
479
+ .option("--force", "Docs: clear file tracking and ignore segment cache (not combinable with --force-update)", false)
480
+ .option("--force-update", "Docs: re-process even when file tracking matches; segment cache still applies", false)
481
+ .option("--no-ui", "Skip UI strings translation", false)
482
+ .option("--no-svg", "Skip standalone SVG translation (when features.translateSVG and config.svg)", false)
483
+ .option("--no-docs", "Skip markdown/JSON documentation translation", false)
484
+ .option("-j, --concurrency <n>", "Max parallel target locales for translate steps (default: config)")
485
+ .option("-b, --batch-concurrency <n>", "Max parallel batch API calls per file for docs (default: config)")
486
+ .action(async (_a, cmd) => {
487
+ const { configFlag, cwd } = withConfig(cmd);
488
+ const { config, projectRoot } = loadConfigOrExit(configFlag, cwd);
489
+ const syncOpts = cmd.opts();
490
+ const noDocs = Boolean(syncOpts.noDocs);
491
+ if (!noDocs && syncOpts.force && syncOpts.forceUpdate) {
492
+ console.error(chalk.red(`\n❌ Use either --force or --force-update, not both.\n` +
493
+ ` --force: docs ignore segment cache and clear file tracking.\n` +
494
+ ` --force-update: docs re-run when file tracking would skip; segment cache still applies.\n`));
495
+ cmd.outputHelp();
496
+ process.exit(1);
497
+ }
498
+ const noUi = Boolean(syncOpts.noUi);
499
+ const noSvg = Boolean(syncOpts.noSvg);
500
+ const g = cmd.optsWithGlobals();
501
+ const cacheDir = path.join(projectRoot, config.cacheDir);
502
+ const logPath = activateWriteLogs(g.writeLogs, cacheDir, "sync");
503
+ const { uiLocales, translateOpts } = buildTranslateOpts(cmd, config, projectRoot, logPath);
504
+ const localeOpt = cmd.opts();
505
+ const svgLocales = resolveLocalesForSvg(config, projectRoot, localeOpt.locale ?? null);
506
+ try {
507
+ await runSyncPipeline({
508
+ config,
509
+ projectRoot,
510
+ uiLocales,
511
+ svgLocales,
512
+ translateOpts,
513
+ noUi,
514
+ noSvg,
515
+ noDocs,
516
+ });
517
+ }
518
+ catch {
519
+ process.exit(1);
520
+ }
521
+ });
522
+ program
523
+ .command("status")
524
+ .description("Show markdown translation status vs cache / output files")
525
+ .action((_a, cmd) => {
526
+ const { configFlag, cwd } = withConfig(cmd);
527
+ const { config, projectRoot } = loadConfigOrExit(configFlag, cwd);
528
+ const cache = new TranslationCache(path.join(projectRoot, config.cacheDir));
529
+ const locales = getDocumentationTargetLocaleCodes(config);
530
+ const headers = ["File", ...locales];
531
+ const padVis = (s, width) => {
532
+ const visLen = stripAnsi(s).length;
533
+ return s + " ".repeat(Math.max(0, width - visLen));
534
+ };
535
+ const printTable = (rows) => {
536
+ const colW = Math.max(12, ...rows.map((r) => stripAnsi(r[0]).length), 8);
537
+ const localeColW = Math.max(4, ...locales.map((l) => l.length), ...rows.flatMap((r) => r.slice(1).map((c) => stripAnsi(String(c)).length)));
538
+ const sep = (cols) => cols.join(" | ");
539
+ console.log(sep(headers.map((h, i) => i === 0 ? padVis(chalk.bold(h), colW) : padVis(chalk.bold(h), localeColW))));
540
+ console.log(sep(headers.map((_, i) => i === 0 ? chalk.bold("-".repeat(colW)) : chalk.bold("-".repeat(localeColW)))));
541
+ for (const r of rows) {
542
+ console.log(sep(r.map((c, i) => (i === 0 ? padVis(String(c), colW) : padVis(String(c), localeColW)))));
543
+ }
544
+ console.log();
545
+ };
546
+ if (config.features.translateUIStrings) {
547
+ const stringsPath = resolveStringsJsonPath(config, projectRoot);
548
+ let stringsData = {};
549
+ try {
550
+ stringsData = JSON.parse(fs.readFileSync(stringsPath, "utf8"));
551
+ }
552
+ catch {
553
+ console.log(chalk.red("\n⚠ Could not read strings.json: " + stringsPath));
554
+ }
555
+ const keys = Object.keys(stringsData);
556
+ const total = keys.length;
557
+ if (total > 0) {
558
+ const uiLocales = resolveLocalesForUI(config, projectRoot);
559
+ const uiHeaders = ["", ...uiLocales];
560
+ const pct = (n) => Math.round((n / total) * 100);
561
+ const translatedRow = [chalk.bold("Translated")];
562
+ const missingRow = [chalk.bold("Missing")];
563
+ const totalRow = [chalk.bold("Total")];
564
+ for (const loc of uiLocales) {
565
+ const translated = keys.filter((k) => stringsData[k]?.translated?.[loc]?.trim()).length;
566
+ const missing = total - translated;
567
+ translatedRow.push(chalk.green(String(translated)) + " " + chalk.gray(`${pct(translated)}%`));
568
+ missingRow.push(missing > 0
569
+ ? chalk.yellow(String(missing)) + " " + chalk.gray(`${pct(missing)}%`)
570
+ : chalk.green(String(missing)) + " " + chalk.gray(`${pct(missing)}%`));
571
+ totalRow.push(String(total));
572
+ }
573
+ const uiRows = [translatedRow, missingRow, totalRow];
574
+ const labelW = Math.max(12, ...uiRows.map((r) => stripAnsi(r[0]).length));
575
+ const uiLocColW = Math.max(4, ...uiLocales.map((l) => l.length), ...uiRows.flatMap((r) => r.slice(1).map((c) => stripAnsi(c).length)));
576
+ const uiSep = (cols) => cols.join(" | ");
577
+ console.log(chalk.bold.cyan("\n📊 UI strings status"));
578
+ console.log(chalk.gray(`(${stringsPath})\n`));
579
+ console.log(uiSep(uiHeaders.map((h, i) => i === 0 ? padVis(chalk.bold(h), labelW) : padVis(chalk.bold(h), uiLocColW))));
580
+ console.log(uiSep(uiHeaders.map((_, i) => (i === 0 ? "-".repeat(labelW) : "-".repeat(uiLocColW)))));
581
+ for (const r of uiRows) {
582
+ console.log(uiSep(r.map((c, i) => (i === 0 ? padVis(c, labelW) : padVis(c, uiLocColW)))));
583
+ }
584
+ console.log();
585
+ }
586
+ }
587
+ console.log(chalk.bold.cyan("\n📊 Translation status (markdown)"));
588
+ console.log(chalk.gray("Legend: ") +
589
+ chalk.green("✓") +
590
+ chalk.gray(" up to date ") +
591
+ chalk.yellow("●") +
592
+ chalk.gray(" stale or missing ") +
593
+ chalk.gray("-") +
594
+ chalk.gray(" not generated ") +
595
+ chalk.red("?") +
596
+ chalk.gray(" source read error"));
597
+ for (let bi = 0; bi < config.documentations.length; bi++) {
598
+ const block = config.documentations[bi];
599
+ const view = toDocTranslateConfig(config, block);
600
+ const md = filterIgnored(collectFilesByExtension(block.contentPaths, [".md", ".mdx"], projectRoot), projectRoot);
601
+ if (md.length === 0) {
602
+ continue;
603
+ }
604
+ const desc = typeof block.description === "string" && block.description.trim()
605
+ ? ` — ${block.description.trim()}`
606
+ : "";
607
+ if (config.documentations.length > 1 || desc) {
608
+ console.log("\n" +
609
+ chalk.bold(`documentations[${bi}]`) +
610
+ chalk.cyan(`${desc} `) +
611
+ chalk.magenta(`(${block.outputDir})`) +
612
+ "\n");
613
+ }
614
+ const rows = [];
615
+ for (const rel of md) {
616
+ const abs = path.join(projectRoot, rel);
617
+ const trackKey = documentationFileTrackingKey(bi, rel);
618
+ let srcHash = "";
619
+ try {
620
+ srcHash = hashFileContent(fs.readFileSync(abs, "utf8"));
621
+ }
622
+ catch {
623
+ rows.push([rel, ...locales.map(() => chalk.red("?"))]);
624
+ continue;
625
+ }
626
+ const cells = locales.map((loc) => {
627
+ const out = resolveTranslatedOutputPath(view, projectRoot, loc, rel, "markdown");
628
+ const tracked = cache.getFileHash(trackKey, loc);
629
+ if (!fs.existsSync(out)) {
630
+ return chalk.gray("-");
631
+ }
632
+ if (tracked === srcHash) {
633
+ return chalk.green("✓");
634
+ }
635
+ return chalk.yellow("●");
636
+ });
637
+ rows.push([rel, ...cells]);
638
+ }
639
+ printTable(rows);
640
+ }
641
+ cache.close();
642
+ });
643
+ program
644
+ .command("cleanup")
645
+ .description("Run sync --force-update (extract, UI, SVG, docs), then clean stale segment rows (null last_hit_at / empty filepath); remove orphaned file_tracking keys and translation rows when resolved paths are missing on disk (SQLite)")
646
+ .option("--dry-run", "Show only", false)
647
+ .option("--no-backup", "Skip SQLite backup before modifications", false)
648
+ .option("--backup <path>", "SQLite backup path (default: timestamped file under documentation cacheDir)")
649
+ .action(async (opts, cmd) => {
650
+ const { configFlag, cwd } = withConfig(cmd);
651
+ const { config: loaded, projectRoot } = loadConfigOrExit(configFlag, cwd);
652
+ const g = cmd.optsWithGlobals();
653
+ const cacheDir = path.join(projectRoot, loaded.cacheDir);
654
+ const logPath = activateWriteLogs(g.writeLogs, cacheDir, "cleanup");
655
+ const { uiLocales, translateOpts } = buildCleanupSyncTranslateOpts(loaded, projectRoot, logPath, g, Boolean(opts.dryRun));
656
+ const svgLocales = resolveLocalesForSvg(loaded, projectRoot, null);
657
+ console.log(chalk.cyan("[cleanup] Running sync --force-update first…"));
658
+ try {
659
+ await runSyncPipeline({
660
+ config: loaded,
661
+ projectRoot,
662
+ uiLocales,
663
+ svgLocales,
664
+ translateOpts,
665
+ noUi: false,
666
+ noSvg: false,
667
+ noDocs: false,
668
+ });
669
+ }
670
+ catch {
671
+ process.exit(1);
672
+ }
673
+ const cache = new TranslationCache(cacheDir);
674
+ const shouldBackup = !opts.dryRun && !opts.noBackup;
675
+ if (shouldBackup) {
676
+ const backupPath = opts.backup
677
+ ? path.resolve(cwd, opts.backup)
678
+ : path.join(cacheDir, `cache.db.backup.${new Date().toISOString().replace(/[:.]/g, "-")}.sqlite`);
679
+ await cache.backupTo(backupPath);
680
+ console.log(`[cleanup] Backup → ${backupPath}`);
681
+ }
682
+ const { count, deletedRows } = cache.cleanupStaleTranslations(Boolean(opts.dryRun));
683
+ console.log(`[cleanup] stale: ${count} row(s)${opts.dryRun ? " (dry-run)" : ""}`);
684
+ if (opts.dryRun && deletedRows.length) {
685
+ console.log(deletedRows
686
+ .slice(0, 20)
687
+ .map((r) => ` ${r.source_hash} ${r.locale}`)
688
+ .join("\n"));
689
+ }
690
+ const prunedTracking = cache.pruneOrphanedFileTrackingByDisk(projectRoot, Boolean(opts.dryRun));
691
+ console.log(`[cleanup] orphaned file_tracking (missing on disk): ${prunedTracking}${opts.dryRun ? " (dry-run)" : ""}`);
692
+ let orphanTranslations = 0;
693
+ for (const fp of cache.getUniqueFilepaths()) {
694
+ const abs = resolveCacheTrackingKeyToAbs(projectRoot, fp);
695
+ if (!fs.existsSync(abs)) {
696
+ if (!opts.dryRun) {
697
+ orphanTranslations += cache.deleteTranslationsByFilepath(fp);
698
+ }
699
+ else {
700
+ orphanTranslations += 1;
701
+ }
702
+ }
703
+ }
704
+ console.log(`[cleanup] orphaned translations (missing on disk): ${orphanTranslations}${opts.dryRun ? " (dry-run)" : ""}`);
705
+ cache.close();
706
+ });
707
+ program
708
+ .command("editor")
709
+ .description("Launch local web UI for cache segments, strings.json, and glossary CSV")
710
+ .option("-p, --port <n>", "Port", "8787")
711
+ .option("--no-open", "Do not open the default browser")
712
+ .action((_opts, cmd) => {
713
+ const { configFlag, cwd } = withConfig(cmd);
714
+ const { config, projectRoot } = loadConfigOrExit(configFlag, cwd);
715
+ const cmdOpts = cmd.opts();
716
+ const port = parseInt(cmdOpts.port || "8787", 10);
717
+ const cache = new TranslationCache(path.join(projectRoot, config.cacheDir));
718
+ const stringsPath = config.glossary?.uiGlossary
719
+ ? path.join(projectRoot, config.glossary.uiGlossary)
720
+ : resolveStringsJsonPath(config, projectRoot);
721
+ const glossaryPath = config.glossary?.userGlossary
722
+ ? path.join(projectRoot, config.glossary.userGlossary)
723
+ : null;
724
+ const jsonSourceBlock = config.documentations.find((b) => b.jsonSource?.trim());
725
+ const app = createTranslationEditorApp(cache, {
726
+ cwd: projectRoot,
727
+ stringsJsonPath: stringsPath,
728
+ glossaryUserPath: glossaryPath,
729
+ sourceLocale: config.sourceLocale,
730
+ targetLocales: config.targetLocales,
731
+ jsonSource: jsonSourceBlock?.jsonSource?.trim() || null,
732
+ });
733
+ const staticDir = resolveEditCacheStaticDir();
734
+ if (fs.existsSync(staticDir)) {
735
+ app.use(express.static(staticDir));
736
+ }
737
+ else {
738
+ console.warn(`[editor] Static dir missing: ${staticDir}`);
739
+ }
740
+ const server = http.createServer(app);
741
+ server.listen(port, () => {
742
+ const url = `http://127.0.0.1:${port}/`;
743
+ console.log(chalk.green("-------------------------------------------"));
744
+ console.log(chalk.green(" ai-i18n-tools Translation Cache Editor"));
745
+ console.log(chalk.green("-------------------------------------------\n"));
746
+ console.log(`[editor] Running at ` + chalk.cyan(`${url}`));
747
+ if (!cmdOpts.noOpen) {
748
+ openBrowser(url);
749
+ }
750
+ console.log("\n");
751
+ });
752
+ });
753
+ program
754
+ .command("glossary-generate")
755
+ .description("Write an empty glossary-user.csv with standard headers")
756
+ .option("-o, --output <path>", "Output path (default: config glossary.userGlossary or glossary-user.csv)")
757
+ .action((opts, cmd) => {
758
+ const { configFlag, cwd } = withConfig(cmd);
759
+ const { config, projectRoot } = loadConfigOrExit(configFlag, cwd);
760
+ const out = opts.output
761
+ ? path.resolve(cwd, opts.output)
762
+ : path.join(projectRoot, config.glossary?.userGlossary || "glossary-user.csv");
763
+ const header = `"Original language string","locale","Translation","Force"\n`;
764
+ if (fs.existsSync(out)) {
765
+ console.error(`Refusing to overwrite existing file: ${out}`);
766
+ process.exit(1);
767
+ }
768
+ writeAtomicUtf8(out, header);
769
+ console.log(`Wrote ${out}`);
770
+ });
771
+ program.parse();
772
+ //# sourceMappingURL=index.js.map