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,697 @@
1
+ # ai-i18n-tools: Getting Started
2
+
3
+ `ai-i18n-tools` provides two independent, composable workflows:
4
+
5
+ - **Workflow 1 - UI Translation**: extract `t("…")` calls from any JS/TS source, translate them via OpenRouter, and write flat per-locale JSON files ready for i18next.
6
+ - **Workflow 2 - Document Translation**: translate markdown (MDX) and Docusaurus JSON label files to any number of locales, with smart caching. **SVG** assets use `features.translateSVG`, the top-level `svg` block, and `translate-svg` (see [CLI reference](#cli-reference)).
7
+
8
+ Both workflows use OpenRouter (any compatible LLM) and share a single config file.
9
+
10
+
11
+ <small>**Read in other languages:** </small>
12
+ <small id="lang-list">[en-GB](./GETTING_STARTED.md) · [de](../translated-docs/docs/GETTING_STARTED.de.md) · [es](../translated-docs/docs/GETTING_STARTED.es.md) · [fr](../translated-docs/docs/GETTING_STARTED.fr.md) · [hi](../translated-docs/docs/GETTING_STARTED.hi.md) · [ja](../translated-docs/docs/GETTING_STARTED.ja.md) · [ko](../translated-docs/docs/GETTING_STARTED.ko.md) · [pt-BR](../translated-docs/docs/GETTING_STARTED.pt-BR.md) · [zh-CN](../translated-docs/docs/GETTING_STARTED.zh-CN.md) · [zh-TW](../translated-docs/docs/GETTING_STARTED.zh-TW.md)</small>
13
+
14
+ ---
15
+
16
+ <!-- START doctoc generated TOC please keep comment here to allow auto update -->
17
+ <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
18
+ **Table of Contents**
19
+
20
+ - [Installation](#installation)
21
+ - [Quick Start](#quick-start)
22
+ - [Workflow 1 - UI Translation](#workflow-1---ui-translation)
23
+ - [Step 1: Initialise](#step-1-initialise)
24
+ - [Step 2: Extract strings](#step-2-extract-strings)
25
+ - [Step 3: Translate UI strings](#step-3-translate-ui-strings)
26
+ - [Exporting to XLIFF 2.0 (optional)](#exporting-to-xliff-20-optional)
27
+ - [Step 4: Wire i18next at runtime](#step-4-wire-i18next-at-runtime)
28
+ - [Using `t()` in source code](#using-t-in-source-code)
29
+ - [Interpolation](#interpolation)
30
+ - [Language switcher UI](#language-switcher-ui)
31
+ - [RTL languages](#rtl-languages)
32
+ - [Workflow 2 - Document Translation](#workflow-2---document-translation)
33
+ - [Step 1: Initialise](#step-1-initialise-1)
34
+ - [Step 2: Translate documents](#step-2-translate-documents)
35
+ - [Cache behaviour and `translate-docs` flags](#cache-behaviour-and-translate-docs-flags)
36
+ - [Output layouts](#output-layouts)
37
+ - [Combined workflow (UI + Docs)](#combined-workflow-ui--docs)
38
+ - [Configuration reference](#configuration-reference)
39
+ - [`sourceLocale`](#sourcelocale)
40
+ - [`targetLocales`](#targetlocales)
41
+ - [`uiLanguagesPath` (optional)](#uilanguagespath-optional)
42
+ - [`concurrency` (optional)](#concurrency-optional)
43
+ - [`batchConcurrency` (optional)](#batchconcurrency-optional)
44
+ - [`batchSize` / `maxBatchChars` (optional)](#batchsize--maxbatchchars-optional)
45
+ - [`openrouter`](#openrouter)
46
+ - [`features`](#features)
47
+ - [`ui`](#ui)
48
+ - [`cacheDir`](#cachedir)
49
+ - [`documentations`](#documentations)
50
+ - [`svg` (optional)](#svg-optional)
51
+ - [`glossary`](#glossary)
52
+ - [CLI reference](#cli-reference)
53
+ - [Environment variables](#environment-variables)
54
+
55
+ <!-- END doctoc generated TOC please keep comment here to allow auto update -->
56
+
57
+ ## Installation
58
+
59
+ The published package is **ESM-only**. Use `import`/`import()` in Node.js or your bundler; **do not use `require('ai-i18n-tools')`.**
60
+
61
+ ```bash
62
+ npm install ai-i18n-tools
63
+ # or
64
+ pnpm add ai-i18n-tools
65
+ # or
66
+ yarn add ai-i18n-tools
67
+ ```
68
+
69
+ Set your OpenRouter API key:
70
+
71
+ ```bash
72
+ export OPENROUTER_API_KEY=sk-or-v1-your-key-here
73
+ ```
74
+
75
+ Or create a `.env` file in the project root:
76
+
77
+ ```env
78
+ OPENROUTER_API_KEY=sk-or-v1-your-key-here
79
+ ```
80
+
81
+ ---
82
+
83
+ ## Quick Start
84
+
85
+ The default `init` template (`ui-markdown`) enables **UI** extraction and translation only. The `ui-docusaurus` template enables **document** translation (`translate-docs`). Use `sync` when you want one command that runs extract, UI translation, optional standalone SVG translation, and documentation translation according to your config.
86
+
87
+ ```bash
88
+ # Workflow 1 - UI strings (default template enables extract + translate-ui)
89
+ npx ai-i18n-tools init
90
+ npx ai-i18n-tools extract
91
+ npx ai-i18n-tools translate-ui
92
+
93
+ # Workflow 2 - docs (Docusaurus-oriented template)
94
+ npx ai-i18n-tools init -t ui-docusaurus
95
+ npx ai-i18n-tools translate-docs
96
+
97
+ # Combined: extract UI strings, then translate UI + SVG + docs (per config features)
98
+ npx ai-i18n-tools sync
99
+
100
+ # Markdown translation status (per file × locale)
101
+ npx ai-i18n-tools status
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Workflow 1 - UI Translation
107
+
108
+ Designed for any JS/TS project that uses i18next: React apps, Next.js (client and server components), Node.js services, CLI tools.
109
+
110
+ ### Step 1: Initialise
111
+
112
+ ```bash
113
+ npx ai-i18n-tools init
114
+ ```
115
+
116
+ This writes `ai-i18n-tools.config.json` with the `ui-markdown` template. Edit it to set:
117
+
118
+ - `sourceLocale` - your source language BCP-47 code (e.g. `"en-GB"`). **Must match** `SOURCE_LOCALE` exported from your runtime i18n setup file (`src/i18n.ts` / `src/i18n.js`).
119
+ - `targetLocales` - path to your `ui-languages.json` manifest OR an array of BCP-47 codes.
120
+ - `ui.sourceRoots` - directories to scan for `t("…")` calls (e.g. `["src/"]`).
121
+ - `ui.stringsJson` - where to write the master catalog (e.g. `"src/locales/strings.json"`).
122
+ - `ui.flatOutputDir` - where to write `de.json`, `pt-BR.json`, etc. (e.g. `"src/locales/"`).
123
+ - `ui.preferredModel` (optional) - OpenRouter model id to try **first** for `translate-ui` only; on failure the CLI continues with `openrouter.translationModels` (or legacy `defaultModel` / `fallbackModel`) in order, skipping duplicates.
124
+
125
+ ### Step 2: Extract strings
126
+
127
+ ```bash
128
+ npx ai-i18n-tools extract
129
+ ```
130
+
131
+ Scans all JS/TS files under `ui.sourceRoots` for `t("literal")` and `i18n.t("literal")` calls. Writes (or merges into) `ui.stringsJson`.
132
+
133
+ The scanner is configurable: add custom function names via `ui.reactExtractor.funcNames`.
134
+
135
+ ### Step 3: Translate UI strings
136
+
137
+ ```bash
138
+ npx ai-i18n-tools translate-ui
139
+ ```
140
+
141
+ Reads `strings.json`, sends batches to OpenRouter for each target locale, writes flat JSON files (`de.json`, `fr.json`, etc.) to `ui.flatOutputDir`. When `ui.preferredModel` is set, that model is attempted before the ordered list in `openrouter.translationModels` (document translation and other commands still use only `openrouter`).
142
+
143
+ For each entry, `translate-ui` stores the **OpenRouter model id** that successfully translated each locale in an optional `models` object (same locale keys as `translated`). Strings edited in the local `editor` command are marked with the sentinel value `user-edited` in `models` for that locale. The per-locale flat files under `ui.flatOutputDir` remain **source string → translation** only; they do not include `models` (so runtime bundles stay unchanged).
144
+
145
+ > **Note on using the Cache Editor:** If you edit an entry in the cache editor, you need to run a `sync --force-update` (or the equivalent `translate` command with `--force-update`) to rewrite the output files with the updated cache entry. Also, keep in mind that if the source text changes later, your manual edit will be lost because a new cache key (hash) will be generated for the new source string.
146
+
147
+ ### Exporting to XLIFF 2.0 (optional)
148
+
149
+ To hand UI strings off to a translation vendor, TMS, or CAT tool, export the catalog as **XLIFF 2.0** (one file per target locale). This command is **read-only**: it does not modify `strings.json` or call any API.
150
+
151
+ ```bash
152
+ npx ai-i18n-tools export-ui-xliff
153
+ ```
154
+
155
+ By default, files are written next to `ui.stringsJson`, named like `strings.de.xliff`, `strings.pt-BR.xliff` (basename of your catalog + locale + `.xliff`). Use `-o` / `--output-dir` to write elsewhere. Existing translations from `strings.json` appear in `<target>`; missing locales use `state="initial"` with no `<target>` so tools can fill them in. Use `--untranslated-only` to export only units that still need a translation for each locale (useful for vendor batches). `--dry-run` prints paths without writing files.
156
+
157
+ ### Step 4: Wire i18next at runtime
158
+
159
+ Create your i18n setup file using the helpers exported by `'ai-i18n-tools/runtime'`:
160
+
161
+ ```js
162
+ // src/i18n.js (or src/i18n.ts)
163
+ import i18n from 'i18next';
164
+ import { initReactI18next } from 'react-i18next';
165
+ import uiLanguages from './locales/ui-languages.json';
166
+ import {
167
+ defaultI18nInitOptions,
168
+ wrapI18nWithKeyTrim,
169
+ makeLoadLocale,
170
+ applyDirection,
171
+ } from 'ai-i18n-tools/runtime';
172
+
173
+ // Must match sourceLocale in ai-i18n-tools.config.json
174
+ export const SOURCE_LOCALE = 'en-GB';
175
+
176
+ void i18n.use(initReactI18next).init(defaultI18nInitOptions(SOURCE_LOCALE));
177
+ wrapI18nWithKeyTrim(i18n);
178
+ i18n.on('languageChanged', applyDirection);
179
+ applyDirection(i18n.language);
180
+
181
+ const localeLoaders = Object.fromEntries(
182
+ uiLanguages
183
+ .filter(({ code }) => code !== SOURCE_LOCALE)
184
+ .map(({ code }) => [code, () => import(`./locales/${code}.json`)])
185
+ );
186
+
187
+ export const loadLocale = makeLoadLocale(i18n, localeLoaders, SOURCE_LOCALE);
188
+ export default i18n;
189
+ ```
190
+
191
+ Import `i18n.js` before React renders (e.g. at the top of your entry point). When the user changes language, call `await loadLocale(code)` then `i18n.changeLanguage(code)`.
192
+
193
+ `SOURCE_LOCALE` is exported so any other file that needs it (e.g. a language switcher) can import it directly from `'./i18n'`.
194
+
195
+ `defaultI18nInitOptions(sourceLocale)` returns the standard options for key-as-default setups:
196
+
197
+ - `parseMissingKeyHandler` returns the key itself, so untranslated strings display the source text.
198
+ - `nsSeparator: false` allows keys that contain colons.
199
+ - `interpolation.escapeValue: false` - safe to disable: React escapes values itself, and Node.js/CLI output has no HTML to escape.
200
+
201
+ `wrapI18nWithKeyTrim(i18n)` wraps `i18n.t` so that: (1) keys are trimmed before lookup, matching how the extract script stores them; (2) <code>{"{{var}}"}</code> interpolation is applied when the source locale returns the raw key - so <code>{"t('Hello {{name}}', { name })"}</code> works correctly even for the source language.
202
+
203
+ `makeLoadLocale(i18n, loaders, sourceLocale)` returns an async `loadLocale(lang)` function that dynamically imports the JSON bundle for a locale and registers it with i18next.
204
+
205
+ ### Using `t()` in source code
206
+
207
+ Call `t()` with a **literal string** so the extract script can find it:
208
+
209
+ ```jsx
210
+ import { useTranslation } from 'react-i18next';
211
+
212
+ function MyComponent() {
213
+ const { t } = useTranslation();
214
+ return <button>{t('Save')}</button>;
215
+ }
216
+ ```
217
+
218
+ The same pattern works outside React (Node.js, server components, CLI):
219
+
220
+ ```js
221
+ import i18n from './i18n.js';
222
+ console.log(i18n.t('Processing complete'));
223
+ ```
224
+
225
+ **Rules:**
226
+
227
+ - Only these forms are extracted: `t("…")`, `t('…')`, `t(`…`)`, `i18n.t("…")`.
228
+ - The key must be a **literal string** - no variables or expressions as the key.
229
+ - Do not use template literals for the key: <code>{'t(`Hello ${name}`)'}</code> is not extractable.
230
+
231
+ ### Interpolation
232
+
233
+ Use i18next's native second-argument interpolation for <code>{"{{var}}"}</code> placeholders:
234
+
235
+ ```js
236
+ // i18next handles substitution natively, even in key-as-default mode
237
+ t('Hello {{name}}, you have {{count}} messages', { name, count })
238
+ // → "Hello Alice, you have 3 messages"
239
+ ```
240
+
241
+ The extract script ignores the second argument - only the literal key string <code>{"\"Hello {{name}}, you have {{count}} messages\""}</code> is extracted and sent for translation. Translators are instructed to preserve <code>{"{{...}}"}</code> tokens.
242
+
243
+ ### Language switcher UI
244
+
245
+ Use the `ui-languages.json` manifest to build a language selector. `ai-i18n-tools` exports two display helpers:
246
+
247
+ ```tsx
248
+ import { useMemo } from 'react';
249
+ import { useTranslation } from 'react-i18next';
250
+ import {
251
+ getUILanguageLabel,
252
+ getUILanguageLabelNative,
253
+ type UiLanguageEntry,
254
+ } from 'ai-i18n-tools/runtime';
255
+ import uiLanguages from './locales/ui-languages.json';
256
+ import { loadLocale } from './i18n';
257
+
258
+ function LanguageSelect({
259
+ value,
260
+ onChange,
261
+ }: {
262
+ value: string;
263
+ onChange: (code: string) => void;
264
+ }) {
265
+ const { t, i18n } = useTranslation();
266
+
267
+ const options = useMemo(
268
+ () =>
269
+ (uiLanguages as UiLanguageEntry[]).map((lang) => ({
270
+ code: lang.code,
271
+ // Settings/content dropdowns: shows translated name when available
272
+ label: getUILanguageLabel(lang, t),
273
+ // Header globe menu: shows "English / Deutsch"-style label, no t() call
274
+ nativeLabel: getUILanguageLabelNative(lang),
275
+ })),
276
+ [t]
277
+ );
278
+
279
+ const handleChange = async (code: string) => {
280
+ await loadLocale(code);
281
+ i18n.changeLanguage(code);
282
+ onChange(code);
283
+ };
284
+
285
+ return (
286
+ <select value={value} onChange={(e) => handleChange(e.target.value)}>
287
+ {options.map((row) => (
288
+ <option key={row.code} value={row.code}>
289
+ {row.label}
290
+ </option>
291
+ ))}
292
+ </select>
293
+ );
294
+ }
295
+ ```
296
+
297
+ `getUILanguageLabel(lang, t)` - shows `t(englishName)` when translated, or `englishName / t(englishName)` when both differ. Suitable for settings screens.
298
+
299
+ `getUILanguageLabelNative(lang)` - shows `englishName / label` (no `t()` call on each row). Suitable for header menus where you want the native name visible.
300
+
301
+ The `ui-languages.json` manifest is a JSON array of <code>{"{ code, label, englishName }"}</code> entries. Example:
302
+
303
+ ```json
304
+ [
305
+ { "code": "en-GB", "label": "English (UK)", "englishName": "English (UK)" },
306
+ { "code": "pt-BR", "label": "Português (BR)", "englishName": "Portuguese (BR)" },
307
+ { "code": "de", "label": "Deutsch", "englishName": "German" },
308
+ { "code": "fr", "label": "Français", "englishName": "French" },
309
+ { "code": "ar", "label": "العربية", "englishName": "Arabic" }
310
+ ]
311
+ ```
312
+
313
+ Set `targetLocales` in config to the path of this file so the translate command uses the same list.
314
+
315
+ ### RTL languages
316
+
317
+ `ai-i18n-tools` exports `getTextDirection(lng)` and `applyDirection(lng)`:
318
+
319
+ ```js
320
+ import { getTextDirection, applyDirection } from 'ai-i18n-tools/runtime';
321
+
322
+ getTextDirection('ar') // 'rtl'
323
+ getTextDirection('en-GB') // 'ltr'
324
+
325
+ // Applied automatically via i18n.on('languageChanged', applyDirection) - see Step 4
326
+ ```
327
+
328
+ `applyDirection` sets `document.documentElement.dir` (browser) or is a no-op (Node.js). Pass an optional `element` argument to target a specific element.
329
+
330
+ For strings that may contain `→` arrows, flip them for RTL layouts:
331
+
332
+ ```js
333
+ import { flipUiArrowsForRtl } from 'ai-i18n-tools/runtime';
334
+ const { i18n } = useTranslation();
335
+ const isRtl = getTextDirection(i18n.language) === 'rtl';
336
+ const label = flipUiArrowsForRtl(t('Next → Step'), isRtl);
337
+ ```
338
+
339
+ ---
340
+
341
+ ## Workflow 2 - Document Translation
342
+
343
+ Designed for markdown documentation, Docusaurus sites, and JSON label files. Standalone SVG assets are translated via [`translate-svg`](#cli-reference) when `features.translateSVG` is enabled and the top-level `svg` block is set — not via `documentations[].contentPaths`.
344
+
345
+ ### Step 1: Initialise
346
+
347
+ ```bash
348
+ npx ai-i18n-tools init -t ui-docusaurus
349
+ ```
350
+
351
+ Edit the generated `ai-i18n-tools.config.json`:
352
+
353
+ - `sourceLocale` - source language (must match `defaultLocale` in `docusaurus.config.js`).
354
+ - `targetLocales` - array of locale codes or path to a manifest.
355
+ - `cacheDir` - shared SQLite cache directory for all documentation pipelines (and default log directory for `--write-logs`).
356
+ - `documentations` - array of documentation blocks. Each block has optional `description`, `contentPaths`, `outputDir`, optional `jsonSource`, `markdownOutput`, `targetLocales`, `addFrontmatter`, etc.
357
+ - `documentations[].description` - optional short note for maintainers (what this block covers). When set, it appears in the `translate-docs` headline (`🌐 …: translating …`) and in `status` section headers.
358
+ - `documentations[].contentPaths` - markdown/MDX source directories or files (see also `documentations[].jsonSource` for JSON labels).
359
+ - `documentations[].outputDir` - translated output root for that block.
360
+ - `documentations[].markdownOutput.style` - `"nested"` (default), `"docusaurus"`, or `"flat"` (see [Output layouts](#output-layouts)).
361
+
362
+ ### Step 2: Translate documents
363
+
364
+ ```bash
365
+ npx ai-i18n-tools translate-docs
366
+ ```
367
+
368
+ This translates all files in every `documentations` block’s `contentPaths` to all effective documentation locales (union of each block’s `targetLocales` when set, otherwise root `targetLocales`). Already-translated segments are served from the SQLite cache - only new or changed segments are sent to the LLM.
369
+
370
+ To translate a single locale:
371
+
372
+ ```bash
373
+ npx ai-i18n-tools translate-docs --locale de
374
+ ```
375
+
376
+ To check what needs translating:
377
+
378
+ ```bash
379
+ npx ai-i18n-tools status
380
+ ```
381
+
382
+ #### Cache behaviour and `translate-docs` flags
383
+
384
+ The CLI keeps **file tracking** in SQLite (source hash per file × locale) and **segment** rows (hash × locale per translatable chunk). A normal run skips a file entirely when the tracked hash matches the current source **and** the output file already exists; otherwise it processes the file and uses the segment cache so unchanged text does not call the API.
385
+
386
+
387
+ | Flag | Effect |
388
+ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
389
+ | *(default)* | Skip unchanged files when tracking + on-disk output match; use segment cache for the rest. |
390
+ | `--force-update` | Re-process every matched file (extract, reassemble, write outputs) even when file tracking would skip. **Segment cache still applies** - unchanged segments are not sent to the LLM. |
391
+ | `--force` | Clears file tracking for each processed file and **does not read** the segment cache for API translation (full re-translation). New results are still **written** to the segment cache. |
392
+ | `--stats` | Print segment counts, tracked file counts, and per-locale segment totals, then exit. |
393
+ | `--clear-cache [locale]` | Delete cached translations (and file tracking): all locales, or a single locale, then exit. |
394
+ | `--prompt-format <mode>` | How each **batch** of segments is sent to the model and parsed (`xml`, `json-array`, or `json-object`). Default **`xml`**. Does not change extraction, placeholders, validation, cache, or fallback behaviour — see [Batch prompt format](#batch-prompt-format). |
395
+
396
+
397
+ You cannot combine `--force` with `--force-update` (they are mutually exclusive).
398
+
399
+ #### Batch prompt format
400
+
401
+ `translate-docs` sends translatable segments to OpenRouter in **batches** (grouped by `batchSize` / `maxBatchChars`). The **`--prompt-format`** flag only changes that batch’s **wire format**; segment splitting, `PlaceholderHandler` tokens, markdown AST checks, SQLite cache keys, and per-segment fallback when batch parsing fails are unchanged.
402
+
403
+ | Mode | User message | Model reply |
404
+ | ---- | ------------ | ----------- |
405
+ | **`xml`** (default) | Pseudo-XML: one `<seg id="N">…</seg>` per segment (with XML escaping). | Only `<t id="N">…</t>` blocks, one per segment index. |
406
+ | **`json-array`** | A JSON array of strings, one entry per segment in order. | A JSON array of the **same length** (same order). |
407
+ | **`json-object`** | A JSON object `{"0":"…","1":"…",…}` keyed by segment index. | A JSON object with the **same keys** and translated values. |
408
+
409
+ The run header also prints `Batch prompt format: …` so you can confirm the active mode. JSON label files (`jsonSource`) and standalone SVG batches use the same setting when those steps run as part of `translate-docs` (or `sync`’s docs phase — `sync` does not expose this flag; it defaults to **`xml`**).
410
+
411
+ **Segment dedupe and paths in SQLite**
412
+
413
+ - Segment rows are keyed globally by `(source_hash, locale)` (hash = normalized content). Identical text in two files shares one row; `translations.filepath` is metadata (last writer), not a second cache entry per file.
414
+ - `file_tracking.filepath` uses namespaced keys: `doc-block:{index}:{relPath}` per `documentations` block (`relPath` is project-root-relative posix: markdown paths as collected; **JSON label files use the cwd-relative path to the source file**, e.g. `docs-site/i18n/en/code.json`, so cleanup can resolve the real file), and `svg-assets:{relPath}` for standalone SVG assets under `translate-svg`.
415
+ - `translations.filepath` stores cwd-relative posix paths for markdown, JSON, and SVG segments (SVG uses the same path shape as other assets; the `svg-assets:…` prefix is **only** on `file_tracking`).
416
+ - After a run, `last_hit_at` is cleared only for segment rows **in the same translate scope** (respecting `--path` and enabled kinds) that were not hit, so a filtered or docs-only run does not mark unrelated files stale.
417
+
418
+ ### Output layouts
419
+
420
+ `"nested"` (default when omitted) — mirrors the source tree under `{outputDir}/{locale}/` (e.g. `docs/guide.md` → `i18n/de/docs/guide.md`).
421
+
422
+ `"docusaurus"` — places files that lie under `docsRoot` at `i18n/<locale>/docusaurus-plugin-content-docs/current/<relativeToDocsRoot>`, matching the usual Docusaurus i18n layout. Set `documentations[].markdownOutput.docsRoot` to your docs source root (e.g. `"docs"`).
423
+
424
+ ```
425
+ docs/guide.md → i18n/de/docusaurus-plugin-content-docs/current/guide.md
426
+ i18n/en/sidebar.json → i18n/de/sidebar.json (JSON label files)
427
+ ```
428
+
429
+ `"flat"` - places translated files next to the source with a locale suffix, or in a subdirectory. Relative links between pages are rewritten automatically.
430
+
431
+ ```
432
+ docs/guide.md → i18n/guide.de.md
433
+ ```
434
+
435
+ You can override paths entirely with `documentations[].markdownOutput.pathTemplate`. Placeholders: <code>{"{outputDir}"}</code>, <code>{"{locale}"}</code>, <code>{"{LOCALE}"}</code>, <code>{"{relPath}"}</code>, <code>{"{stem}"}</code>, <code>{"{basename}"}</code>, <code>{"{extension}"}</code>, <code>{"{docsRoot}"}</code>, <code>{"{relativeToDocsRoot}"}</code>.
436
+
437
+ ---
438
+
439
+ ## Combined workflow (UI + Docs)
440
+
441
+ Enable all features in a single config to run both workflows together:
442
+
443
+ ```json
444
+ {
445
+ "sourceLocale": "en-GB",
446
+ "targetLocales": "src/locales/ui-languages.json",
447
+ "features": {
448
+ "extractUIStrings": true,
449
+ "translateUIStrings": true,
450
+ "translateMarkdown": true,
451
+ "translateJSON": false,
452
+ "translateSVG": false
453
+ },
454
+ "glossary": {
455
+ "uiGlossary": "src/locales/strings.json",
456
+ "userGlossary": "glossary-user.csv"
457
+ },
458
+ "ui": {
459
+ "sourceRoots": ["src/"],
460
+ "stringsJson": "src/locales/strings.json",
461
+ "flatOutputDir": "src/locales/"
462
+ },
463
+ "cacheDir": ".translation-cache",
464
+ "documentations": [
465
+ {
466
+ "contentPaths": ["docs/"],
467
+ "outputDir": "i18n/",
468
+ "markdownOutput": { "style": "flat" }
469
+ }
470
+ ]
471
+ }
472
+ ```
473
+
474
+ `glossary.uiGlossary` points document translation at the same `strings.json` catalog as the UI so terminology stays consistent; `glossary.userGlossary` adds CSV overrides for product terms.
475
+
476
+ Run `npx ai-i18n-tools sync` to run one pipeline: **extract** UI strings (if `features.extractUIStrings`), **translate UI** strings (if `features.translateUIStrings`), **translate standalone SVG assets** (if `features.translateSVG` and a `svg` block are set), then **translate documentation** (each `documentations` block: markdown/JSON as configured). Skip parts with `--no-ui`, `--no-svg`, or `--no-docs`. The docs step accepts `--dry-run`, `-p` / `--path`, `--force`, and `--force-update` (the last two only apply when documentation translation runs; they are ignored if you pass `--no-docs`).
477
+
478
+ Use `documentations[].targetLocales` on a block to translate that block’s files to a **smaller subset** than the UI (effective documentation locales are the **union** across blocks):
479
+
480
+ ```json
481
+ {
482
+ "targetLocales": "src/locales/ui-languages.json",
483
+ "documentations": [
484
+ {
485
+ "contentPaths": ["docs/"],
486
+ "outputDir": "i18n/",
487
+ "targetLocales": ["de", "fr", "es"]
488
+ }
489
+ ]
490
+ }
491
+ ```
492
+
493
+ ---
494
+
495
+ ## Configuration reference
496
+
497
+ ### `sourceLocale`
498
+
499
+ BCP-47 code for the source language (e.g. `"en-GB"`, `"en"`, `"pt-BR"`). No translation file is generated for this locale - the key string itself is the source text.
500
+
501
+ **Must match** `SOURCE_LOCALE` exported from your runtime i18n setup file (`src/i18n.ts` / `src/i18n.js`).
502
+
503
+ ### `targetLocales`
504
+
505
+ Which locales to translate to. Accepts:
506
+
507
+ - **String path** to a `ui-languages.json` manifest (`"src/locales/ui-languages.json"`). The file is loaded and locale codes are extracted.
508
+ - **Array of BCP-47 codes** (`["de", "fr", "es"]`).
509
+ - **One-element array with a path** (`["src/locales/ui-languages.json"]`) - same behaviour as the string form.
510
+
511
+ `targetLocales` is the primary locale list for UI translation and the default locale list for documentation blocks. If you prefer to keep an explicit array here but still want manifest-driven labels and locale filtering, also set `uiLanguagesPath`.
512
+
513
+ ### `uiLanguagesPath` (optional)
514
+
515
+ Path to a `ui-languages.json` manifest used for display names, locale filtering, and language-list post-processing.
516
+
517
+ Use this when:
518
+
519
+ - `targetLocales` is an explicit array, but you still want English/native labels from the manifest.
520
+ - You want `markdownOutput.postProcessing.languageListBlock` to build locale labels from the same manifest.
521
+ - Only UI translation is enabled and you want the manifest to provide the effective UI locale list.
522
+
523
+ ### `concurrency` (optional)
524
+
525
+ Maximum **target locales** translated at the same time (`translate-ui`, `translate-docs`, `translate-svg`, and the matching steps inside `sync`). If omitted, the CLI uses **4** for UI translation and **3** for documentation translation (built-in defaults). Override per run with `-j` / `--concurrency`.
526
+
527
+ ### `batchConcurrency` (optional)
528
+
529
+ **translate-docs** and **translate-svg** (and the documentation step of `sync`): maximum parallel OpenRouter **batch** requests per file (each batch can contain many segments). Default **4** when omitted. Ignored by `translate-ui`. Override with `-b` / `--batch-concurrency`. On `sync`, `-b` applies to the documentation translation step only.
530
+
531
+ ### `batchSize` / `maxBatchChars` (optional)
532
+
533
+ Segment batching for document translation: how many segments per API request, and a character ceiling. Defaults: **20** segments, **4096** characters (when omitted).
534
+
535
+ ### `openrouter`
536
+
537
+
538
+ | Field | Description |
539
+ | ------------------- | ---------------------------------------------------------------------------------------- |
540
+ | `baseUrl` | OpenRouter API base URL. Default: `https://openrouter.ai/api/v1`. |
541
+ | `translationModels` | Preferred ordered list of model IDs. The first is tried first; later entries are fallbacks on error. For `translate-ui` only**, you can also set `ui.preferredModel` to try one model before this list (see `ui`). |
542
+ | `defaultModel` | Legacy single primary model. Used only when `translationModels` is unset or empty. |
543
+ | `fallbackModel` | Legacy single fallback model. Used after `defaultModel` when `translationModels` is unset or empty. |
544
+ | `maxTokens` | Max completion tokens per request. Default: `8192`. |
545
+ | `temperature` | Sampling temperature. Default: `0.2`. |
546
+
547
+
548
+ Set `OPENROUTER_API_KEY` in your environment or `.env` file.
549
+
550
+ ### `features`
551
+
552
+
553
+ | Field | Workflow | Description |
554
+ | -------------------- | -------- | ----------------------------------------------------------------- |
555
+ | `extractUIStrings` | 1 | Scan source for `t("…")` and write/merge `strings.json`. |
556
+ | `translateUIStrings` | 1 | Translate `strings.json` entries and write per-locale JSON files. |
557
+ | `translateMarkdown` | 2 | Translate `.md` / `.mdx` files. |
558
+ | `translateJSON` | 2 | Translate Docusaurus JSON label files. |
559
+ | `translateSVG` | 2 | Translate standalone `.svg` assets (requires the top-level `svg` block). |
560
+
561
+ Translate **standalone** SVG assets with `translate-svg` when `features.translateSVG` is true and a top-level `svg` block is configured. The `sync` command runs that step when both are set (unless `--no-svg`).
562
+
563
+ ### `ui`
564
+
565
+
566
+ | Field | Description |
567
+ | --------------------------- | ----------------------------------------------------------------------- |
568
+ | `sourceRoots` | Directories (relative to cwd) scanned for `t("…")` calls. |
569
+ | `stringsJson` | Path to the master catalog file. Updated by `extract`. |
570
+ | `flatOutputDir` | Directory where per-locale JSON files are written (`de.json`, etc.). |
571
+ | `preferredModel` | Optional. OpenRouter model id tried first for `translate-ui` only; then `openrouter.translationModels` (or legacy models) in order, without duplicating this id. |
572
+ | `reactExtractor.funcNames` | Additional function names to scan (default: `["t", "i18n.t"]`). |
573
+ | `reactExtractor.extensions` | File extensions to include (default: `[".js", ".jsx", ".ts", ".tsx"]`). |
574
+ | `reactExtractor.includePackageDescription` | When `true` (default), `extract` also includes `package.json` `description` as a UI string when present. |
575
+ | `reactExtractor.packageJsonPath` | Custom path to the `package.json` file used for that optional description extraction. |
576
+
577
+
578
+ ### `cacheDir`
579
+
580
+ | Field | Description |
581
+ | ---------- | ----------------------------------------------------------------------------- |
582
+ | `cacheDir` | SQLite cache directory (shared by all `documentations` blocks). Reuse across runs. |
583
+
584
+ ### `documentations`
585
+
586
+ Array of documentation pipeline blocks. `translate-docs` and the docs phase of `sync` process **each** block in order.
587
+
588
+ | Field | Description |
589
+ | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
590
+ | `description` | Optional human-readable note for this block (not used for translation). Prefixed in the `translate-docs` `🌐` headline when set; also shown in `status` section headers. |
591
+ | `contentPaths` | Markdown/MDX sources to translate (`translate-docs` scans these for `.md` / `.mdx`). JSON labels come from `jsonSource` on the same block. |
592
+ | `outputDir` | Root directory for translated output for this block. |
593
+ | `sourceFiles` | Optional alias merged into `contentPaths` at load. |
594
+ | `targetLocales` | Optional subset of locales for this block only (otherwise root `targetLocales`). Effective documentation locales are the union across blocks. |
595
+ | `jsonSource` | Source directory for Docusaurus JSON label files for this block (e.g. `"i18n/en"`). |
596
+ | `markdownOutput.style` | `"nested"` (default), `"docusaurus"`, or `"flat"`. |
597
+ | `markdownOutput.docsRoot` | Source docs root for Docusaurus layout (e.g. `"docs"`). |
598
+ | `markdownOutput.pathTemplate` | Custom markdown output path. Placeholders: <code>{"{outputDir}"}</code>, <code>{"{locale}"}</code>, <code>{"{LOCALE}"}</code>, <code>{"{relPath}"}</code>, <code>{"{stem}"}</code>, <code>{"{basename}"}</code>, <code>{"{extension}"}</code>, <code>{"{docsRoot}"}</code>, <code>{"{relativeToDocsRoot}"}</code>. |
599
+ | `markdownOutput.jsonPathTemplate` | Custom JSON output path for label files. Supports the same placeholders as `pathTemplate`. |
600
+ | `markdownOutput.flatPreserveRelativeDir` | For `flat` style, keep source subdirectories so files with the same basename do not collide. |
601
+ | `markdownOutput.rewriteRelativeLinks` | Rewrite relative links after translation (auto-enabled for `flat` style). |
602
+ | `markdownOutput.linkRewriteDocsRoot` | Repo root used when computing flat-link rewrite prefixes. Usually leave this as `"."` unless your translated docs live under a different project root. |
603
+ | `markdownOutput.postProcessing` | Optional transforms on the translated markdown **body** (YAML front matter is preserved). Runs after segment reassembly and flat link rewriting, and before `addFrontmatter`. |
604
+ | `markdownOutput.postProcessing.regexAdjustments` | Ordered list of `{ "description"?, "search", "replace" }`. `search` is a regex pattern (plain string uses flag `g`, or `/pattern/flags`). `replace` supports placeholders such as `${translatedLocale}`, `${sourceLocale}`, `${sourceFullPath}`, `${translatedFullPath}`, `${sourceFilename}`, `${translatedFilename}`, `${sourceBasedir}`, `${translatedBasedir}` (same idea as the reference `additional-adjustments`). |
605
+ | `markdownOutput.postProcessing.languageListBlock` | `{ "start", "end", "separator" }` — the translator finds the first line containing `start` and the matching `end` line, then replaces that slice with a canonical language switcher. Links are built with paths relative to the translated file; labels come from `uiLanguagesPath` / `ui-languages.json` when configured, otherwise from `localeDisplayNames` and locale codes. |
606
+ | `addFrontmatter` | When `true` (default when omitted), translated markdown files include YAML keys: `translation_last_updated`, `source_file_mtime`, `source_file_hash`, `translation_language`, `source_file_path`, and when at least one segment has model metadata, `translation_models` (sorted list of OpenRouter model ids used). Set to `false` to skip. |
607
+
608
+
609
+ Example (flat README pipeline — screenshot paths + optional language list wrapper):
610
+
611
+ ```json
612
+ "markdownOutput": {
613
+ "style": "flat",
614
+ "postProcessing": {
615
+ "regexAdjustments": [
616
+ {
617
+ "description": "Per-locale screenshot folders",
618
+ "search": "images/screenshots/[^/]+/",
619
+ "replace": "images/screenshots/${translatedLocale}/"
620
+ }
621
+ ],
622
+ "languageListBlock": {
623
+ "start": "<small id=\"lang-list\">",
624
+ "end": "</small>",
625
+ "separator": " · "
626
+ }
627
+ }
628
+ }
629
+ ```
630
+
631
+ ### `svg` (optional)
632
+
633
+ Top-level paths and layout for standalone SVG assets. Translation runs only when **`features.translateSVG`** is true (via `translate-svg` or the SVG stage of `sync`).
634
+
635
+ | Field | Description |
636
+ | --------------------------- | ----------- |
637
+ | `sourcePath` | One directory or an array of directories scanned recursively for `.svg` files. |
638
+ | `outputDir` | Root directory for translated SVG output. |
639
+ | `style` | `"flat"` or `"nested"` when `pathTemplate` is unset. |
640
+ | `pathTemplate` | Custom SVG output path. Placeholders: <code>{"{outputDir}"}</code>, <code>{"{locale}"}</code>, <code>{"{LOCALE}"}</code>, <code>{"{relPath}"}</code>, <code>{"{stem}"}</code>, <code>{"{basename}"}</code>, <code>{"{extension}"}</code>, <code>{"{relativeToSourceRoot}"}</code>. |
641
+ | `svgExtractor.forceLowercase` | Lower-case translated text on SVG reassembly. Useful for designs that rely on all-lowercase labels. |
642
+
643
+ ### `glossary`
644
+
645
+
646
+ | Field | Description |
647
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
648
+ | `uiGlossary` | Path to `strings.json` - auto-builds a glossary from existing translations. |
649
+ | `userGlossary` | Path to a CSV with columns `Original language string` (or `en`), `locale`, `Translation` - one row per source term and target locale (`locale` may be `*` for all targets). |
650
+
651
+
652
+ The legacy key `uiGlossaryFromStringsJson` is still accepted and mapped to `uiGlossary` when loading config.
653
+
654
+ Generate an empty glossary CSV:
655
+
656
+ ```bash
657
+ npx ai-i18n-tools glossary-generate
658
+ ```
659
+
660
+ ---
661
+
662
+ ## CLI reference
663
+
664
+
665
+ | Command | Description |
666
+ | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
667
+ | `init [-t ui-markdown|ui-docusaurus] [-o path] [--with-translate-ignore]` | Write a starter config file (includes `concurrency`, `batchConcurrency`, `batchSize`, `maxBatchChars`, and `documentations[].addFrontmatter`). `--with-translate-ignore` creates a starter `.translate-ignore`. |
668
+ | `extract` | Scan source for `t("…")` calls and update `strings.json`. Requires `features.extractUIStrings`. |
669
+ | `translate-docs …` | Translate markdown/MDX and JSON for each `documentations` block (`contentPaths`, optional `jsonSource`). `-j`: max parallel locales; `-b`: max parallel batch API calls per file. `--prompt-format`: batch wire format (`xml` \| `json-array` \| `json-object`). See [Cache behaviour and `translate-docs` flags](#cache-behaviour-and-translate-docs-flags) and [Batch prompt format](#batch-prompt-format). |
670
+ | `translate-svg …` | Translate standalone SVG assets configured in `config.svg` (separate from docs). Requires `features.translateSVG`. Same cache ideas as docs; supports `--no-cache` to skip SQLite reads/writes for that run. `-j`, `-b`, `--force`, `--force-update`, `-p` / `--path`, `--dry-run`. |
671
+ | `translate-ui [--locale <code>] [--force] [--dry-run] [-j <n>]` | Translate UI strings only. `--force`: re-translate all entries per locale (ignore existing translations). `--dry-run`: no writes, no API calls. `-j`: max parallel locales. Requires `features.translateUIStrings`. |
672
+ | `export-ui-xliff [-l <codes>] [-o <dir>] [--untranslated-only] [--dry-run]` | Export `strings.json` to XLIFF 2.0 (one `.xliff` per target locale). `-o` / `--output-dir`: output directory (default: same folder as the catalog). `--untranslated-only`: only units missing a translation for that locale. Read-only; no API. |
673
+ | `sync …` | Extract (if enabled), then UI translation, then `translate-svg` when `features.translateSVG` and `config.svg` are set, then documentation translation - unless skipped with `--no-ui`, `--no-svg`, or `--no-docs`. Shared flags: `-l`, `-p`, `--dry-run`, `-j`, `-b` (docs batching only), `--force` / `--force-update` (docs only; mutually exclusive when docs run). |
674
+ | `status` | Show markdown translation status per file × locale (no `--locale` filter; locales come from config). |
675
+ | `cleanup [--dry-run] [--no-backup] [--backup <path>]` | Runs `sync --force-update` first (extract, UI, SVG, docs), then removes stale segment rows (null `last_hit_at` / empty filepath); drops `file_tracking` rows whose resolved source path is missing on disk; removes translation rows whose `filepath` metadata points at a missing file. Logs three counts (stale, orphaned `file_tracking`, orphaned translations). Creates a timestamped SQLite backup under the cache dir unless `--no-backup`. |
676
+ | `editor [-p <port>] [--no-open]` | Launch a local web editor for the cache, `strings.json`, and glossary CSV. `--no-open`: do not open the default browser automatically.<br><br>**Note:** If you edit an entry in the cache editor, you must run a `sync --force-update` to rewrite the output files with the updated cache entry. Also, if the source text changes later, the manual edit will be lost since a new cache key is generated. |
677
+ | `glossary-generate [-o <path>]` | Write an empty `glossary-user.csv` template. `-o`: override the output path (default: `glossary.userGlossary` from config, or `glossary-user.csv`). |
678
+
679
+
680
+ All commands accept `-c <path>` to specify a non-default config file, `-v` for verbose output, and `-w` / `--write-logs [path]` to tee console output to a log file (default path: under root `cacheDir`).
681
+
682
+ ---
683
+
684
+ ## Environment variables
685
+
686
+
687
+ | Variable | Description |
688
+ | ---------------------- | ---------------------------------------------------------- |
689
+ | `OPENROUTER_API_KEY` | **Required.** Your OpenRouter API key. |
690
+ | `OPENROUTER_BASE_URL` | Override the API base URL. |
691
+ | `I18N_SOURCE_LOCALE` | Override `sourceLocale` at runtime. |
692
+ | `I18N_TARGET_LOCALES` | Comma-separated locale codes to override `targetLocales`. |
693
+ | `I18N_LOG_LEVEL` | Logger level (`debug`, `info`, `warn`, `error`, `silent`). |
694
+ | `NO_COLOR` | When `1`, disable ANSI colours in log output. |
695
+ | `I18N_LOG_SESSION_MAX` | Max lines kept per log session (default `5000`). |
696
+
697
+