docs-i18n 0.8.1 → 0.8.3

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 (88) hide show
  1. package/admin/dist/server/server.js +32 -32
  2. package/package.json +1 -1
  3. package/template/app/routes/$lang.$project.$version.docs.$.tsx +2 -1
  4. package/template/app/routes/$lang.$project.$version.docs.framework.$framework.$.tsx +2 -0
  5. package/template/app/routes/$lang.$project.$version.docs.tsx +2 -1
  6. package/template/app/routes/$lang.$project.docs.$.tsx +2 -1
  7. package/template/app/routes/$lang.$project.docs.tsx +2 -1
  8. package/template/app/routes/$lang.docs.$.tsx +2 -1
  9. package/template/app/routes/$lang.docs.framework.$framework.$.tsx +2 -0
  10. package/template/app/routes/$lang.docs.tsx +2 -1
  11. package/template/app/utils/content-loader.ts +13 -2
  12. package/template/app/utils/docs.server.ts +17 -15
  13. package/template/content/blog/en/announcing-query-v5.md +110 -0
  14. package/template/content/blog/en/hello-world.md +26 -0
  15. package/template/content/blog/en/i18n-best-practices.md +57 -0
  16. package/template/content/blog/en/react-query-vs-swr.md +100 -0
  17. package/template/content/blog/en/state-management-2024.md +143 -0
  18. package/template/content/blog/en/tanstack-router-1.0.md +121 -0
  19. package/template/content/blog/ja/announcing-query-v5.md +110 -0
  20. package/template/content/blog/ja/hello-world.md +26 -0
  21. package/template/content/blog/zh-hans/announcing-query-v5.md +93 -0
  22. package/template/content/blog/zh-hans/hello-world.md +26 -0
  23. package/template/content/docs-i18n/docs.config.json +25 -0
  24. package/template/content/docs-i18n/en/architecture.md +335 -0
  25. package/template/content/docs-i18n/en/cli.md +13 -1
  26. package/template/content/docs-i18n/en/configuration.md +350 -0
  27. package/template/content/docs-i18n/en/deployment.md +222 -0
  28. package/template/content/docs-i18n/en/getting-started.md +189 -0
  29. package/template/content/docs.config.json +25 -0
  30. package/template/content/en/admin.md +151 -0
  31. package/template/content/en/architecture.md +222 -0
  32. package/template/content/en/cli.md +269 -0
  33. package/template/content/en/configuration.md +331 -0
  34. package/template/content/en/deployment.md +209 -0
  35. package/template/content/en/getting-started.md +168 -0
  36. package/template/content/form/docs.config.json +18 -0
  37. package/template/content/form/en/guides/validation.md +175 -0
  38. package/template/content/form/en/installation.md +63 -0
  39. package/template/content/form/en/overview.md +71 -0
  40. package/template/content/form/en/quick-start.md +121 -0
  41. package/template/content/form/ja/installation.md +63 -0
  42. package/template/content/form/ja/overview.md +71 -0
  43. package/template/content/form/zh-hans/installation.md +63 -0
  44. package/template/content/form/zh-hans/overview.md +71 -0
  45. package/template/content/query/docs.config.json +32 -0
  46. package/template/content/query/en/guides/mutations.md +126 -0
  47. package/template/content/query/en/guides/pagination.md +98 -0
  48. package/template/content/query/en/guides/queries.md +120 -0
  49. package/template/content/query/en/installation.md +78 -0
  50. package/template/content/query/en/overview.md +72 -0
  51. package/template/content/query/en/quick-start.md +108 -0
  52. package/template/content/query/ja/installation.md +78 -0
  53. package/template/content/query/ja/overview.md +72 -0
  54. package/template/content/query/zh-hans/guides/mutations.md +126 -0
  55. package/template/content/query/zh-hans/guides/pagination.md +98 -0
  56. package/template/content/query/zh-hans/guides/queries.md +120 -0
  57. package/template/content/query/zh-hans/installation.md +95 -0
  58. package/template/content/query/zh-hans/overview.md +72 -0
  59. package/template/content/query/zh-hans/quick-start.md +108 -0
  60. package/template/content/router/docs.config.json +18 -0
  61. package/template/content/router/en/guides/routing-concepts.md +131 -0
  62. package/template/content/router/en/installation.md +57 -0
  63. package/template/content/router/en/overview.md +74 -0
  64. package/template/content/router/en/quick-start.md +88 -0
  65. package/template/content/router/ja/installation.md +57 -0
  66. package/template/content/router/ja/overview.md +78 -0
  67. package/template/content/router/zh-hans/guides/routing-concepts.md +131 -0
  68. package/template/content/router/zh-hans/installation.md +57 -0
  69. package/template/content/router/zh-hans/overview.md +81 -0
  70. package/template/content/router/zh-hans/quick-start.md +88 -0
  71. package/template/content/table/docs.config.json +18 -0
  72. package/template/content/table/en/guides/column-definitions.md +135 -0
  73. package/template/content/table/en/installation.md +56 -0
  74. package/template/content/table/en/overview.md +79 -0
  75. package/template/content/table/en/quick-start.md +112 -0
  76. package/template/content/table/ja/installation.md +56 -0
  77. package/template/content/table/ja/overview.md +79 -0
  78. package/template/content/table/zh-hans/installation.md +56 -0
  79. package/template/content/table/zh-hans/overview.md +79 -0
  80. package/template/content/virtual/docs.config.json +18 -0
  81. package/template/content/virtual/en/guides/dynamic-sizing.md +129 -0
  82. package/template/content/virtual/en/installation.md +57 -0
  83. package/template/content/virtual/en/overview.md +74 -0
  84. package/template/content/virtual/en/quick-start.md +114 -0
  85. package/template/content/virtual/ja/installation.md +57 -0
  86. package/template/content/virtual/ja/overview.md +74 -0
  87. package/template/content/virtual/zh-hans/installation.md +57 -0
  88. package/template/content/virtual/zh-hans/overview.md +74 -0
@@ -0,0 +1,189 @@
1
+ ---
2
+ title: Getting Started
3
+ description: Install docs-i18n and translate your first documentation file in minutes.
4
+ ---
5
+
6
+ # Getting Started
7
+
8
+ ## What is docs-i18n?
9
+
10
+ docs-i18n is a universal documentation translation engine. It parses markdown and MDX files into translatable AST nodes, translates them via LLM, caches results in SQLite, and assembles translated output files. It is framework-agnostic and works with any documentation site built on Next.js, Astro, TanStack Start, or similar tools.
11
+
12
+ Key characteristics:
13
+
14
+ - **Incremental** -- only changed or new content is sent to the LLM.
15
+ - **AST-based** -- uses remark to parse markdown into nodes, producing stable MD5 keys for deduplication.
16
+ - **SQLite-backed** -- concurrent-safe cache with WAL mode. No manual save steps.
17
+ - **Multi-project** -- supports multiple documentation projects and versions in a single config.
18
+ - **Admin dashboard** -- web UI for monitoring progress, managing jobs, and previewing translations.
19
+ - **Runtime serve** -- optional D1-compatible translator for SSR sites on Cloudflare Workers.
20
+
21
+ ## How It Works
22
+
23
+ ```
24
+ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐
25
+ │ Your docs │ │ docs-i18n │ │ Translated │
26
+ │ (English) │────→│ translate │────→│ docs (cache) │
27
+ │ .md / .mdx │ │ via LLM │ │ .cache/ │
28
+ └─────────────┘ └─────────────┘ └──────┬───────┘
29
+
30
+ ┌─────────────┐ │
31
+ │ docs-i18n │◄────────────┘
32
+ │ site │
33
+ │ (dev/build) │──→ Multilingual docs site
34
+ └─────────────┘
35
+ ```
36
+
37
+ **Workflow:**
38
+ 1. Write docs in English (`.md` or `.mdx`)
39
+ 2. `docs-i18n translate` sends new/changed content to LLM, caches results in SQLite
40
+ 3. `docs-i18n site` serves a multilingual docs site (assembles translations at runtime)
41
+ 4. `docs-i18n admin` provides a web UI to monitor progress and manage jobs
42
+
43
+ ## Requirements
44
+
45
+ - Node.js 20+ or Bun
46
+ - An API key for an LLM provider (OpenRouter, OpenAI, or Anthropic)
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ npm install docs-i18n
52
+ ```
53
+
54
+ Or with Bun:
55
+
56
+ ```bash
57
+ bun add docs-i18n
58
+ ```
59
+
60
+ ## Quick Start
61
+
62
+ ### 1. Create a configuration file
63
+
64
+ Create `docs-i18n.config.ts` in your project root:
65
+
66
+ ```ts
67
+ import { defineConfig } from 'docs-i18n';
68
+
69
+ export default defineConfig({
70
+ projects: {
71
+ mydocs: {
72
+ sources: {
73
+ latest: 'content/docs',
74
+ },
75
+ },
76
+ },
77
+ languages: ['zh-hans', 'ja', 'es'],
78
+ llm: {
79
+ provider: 'openrouter',
80
+ model: 'deepseek/deepseek-chat-v3-0324:free',
81
+ apiKey: process.env.OPENROUTER_API_KEY,
82
+ },
83
+ });
84
+ ```
85
+
86
+ The `projects` object maps project names to their source directories. Each project can have multiple versions (e.g., `latest`, `v1`, `v2`). The `languages` array lists the target language codes to translate into.
87
+
88
+ ### 2. Scan your source files
89
+
90
+ Before translating, scan your English source files to build the source index:
91
+
92
+ ```bash
93
+ npx docs-i18n rescan
94
+ ```
95
+
96
+ This parses every markdown/MDX file in your source directories, extracts translatable nodes, and stores them in the SQLite cache.
97
+
98
+ ### 3. Check translation status
99
+
100
+ ```bash
101
+ npx docs-i18n status
102
+ ```
103
+
104
+ This displays a progress bar for each language showing how many nodes have been translated.
105
+
106
+ ### 4. Run your first translation
107
+
108
+ ```bash
109
+ npx docs-i18n translate --lang zh-hans
110
+ ```
111
+
112
+ This sends untranslated nodes to your configured LLM, receives translations, and stores them in the cache. Only new or changed content is translated -- cached translations are reused.
113
+
114
+ ### 5. Assemble output files
115
+
116
+ ```bash
117
+ npx docs-i18n assemble
118
+ ```
119
+
120
+ This produces translated markdown files by combining your English sources with cached translations. Output is written to `.cache/content/<version>/<lang>/` by default.
121
+
122
+ ## Minimal Working Example
123
+
124
+ Given this project structure:
125
+
126
+ ```
127
+ my-docs/
128
+ content/
129
+ docs/
130
+ getting-started.md
131
+ api-reference.md
132
+ docs-i18n.config.ts
133
+ package.json
134
+ ```
135
+
136
+ With this config:
137
+
138
+ ```ts
139
+ import { defineConfig } from 'docs-i18n';
140
+
141
+ export default defineConfig({
142
+ projects: {
143
+ docs: {
144
+ sources: { latest: 'content/docs' },
145
+ },
146
+ },
147
+ languages: ['zh-hans'],
148
+ llm: {
149
+ provider: 'openrouter',
150
+ apiKey: process.env.OPENROUTER_API_KEY,
151
+ model: 'deepseek/deepseek-chat-v3-0324:free',
152
+ contextLength: 32768,
153
+ maxTokens: 16384,
154
+ },
155
+ context: 'MyProject is a TypeScript library for building web apps.',
156
+ });
157
+ ```
158
+
159
+ Run these commands:
160
+
161
+ ```bash
162
+ npx docs-i18n rescan
163
+ npx docs-i18n translate --lang zh-hans
164
+ npx docs-i18n assemble --lang zh-hans
165
+ ```
166
+
167
+ The translated files will appear at:
168
+
169
+ ```
170
+ .cache/content/latest/zh-hans/getting-started.md
171
+ .cache/content/latest/zh-hans/api-reference.md
172
+ ```
173
+
174
+ ## Adding npm scripts
175
+
176
+ Add these convenience scripts to your `package.json`:
177
+
178
+ ```json
179
+ {
180
+ "scripts": {
181
+ "i18n": "docs-i18n",
182
+ "i18n:status": "docs-i18n status",
183
+ "i18n:translate": "docs-i18n translate",
184
+ "i18n:assemble": "docs-i18n assemble",
185
+ "i18n:rescan": "docs-i18n rescan",
186
+ "i18n:admin": "docs-i18n admin"
187
+ }
188
+ }
189
+ ```
@@ -0,0 +1,25 @@
1
+ {
2
+ "sections": [
3
+ {
4
+ "label": "Getting Started",
5
+ "children": [
6
+ { "label": "Introduction", "to": "getting-started" }
7
+ ]
8
+ },
9
+ {
10
+ "label": "Usage",
11
+ "children": [
12
+ { "label": "CLI Commands", "to": "cli" },
13
+ { "label": "Configuration", "to": "configuration" },
14
+ { "label": "Admin Dashboard", "to": "admin" }
15
+ ]
16
+ },
17
+ {
18
+ "label": "Advanced",
19
+ "children": [
20
+ { "label": "Architecture", "to": "architecture" },
21
+ { "label": "Deployment", "to": "deployment" }
22
+ ]
23
+ }
24
+ ]
25
+ }
@@ -0,0 +1,151 @@
1
+ ---
2
+ title: Admin Dashboard
3
+ description: Web UI for monitoring translation progress, managing translation jobs, previewing files, and browsing LLM models.
4
+ ---
5
+
6
+ # Admin Dashboard
7
+
8
+ The docs-i18n admin dashboard is a web-based UI for managing your translations. It is built with TanStack Start and React, and runs as a local development server.
9
+
10
+ ## Starting the Dashboard
11
+
12
+ ```bash
13
+ npx docs-i18n admin
14
+ ```
15
+
16
+ The dashboard opens at `http://localhost:3456`. Use `--port` to change the port:
17
+
18
+ ```bash
19
+ npx docs-i18n admin --port 4000
20
+ ```
21
+
22
+ **Prerequisites:**
23
+
24
+ Your project must have `vite` and `@vitejs/plugin-react` installed:
25
+
26
+ ```bash
27
+ npm install -D vite @vitejs/plugin-react
28
+ ```
29
+
30
+ The dashboard reads your `docs-i18n.config.ts` to discover projects, versions, and languages.
31
+
32
+ ## Features
33
+
34
+ ### Translation Overview
35
+
36
+ The main dashboard page shows a grid of all versions and languages with translation progress. For each version/language pair, you see:
37
+
38
+ - Total number of source nodes (EN content units).
39
+ - Number of translated nodes.
40
+ - Percentage complete.
41
+ - Breakdown by section (e.g., `docs/`, `blog/`, `learn/`), showing file counts and node counts per section.
42
+
43
+ English is always shown as 100% complete since it is the source language.
44
+
45
+ The overview auto-scans source files on load. If source files have not been scanned yet, the dashboard triggers a scan automatically.
46
+
47
+ ### File Browser
48
+
49
+ Clicking on a version/language cell opens a file-level coverage list. Each file shows:
50
+
51
+ - File path (relative to the source directory).
52
+ - Total translatable nodes in that file.
53
+ - Number of translated nodes.
54
+
55
+ Files are sorted by path. You can click on any file to open the block-level preview.
56
+
57
+ ### File Preview
58
+
59
+ The file preview shows every block (AST node) in a file side by side:
60
+
61
+ - **Source** -- the original English text.
62
+ - **Translation** -- the cached translation for the selected language, or empty if not yet translated.
63
+
64
+ Each block displays its type (heading, paragraph, list, blockquote, frontmatter, code, html) and MD5 key. Non-translatable blocks (code, pure HTML tags, gaps between nodes) are shown but clearly distinguished.
65
+
66
+ ### Cache Management
67
+
68
+ From the file preview, you can delete individual cache entries. This is useful when a translation is incorrect and you want to re-translate a specific node. After deleting, run the translate command again to get a fresh translation for that key.
69
+
70
+ The dashboard also supports rescanning source files for a specific version via the UI. This rebuilds the source index and cleans orphaned entries.
71
+
72
+ ### Translation Jobs
73
+
74
+ The dashboard includes a job management system for running translations directly from the UI instead of the command line.
75
+
76
+ #### Creating a Job
77
+
78
+ Click the job creation button and configure:
79
+
80
+ - **Language** -- target language code.
81
+ - **Version** -- which version to translate.
82
+ - **Project** -- optionally filter to a specific project.
83
+ - **Model** -- LLM model to use (can be selected from the model browser).
84
+ - **Model rotation** -- optionally provide multiple models to rotate through.
85
+ - **Max chunks** -- limit the number of API call chunks.
86
+ - **Concurrency** -- number of parallel API calls (default: 3).
87
+ - **Files** -- optionally select specific files to translate.
88
+
89
+ #### Job Status
90
+
91
+ Running jobs show:
92
+
93
+ - Status: `running`, `completed`, `failed`, or `cancelled`.
94
+ - Start time and finish time.
95
+ - Number of translated chunks and total chunks.
96
+ - Current chunk being processed.
97
+ - Live log output (last 20 lines displayed, up to 500 lines stored).
98
+
99
+ You can cancel a running job, which sends SIGTERM to the translation process. Completed or failed jobs can be removed from the list.
100
+
101
+ #### How Jobs Work
102
+
103
+ Under the hood, the job manager spawns a child process running the `docs-i18n translate` CLI command with the configured options. It captures stdout and stderr, parses progress information from the output, and exposes it through the dashboard API. The child process inherits the API key from your config or environment variables.
104
+
105
+ ### Model Browser
106
+
107
+ The dashboard includes an OpenRouter model browser that fetches the list of available models from the OpenRouter API. For each model, it displays:
108
+
109
+ - Model ID and name.
110
+ - Pricing (prompt and completion per million tokens).
111
+ - Context length and maximum output tokens.
112
+ - Whether the model supports JSON response format and tool use.
113
+ - Provider name.
114
+ - Whether the model is free.
115
+
116
+ The model list is cached for 5 minutes. It only shows text-to-text models (filtered by architecture modality) and excludes models with negative pricing. Models are sorted by prompt price (cheapest first).
117
+
118
+ This is useful for selecting a model when creating a translation job.
119
+
120
+ ### Open in Editor
121
+
122
+ The dashboard can open source files in your local editor. It tries the following editors in order:
123
+
124
+ 1. The value of the `EDITOR_CMD` environment variable (if set).
125
+ 2. `code` (VS Code)
126
+ 3. `cursor` (Cursor)
127
+ 4. `zed` (Zed)
128
+
129
+ If none are found, it falls back to the system default (`open` on macOS, `xdg-open` on Linux, `start` on Windows).
130
+
131
+ ## Architecture
132
+
133
+ The admin dashboard uses:
134
+
135
+ - **TanStack Start** -- Full-stack React framework with server functions.
136
+ - **TanStack React Query** -- For data fetching and cache management.
137
+ - **TanStack Router** -- For client-side routing.
138
+ - **Vite** -- Dev server and build tool.
139
+ - **Hono** -- HTTP server (used by TanStack Start internally).
140
+
141
+ Server functions are defined in `src/admin/server/functions/` and handle:
142
+
143
+ - `fetchStatus` / `fetchFileCoverage` / `fetchFileBlocks` -- Read translation status from SQLite.
144
+ - `deleteCacheEntry` -- Delete a specific translation from the cache.
145
+ - `rescanVersion` -- Rescan source files for a version.
146
+ - `createJob` / `fetchJobs` / `fetchJob` / `deleteJob` -- Manage translation jobs.
147
+ - `fetchModels` -- Fetch available models from OpenRouter.
148
+ - `fetchVersion` / `fetchConfig` -- Get docs-i18n version and project root.
149
+ - `openFile` -- Open a file in the local editor.
150
+
151
+ The dashboard shares the same `TranslationCache` and `parseMdx` functions as the CLI, ensuring consistent behavior.
@@ -0,0 +1,222 @@
1
+ ---
2
+ title: Architecture
3
+ description: How docs-i18n works internally -- the translation pipeline, AST parsing, caching, and chunking strategies.
4
+ ---
5
+
6
+ # Architecture
7
+
8
+ This document explains how docs-i18n works internally. Understanding the pipeline helps you tune configuration, debug issues, and contribute to the project.
9
+
10
+ ## Translation Pipeline
11
+
12
+ The end-to-end flow is:
13
+
14
+ ```
15
+ Source files (EN)
16
+ |
17
+ v
18
+ [1. Normalize] -- Ensure JSX tags are separated by blank lines
19
+ |
20
+ v
21
+ [2. Parse] -- remark AST -> flat list of typed nodes
22
+ |
23
+ v
24
+ [3. Hash] -- MD5 of each translatable node's text
25
+ |
26
+ v
27
+ [4. Chunk] -- Group nodes into chunks that fit the LLM context window
28
+ |
29
+ v
30
+ [5. Translate] -- Send JSON to LLM, receive JSON translations
31
+ |
32
+ v
33
+ [6. Cache] -- Store translations in SQLite keyed by (lang, md5)
34
+ |
35
+ v
36
+ [7. Assemble] -- EN source + cache -> translated output files
37
+ ```
38
+
39
+ ## Step 1: Normalization
40
+
41
+ The `normalize()` function (`src/core/normalize.ts`) preprocesses MDX content to ensure that JSX tags like `<AppOnly>`, `<PagesOnly>`, `<details>`, and `<div>` are separated from surrounding content by blank lines. This ensures remark parses them as independent HTML nodes rather than merging them with adjacent text.
42
+
43
+ For example, this input:
44
+
45
+ ```
46
+ <AppOnly>
47
+ Some text here
48
+ </AppOnly>
49
+ ```
50
+
51
+ Becomes:
52
+
53
+ ```
54
+ <AppOnly>
55
+
56
+ Some text here
57
+
58
+ </AppOnly>
59
+ ```
60
+
61
+ ## Step 2: AST Parsing
62
+
63
+ The `parseMdx()` function (`src/core/parser.ts`) uses remark to parse markdown into a flat list of `ParsedNode` objects. Each node has:
64
+
65
+ - `type` -- The AST node type: `paragraph`, `heading`, `list`, `blockquote`, `code`, `html`, `thematicBreak`, or `frontmatter`.
66
+ - `rawText` -- The raw text content from the normalized source.
67
+ - `needsTranslation` -- Whether this node contains human-readable text.
68
+ - `md5` -- MD5 hash of the raw text (only for translatable nodes).
69
+ - `startOffset` / `endOffset` -- Character offsets in the normalized content.
70
+
71
+ **Translatable node types:** `paragraph`, `heading`, `list`, `blockquote`, and `html` nodes that contain non-tag text (e.g., `<summary>Examples</summary>`).
72
+
73
+ **Non-translatable:** `code` blocks, `thematicBreak`, and pure HTML/JSX tags (self-closing tags like `<Check size={18} />`, opening/closing tags like `<AppOnly></AppOnly>`).
74
+
75
+ **Frontmatter handling:** If the content starts with `---`, the parser detects YAML frontmatter and emits it as a single `frontmatter` node spanning from the opening `---` to the closing `---`. The frontmatter module (`src/core/frontmatter.ts`) then extracts only the configured translatable fields (e.g., `title`, `description`) using the `yaml` library, sends them to the LLM as plain text, and reconstructs the YAML with translated values while preserving all other fields and formatting.
76
+
77
+ ## Step 3: MD5 Hashing
78
+
79
+ Each translatable node's raw text is hashed with MD5 to produce a stable key. This key is used for:
80
+
81
+ - **Deduplication** -- identical content appearing in multiple files (or even multiple projects) shares a single translation.
82
+ - **Incremental updates** -- when source content changes, only the nodes with new MD5 hashes need translation. Unchanged nodes reuse their cached translations.
83
+ - **Heading differentiation** -- heading nodes include their level markers (`##`, `###`) in the hash, so "## Installation" and "### Installation" produce different keys.
84
+
85
+ ## Step 4: Smart Chunking
86
+
87
+ The translate command groups untranslated nodes into chunks that fit within the LLM's context window. The chunking algorithm (`src/commands/translate.ts`) accounts for:
88
+
89
+ - **Input budget** -- system prompt tokens + source text tokens. Estimated at `text.length / 4 + 80` tokens per node (accounting for JSON structure overhead).
90
+ - **Output budget** -- translated text tokens. Scaled by a per-language multiplier since different languages use tokens differently:
91
+
92
+ | Language | Multiplier | Reason |
93
+ | --- | --- | --- |
94
+ | Japanese, Korean, Hindi, Thai | 2.5x | CJK/Indic tokenization |
95
+ | Russian, Arabic, Ukrainian, Hebrew | 2.0x | Cyrillic/Arabic scripts |
96
+ | Chinese (Simplified/Traditional) | 1.5x | CJK but more concise |
97
+ | German, French, Portuguese | 1.3x | Slightly longer than English |
98
+ | Spanish, Vietnamese | 1.2x | Close to English length |
99
+ | Other languages | 2.0x | Safe default |
100
+
101
+ - **System prompt overhead** -- approximately 700 tokens for the translation prompt.
102
+ - **JSON structure overhead** -- approximately 80 tokens per key for JSON schema properties.
103
+ - **Safety margin** -- 85% of input budget and 75% of output budget are used to leave room for estimation errors.
104
+
105
+ When a chunk reaches its budget, a new chunk is started. This prevents context window overflow and output truncation.
106
+
107
+ ## Step 5: LLM Translation
108
+
109
+ The translator (`src/core/translator.ts`) uses structured JSON mode for translations:
110
+
111
+ **Input format:** A JSON object with a `nodes` array. Each node has a `key` (MD5), `type` (heading/paragraph/list/etc.), and `text` (content to translate).
112
+
113
+ ```json
114
+ {
115
+ "nodes": [
116
+ { "key": "a1b2c3...", "type": "heading", "text": "## Installation" },
117
+ { "key": "d4e5f6...", "type": "paragraph", "text": "Run the following command:" }
118
+ ]
119
+ }
120
+ ```
121
+
122
+ **Output format:** A flat JSON object mapping each key to its translation.
123
+
124
+ ```json
125
+ {
126
+ "a1b2c3...": "## \u5b89\u88c5",
127
+ "d4e5f6...": "\u8fd0\u884c\u4ee5\u4e0b\u547d\u4ee4\uff1a"
128
+ }
129
+ ```
130
+
131
+ **Robustness features:**
132
+
133
+ - **JSON repair** -- Handles common LLM JSON errors: unescaped newlines in strings, trailing commas, missing closing braces.
134
+ - **Thinking block stripping** -- Removes `<think>...</think>` blocks from reasoning models.
135
+ - **Unwrapping** -- If the model wraps output in `{"nodes": {...}}` or `{"translations": {...}}`, it is automatically unwrapped.
136
+ - **Key recovery** -- If the model corrupts an MD5 key (e.g., truncates it), the translator attempts fuzzy matching to recover the translation (up to 3 character differences).
137
+ - **Garbage detection** -- If more than 50% of translation values are identical, the model output is rejected.
138
+ - **Retry with backoff** -- Retries on 429 (rate limit), 503, 405, timeout, and connection errors. Uses exponential backoff starting at 2 seconds.
139
+ - **Model rotation** -- Supports a list of models to rotate through. Dead models (400/404 errors) are skipped. Rate-limited models (429) are deprioritized.
140
+ - **Truncation detection** -- If `finish_reason` is `'length'`, the output was truncated and the request is retried.
141
+
142
+ **Frontmatter translation:**
143
+
144
+ Frontmatter nodes are handled specially. Instead of sending the entire YAML block, the translator extracts individual translatable fields (e.g., `title`, `description`) and sends them as plain `paragraph` type nodes with virtual keys like `fm:<md5>:title`. After translation, the fields are reassembled into the original YAML structure using `reconstructFrontmatter()`.
145
+
146
+ ## Step 6: SQLite Cache
147
+
148
+ The `TranslationCache` class (`src/core/cache.ts`) manages all persistent state in a single SQLite database at `<cacheDir>/translations.db`.
149
+
150
+ **Schema:**
151
+
152
+ ```sql
153
+ -- EN source texts (deduplicated by MD5)
154
+ CREATE TABLE sources (
155
+ key TEXT PRIMARY KEY NOT NULL, -- MD5 hash
156
+ text TEXT NOT NULL, -- original English text
157
+ type TEXT NOT NULL DEFAULT 'paragraph'
158
+ );
159
+
160
+ -- Which files use each source node
161
+ CREATE TABLE source_files (
162
+ key TEXT NOT NULL, -- MD5 hash
163
+ file TEXT NOT NULL, -- relative file path
164
+ line INTEGER NOT NULL, -- line number
165
+ version TEXT NOT NULL DEFAULT 'latest',
166
+ PRIMARY KEY (version, key, file, line)
167
+ );
168
+
169
+ -- Translated texts per language
170
+ CREATE TABLE translations (
171
+ lang TEXT NOT NULL, -- language code
172
+ key TEXT NOT NULL, -- MD5 hash
173
+ value TEXT NOT NULL, -- translated text
174
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
175
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
176
+ PRIMARY KEY (lang, key)
177
+ );
178
+ ```
179
+
180
+ **Performance configuration:**
181
+
182
+ - **WAL mode** -- Write-Ahead Logging enables concurrent readers with a single writer. No locking issues during parallel translation and assembly.
183
+ - **busy_timeout = 5000** -- Wait up to 5 seconds for a lock before failing.
184
+ - **synchronous = NORMAL** -- Balanced between safety and performance.
185
+ - **WITHOUT ROWID** -- Tables use the primary key directly, avoiding an extra rowid column.
186
+ - **Immediate writes** -- All `set()` calls write directly to disk. No explicit save step needed.
187
+
188
+ **Key operations:**
189
+
190
+ - `get(lang, md5)` -- Look up a cached translation.
191
+ - `set(lang, md5, translation)` -- Store or update a translation (upsert).
192
+ - `untranslatedKeys(lang, version)` -- Find all source keys that have no translation for a given language.
193
+ - `fileCoverage(version, lang)` -- Get per-file translation coverage (used by the admin dashboard).
194
+ - `prune(lang, usedMd5s)` -- Remove translations whose keys are no longer referenced.
195
+ - `exportJsonl(lang, outputPath)` / `importJsonl(lang, inputPath)` -- Export/import translations in JSONL format for backup or migration.
196
+
197
+ **SQLite compatibility:**
198
+
199
+ The `openDatabase()` function (`src/core/sqlite.ts`) automatically detects the runtime environment. Under Bun, it uses `bun:sqlite`. Under Node.js, it uses `better-sqlite3`. Both expose the same interface.
200
+
201
+ ## Step 7: Assembly
202
+
203
+ The `assemble()` function (`src/core/assembler.ts`) produces a translated file from English source content and cached translations:
204
+
205
+ 1. Normalizes the source content.
206
+ 2. Parses it into AST nodes.
207
+ 3. For each node:
208
+ - **Non-translatable nodes** (code blocks, HTML tags) are kept as-is.
209
+ - **Translatable nodes with a cached translation** are replaced with the cached value.
210
+ - **Translatable nodes without a cache hit** are either wrapped in `<!-- NEEDS_TRANSLATION -->` markers (for the legacy whole-file translation mode) or fall back to the original English text (for assembled output files).
211
+ 4. Preserves all whitespace and newlines between nodes.
212
+
213
+ The `AssembleResult` includes statistics: `cachedCount`, `uncachedCount`, `totalTranslatable`, and whether all nodes were cached (`allCached`).
214
+
215
+ ## Validation
216
+
217
+ The `validate()` function (`src/core/validator.ts`) compares LLM output against the translation cache to detect and correct modifications to already-cached translations. It uses two alignment strategies:
218
+
219
+ - **Fast path** -- When the number of translatable nodes in the source and output match, nodes are aligned by index.
220
+ - **Anchor-based alignment** -- When node counts differ (the LLM merged or split paragraphs), cached translations serve as anchor points. The validator finds exact matches between output text and cached translations, then aligns nodes between anchors by type matching.
221
+
222
+ Cached translations always override LLM modifications, ensuring translation consistency across runs.