docs-i18n 0.8.2 → 0.8.4

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.
@@ -15572,10 +15572,6 @@ var manifest = {
15572
15572
  functionName: "openFile_createServerFn_handler",
15573
15573
  importer: () => import("./assets/misc-y6t3-UOP.js")
15574
15574
  },
15575
- "5080dc3f2f2309ec6981b94c431969637130c657e8a1dfb10400b4614eecc1ea": {
15576
- functionName: "fetchModels_createServerFn_handler",
15577
- importer: () => import("./assets/models-YNa3F3nn.js")
15578
- },
15579
15575
  "4e218d79545765572808c7eab33b7663d4496209c15406d0b449366905b6b83f": {
15580
15576
  functionName: "fetchStatus_createServerFn_handler",
15581
15577
  importer: () => import("./assets/status-CM7Azp4n.js")
@@ -15611,6 +15607,10 @@ var manifest = {
15611
15607
  "88c2855c84e91504070bfecc50ddfa50339d22c305626800b6d9b05d79385d71": {
15612
15608
  functionName: "deleteJob_createServerFn_handler",
15613
15609
  importer: () => import("./assets/jobs-FXffC7LH.js")
15610
+ },
15611
+ "5080dc3f2f2309ec6981b94c431969637130c657e8a1dfb10400b4614eecc1ea": {
15612
+ functionName: "fetchModels_createServerFn_handler",
15613
+ importer: () => import("./assets/models-YNa3F3nn.js")
15614
15614
  }
15615
15615
  };
15616
15616
  async function getServerFnById(id) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docs-i18n",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Universal documentation translation engine — parse, translate, cache, assemble, manage.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,7 @@ import { MarkdownContent } from '~/components/markdown'
19
19
  import { Toc } from '~/components/Toc'
20
20
  import { Breadcrumbs } from '~/components/Breadcrumbs'
21
21
  import { FallbackBanner } from '~/components/FallbackBanner'
22
- import { useSiteConfig } from '~/utils/site-config'
22
+ import { siteConfig } from '~/site.config'
23
23
  import type { LoadedBlogPost } from '~/utils/blog'
24
24
 
25
25
  type BlogArticleProps = {
@@ -30,7 +30,6 @@ type BlogArticleProps = {
30
30
  }
31
31
 
32
32
  export function BlogArticle({ post, lang, locale }: BlogArticleProps) {
33
- const siteConfig = useSiteConfig()
34
33
  const { title, content, authors, published, filePath, isFallback } = post
35
34
 
36
35
  // Prepend byline to content (matches tanstack.com pattern)
@@ -11,7 +11,7 @@ import { MarkdownContent } from '~/components/markdown'
11
11
  import type { DocsConfig, MarkdownHeading } from '~/types'
12
12
  import { useLocalCurrentFramework } from './FrameworkSelect'
13
13
  import { useParams } from '@tanstack/react-router'
14
- import { useSiteConfig } from '~/utils/site-config'
14
+ import { siteConfig } from '~/site.config'
15
15
 
16
16
  type DocProps = {
17
17
  title: string
@@ -51,8 +51,6 @@ export function Doc({
51
51
  isFallback = false,
52
52
  locale,
53
53
  }: DocProps) {
54
- const siteConfig = useSiteConfig()
55
-
56
54
  // Extract headings synchronously during render to avoid hydration mismatch
57
55
  const { headings, markup } = React.useMemo(
58
56
  () => renderMarkdown(content),
@@ -15,7 +15,8 @@ const fetchDoc = createServerFn({ method: 'GET' })
15
15
  )
16
16
  .handler(async ({ data }) => {
17
17
  const { loadDoc } = await import('~/utils/docs.server')
18
- return loadDoc(data.project, data.version, data.lang, data.slug)
18
+ const projectConfig = findProject(data.project)
19
+ return loadDoc(data.project, data.version, data.lang, data.slug, projectConfig)
19
20
  })
20
21
 
21
22
  export const Route = createFileRoute('/$lang/$project/$version/docs/$')({
@@ -19,11 +19,13 @@ const fetchFrameworkDoc = createServerFn({ method: 'GET' })
19
19
  )
20
20
  .handler(async ({ data }) => {
21
21
  const { loadDoc } = await import('~/utils/docs.server')
22
+ const projectConfig = findProject(data.project)
22
23
  return loadDoc(
23
24
  data.project,
24
25
  data.version,
25
26
  data.lang,
26
27
  `framework/${data.framework}/${data.slug}`,
28
+ projectConfig,
27
29
  )
28
30
  })
29
31
 
@@ -13,7 +13,8 @@ const fetchDocsConfig = createServerFn({ method: 'GET' })
13
13
  .inputValidator((data: { project: string; version: string }) => data)
14
14
  .handler(async ({ data }) => {
15
15
  const { loadDocsConfig } = await import('~/utils/docs.server')
16
- return loadDocsConfig(data.project, data.version)
16
+ const projectConfig = findProject(data.project)
17
+ return loadDocsConfig(data.project, data.version, projectConfig)
17
18
  })
18
19
 
19
20
  export const Route = createFileRoute('/$lang/$project/$version/docs')({
@@ -13,7 +13,8 @@ const fetchDoc = createServerFn({ method: 'GET' })
13
13
  )
14
14
  .handler(async ({ data }) => {
15
15
  const { loadDoc } = await import('~/utils/docs.server')
16
- return loadDoc(data.project, data.version, data.lang, data.slug)
16
+ const projectConfig = findProject(data.project)
17
+ return loadDoc(data.project, data.version, data.lang, data.slug, projectConfig)
17
18
  })
18
19
 
19
20
  export const Route = createFileRoute('/$lang/$project/docs/$')({
@@ -13,7 +13,8 @@ const fetchDocsConfig = createServerFn({ method: 'GET' })
13
13
  .inputValidator((data: { project: string; version: string }) => data)
14
14
  .handler(async ({ data }) => {
15
15
  const { loadDocsConfig } = await import('~/utils/docs.server')
16
- return loadDocsConfig(data.project, data.version)
16
+ const projectConfig = findProject(data.project)
17
+ return loadDocsConfig(data.project, data.version, projectConfig)
17
18
  })
18
19
 
19
20
  export const Route = createFileRoute('/$lang/$project/docs')({
@@ -13,7 +13,8 @@ const fetchDoc = createServerFn({ method: 'GET' })
13
13
  )
14
14
  .handler(async ({ data }) => {
15
15
  const { loadDoc } = await import('~/utils/docs.server')
16
- return loadDoc(data.project, data.version, data.lang, data.slug)
16
+ const projectConfig = getSingleProject()
17
+ return loadDoc(data.project, data.version, data.lang, data.slug, projectConfig)
17
18
  })
18
19
 
19
20
  export const Route = createFileRoute('/$lang/docs/$')({
@@ -19,11 +19,13 @@ const fetchFrameworkDoc = createServerFn({ method: 'GET' })
19
19
  )
20
20
  .handler(async ({ data }) => {
21
21
  const { loadDoc } = await import('~/utils/docs.server')
22
+ const projectConfig = getSingleProject()
22
23
  return loadDoc(
23
24
  data.project,
24
25
  data.version,
25
26
  data.lang,
26
27
  `framework/${data.framework}/${data.slug}`,
28
+ projectConfig,
27
29
  )
28
30
  })
29
31
 
@@ -15,7 +15,8 @@ const fetchDocsConfig = createServerFn({ method: 'GET' })
15
15
  .inputValidator((data: { project: string; version: string }) => data)
16
16
  .handler(async ({ data }) => {
17
17
  const { loadDocsConfig } = await import('~/utils/docs.server')
18
- return loadDocsConfig(data.project, data.version)
18
+ const projectConfig = getSingleProject()
19
+ return loadDocsConfig(data.project, data.version, projectConfig)
19
20
  })
20
21
 
21
22
  export const Route = createFileRoute('/$lang/docs')({
@@ -64,6 +64,7 @@ function getTranslationCache(projectRoot: string): TranslationCache | null {
64
64
 
65
65
  export function createFsLoader(
66
66
  projectRoot: string,
67
+ docsRoot = 'content',
67
68
  urlMapper?: (filePath: string) => string,
68
69
  ): ContentLoader {
69
70
  /** Supported markdown extensions, in priority order. */
@@ -79,14 +80,20 @@ export function createFsLoader(
79
80
  lang: string,
80
81
  slug: string,
81
82
  ): { raw: string; filePath: string } | null {
83
+ // Resolve content base directory using docsRoot from site config.
84
+ // docsRoot encodes the project-specific path (e.g. 'content/query', 'content/docs').
85
+ // Convention: no /en/ subdir — English source files live directly under version/.
86
+ // Non-English comes from .cache/ (translation assembly).
82
87
  const baseDirs = [
83
- resolve(projectRoot, 'content', project, version, lang),
84
- resolve(projectRoot, 'content', project, lang),
85
- resolve(projectRoot, 'content', version, lang),
86
- resolve(projectRoot, 'content', lang),
87
- // Flat structure (no lang subdir): content/{version}/ directly
88
+ resolve(projectRoot, docsRoot, version),
89
+ resolve(projectRoot, docsRoot),
90
+ // Fallback: try content/{project}/{version} pattern
88
91
  resolve(projectRoot, 'content', project, version),
89
92
  resolve(projectRoot, 'content', version),
93
+ // Legacy: with lang subdir
94
+ resolve(projectRoot, 'content', project, version, lang),
95
+ resolve(projectRoot, docsRoot, version, lang),
96
+ resolve(projectRoot, docsRoot, lang),
90
97
  ]
91
98
 
92
99
  if (!urlMapper) {
@@ -62,7 +62,7 @@ export function setContentLoader(loader: ContentLoader) {
62
62
 
63
63
  /**
64
64
  * Load a document with i18n fallback.
65
- * When projectConfig has urlMapper, creates a mapper-aware loader.
65
+ * Uses docsRoot from projectConfig to resolve content paths.
66
66
  */
67
67
  export async function loadDoc(
68
68
  project: string,
@@ -71,11 +71,9 @@ export async function loadDoc(
71
71
  slug: string,
72
72
  projectConfig?: ProjectConfig,
73
73
  ): Promise<LoadedDoc> {
74
- if (projectConfig?.urlMapper) {
75
- const loader = createFsLoader(getProjectRoot(), projectConfig.urlMapper)
76
- return loader.loadDoc(project, version, lang, slug)
77
- }
78
- return getLoader().loadDoc(project, version, lang, slug)
74
+ const docsRoot = projectConfig?.docsRoot || 'content'
75
+ const loader = createFsLoader(getProjectRoot(), docsRoot, projectConfig?.urlMapper)
76
+ return loader.loadDoc(project, version, lang, slug)
79
77
  }
80
78
 
81
79
  /**
@@ -91,11 +89,13 @@ export async function loadDocsConfig(
91
89
  return loadFilesystemSidebar(project, version, projectConfig)
92
90
  }
93
91
 
94
- const config = await getLoader().loadDocsConfig(project, version)
92
+ const docsRoot = projectConfig?.docsRoot || 'content'
93
+ const loader = createFsLoader(getProjectRoot(), docsRoot)
94
+ const config = await loader.loadDocsConfig(project, version)
95
95
  if (config) return config
96
96
 
97
97
  // Auto-scan fallback (filesystem only)
98
- return autoScanDocs(getProjectRoot(), project, version)
98
+ return autoScanDocs(getProjectRoot(), docsRoot, project, version)
99
99
  }
100
100
 
101
101
  /** Generate sidebar from filesystem using numeric prefix ordering. */
@@ -105,14 +105,11 @@ function loadFilesystemSidebar(
105
105
  projectConfig: ProjectConfig,
106
106
  ): DocsConfig {
107
107
  const root = getProjectRoot()
108
+ const docsRoot = projectConfig.docsRoot || 'content'
108
109
  const candidates = [
109
- resolve(root, 'content', project, version, 'en'),
110
- resolve(root, 'content', project, 'en'),
111
- resolve(root, 'content', version, 'en'),
112
- resolve(root, 'content', 'en'),
113
- // Flat structure (no lang subdir)
110
+ resolve(root, docsRoot, version),
111
+ resolve(root, docsRoot),
114
112
  resolve(root, 'content', project, version),
115
- resolve(root, 'content', version),
116
113
  ]
117
114
  for (const dir of candidates) {
118
115
  if (existsSync(dir) && statSync(dir).isDirectory()) {
@@ -124,20 +121,18 @@ function loadFilesystemSidebar(
124
121
  }
125
122
 
126
123
  /**
127
- * Auto-generate sidebar config by scanning .md files in the content directory.
124
+ * Auto-generate sidebar config by scanning .md/.mdx files in the content directory.
128
125
  */
129
126
  function autoScanDocs(
130
127
  root: string,
128
+ docsRoot: string,
131
129
  project: string,
132
130
  version: string,
133
131
  ): DocsConfig {
134
132
  const candidates = [
135
- resolve(root, 'content', project, version, 'en'),
136
- resolve(root, 'content', project, 'en'),
137
- resolve(root, 'content', version, 'en'),
138
- resolve(root, 'content', 'en'),
133
+ resolve(root, docsRoot, version),
134
+ resolve(root, docsRoot),
139
135
  resolve(root, 'content', project, version),
140
- resolve(root, 'content', version),
141
136
  ]
142
137
 
143
138
  for (const dir of candidates) {
@@ -7,33 +7,146 @@ description: How docs-i18n works internally -- the translation pipeline, AST par
7
7
 
8
8
  This document explains how docs-i18n works internally. Understanding the pipeline helps you tune configuration, debug issues, and contribute to the project.
9
9
 
10
+ ## System Overview
11
+
12
+ ```
13
+ ┌─────────────────────────────────────────────────────────────────┐
14
+ │ docs-i18n ecosystem │
15
+ │ │
16
+ │ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌─────────┐ │
17
+ │ │ CLI │ │ Admin │ │ Template │ │ Runtime │ │
18
+ │ │ │ │ Dashboard │ │ (Site) │ │ (D1) │ │
19
+ │ │ translate│ │ (pre- │ │ TanStack │ │ CF │ │
20
+ │ │ rescan │ │ built) │ │ Start │ │ Workers │ │
21
+ │ │ assemble │ │ │ │ │ │ │ │
22
+ │ │ status │ │ Web UI │ │ SSR docs │ │ Edge │ │
23
+ │ └────┬─────┘ └─────┬─────┘ └────┬─────┘ └────┬────┘ │
24
+ │ │ │ │ │ │
25
+ │ └────────┬───────┴───────┬───────┘ │ │
26
+ │ │ │ │ │
27
+ │ ┌──────▼──────┐ ┌─────▼──────┐ ┌─────▼─────┐ │
28
+ │ │ SQLite │ │ Content │ │ D1 │ │
29
+ │ │ .cache/ │ │ content/ │ │ (cloud) │ │
30
+ │ │ translations│ │ {project}/ │ │ │ │
31
+ │ │ .db │ │ {version}/ │ │ translate │ │
32
+ │ └─────────────┘ └────────────┘ │ ions │ │
33
+ │ └───────────┘ │
34
+ └─────────────────────────────────────────────────────────────────┘
35
+ ```
36
+
37
+ ## Monorepo Structure
38
+
39
+ ```
40
+ docs-i18n/
41
+ packages/
42
+ core/ ← Published as "docs-i18n" on npm
43
+ │ src/
44
+ │ cli.ts CLI entry point
45
+ │ core/ Parser, cache, assembler, translator
46
+ │ commands/ translate, rescan, assemble, status, upload
47
+ │ dist/ Built output
48
+
49
+ admin/ ← Pre-built admin dashboard
50
+ │ app/ TanStack Start React app
51
+ │ server/ Server functions (status, jobs, models)
52
+ │ dist/ Pre-built (zero-install runtime)
53
+ │ serve.mjs Node.js HTTP adapter
54
+
55
+ template/ ← Docs site template (built per-project)
56
+ app/ TanStack Start React app
57
+ content/ Demo content + docs-i18n's own docs
58
+ ```
59
+
10
60
  ## Translation Pipeline
11
61
 
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
62
+ ```
63
+ Source .md/.mdx files (English)
64
+
65
+
66
+ ┌──────────────────────────────────────────────────┐
67
+ │ 1. Normalize │
68
+ Ensure JSX tags (<AppOnly> etc.) are separated
69
+ │ by blank lines for correct AST parsing │
70
+ └──────────────────┬───────────────────────────────┘
71
+
72
+
73
+ ┌──────────────────────────────────────────────────┐
74
+ │ 2. Parse (remark AST) │
75
+ │ .md/.mdx → flat list of typed nodes │
76
+ │ heading | paragraph | list | code | html | ... │
77
+ │ Each node: { type, rawText, needsTranslation } │
78
+ └──────────────────┬───────────────────────────────┘
79
+
80
+
81
+ ┌──────────────────────────────────────────────────┐
82
+ │ 3. Hash (MD5) │
83
+ │ "## Installation" "a1b2c3d4..." │
84
+ │ Same content = same key = deduplicated │
85
+ └──────────────────┬───────────────────────────────┘
86
+
87
+
88
+ ┌──────────────────────────────────────────────────┐
89
+ │ 4. Smart Chunking │
90
+ │ Group nodes into chunks fitting LLM context │
91
+ │ Input budget + output budget + language mult. │
92
+ │ CJK: 2.5x │ Cyrillic: 2.0x │ Chinese: 1.5x │
93
+ └──────────────────┬───────────────────────────────┘
94
+
95
+
96
+ ┌──────────────────────────────────────────────────┐
97
+ │ 5. LLM Translation │
98
+ │ Send: { nodes: [{ key, type, text }] } │
99
+ │ Recv: { "a1b2c3": "翻译结果" } │
100
+ │ JSON repair + key recovery + retry + rotation │
101
+ └──────────────────┬───────────────────────────────┘
102
+
103
+
104
+ ┌──────────────────────────────────────────────────┐
105
+ │ 6. Cache (SQLite) │
106
+ │ INSERT INTO translations (lang, key, value) │
107
+ │ WAL mode │ concurrent-safe │ instant writes │
108
+ └──────────────────┬───────────────────────────────┘
109
+
110
+
111
+ ┌──────────────────────────────────────────────────┐
112
+ │ 7. Assemble │
113
+ │ English source + cached translations │
114
+ │ → translated .md/.mdx files │
115
+ │ Missing translations → fallback to English │
116
+ └──────────────────────────────────────────────────┘
117
+ ```
118
+
119
+ ## Content Structure Convention
120
+
121
+ ```
122
+ your-project/
123
+ content/ ← English source (no /en/ subdir)
124
+ {project}/{version}/ ← e.g. query/v5/ or nextjs/latest/
125
+ overview.md
126
+ guides/routing.md
127
+ ...
128
+
129
+ .cache/
130
+ translations.db ← SQLite cache (all languages)
131
+ content/{version}/{lang}/ ← assembled translations
132
+ overview.md
133
+ guides/routing.md
134
+
135
+ docs-i18n.config.ts ← translation config
136
+ site.config.ts ← site template config
137
+ ```
138
+
139
+ The site template resolves content as:
140
+ ```
141
+ Request: /zh-hans/query/v5/docs/overview
142
+ │ │ │
143
+ │ │ └─ slug
144
+ │ └─ version
145
+ └─ project
146
+
147
+ English: content/query/v5/overview.md ← direct read
148
+ zh-hans: .cache/ → translations.db → assemble() ← from cache
149
+ Fallback: English source (isFallback: true)
37
150
  ```
38
151
 
39
152
  ## Step 1: Normalization
@@ -271,6 +271,15 @@ For non-English languages, the site automatically loads translations from the `.
271
271
 
272
272
  ### Full translation pipeline
273
273
 
274
+ ```
275
+ rescan ──→ status ──→ translate ──→ assemble ──→ site dev
276
+ │ │ │ │ │
277
+ ▼ ▼ ▼ ▼ ▼
278
+ Index Progress Send to EN + cache Serve
279
+ source bars LLM, cache → output multilingual
280
+ files results files docs site
281
+ ```
282
+
274
283
  ```bash
275
284
  # 1. Scan source files
276
285
  docs-i18n rescan
@@ -281,8 +290,11 @@ docs-i18n status
281
290
  # 3. Translate
282
291
  docs-i18n translate --lang zh-hans
283
292
 
284
- # 4. Assemble output
293
+ # 4. Assemble output (optional — site does this at runtime)
285
294
  docs-i18n assemble --lang zh-hans
295
+
296
+ # 5. Start docs site
297
+ docs-i18n site
286
298
  ```
287
299
 
288
300
  ### Translate a single file for review
@@ -62,9 +62,20 @@ projects: {
62
62
 
63
63
  The version key (e.g., `latest`, `v1`) is used to organize translations in the cache and in assembled output. The source path points to the directory containing your English markdown/MDX files.
64
64
 
65
- **Single project:**
65
+ ```
66
+ Content directory convention:
67
+
68
+ content/ ← English source (no /en/ subdir)
69
+ {project}/{version}/ ← source path in config
70
+ file.md
71
+ subdir/file.mdx
72
+
73
+ .cache/
74
+ translations.db ← all translations
75
+ content/{version}/{lang}/ ← assembled output
76
+ ```
66
77
 
67
- When you have a single project, version keys are used directly in the database and output paths (e.g., `latest`).
78
+ **Single project:**
68
79
 
69
80
  ```ts
70
81
  projects: {
@@ -74,6 +85,12 @@ projects: {
74
85
  }
75
86
  ```
76
87
 
88
+ ```
89
+ content/docs/ ← source files (English)
90
+ getting-started.md
91
+ api-reference.md
92
+ ```
93
+
77
94
  **Multi-project:**
78
95
 
79
96
  When you have multiple projects, keys become compound: `project/version` (e.g., `query/latest`, `table/v1`).
@@ -82,7 +99,7 @@ When you have multiple projects, keys become compound: `project/version` (e.g.,
82
99
  projects: {
83
100
  query: {
84
101
  sources: {
85
- latest: 'content/query/latest',
102
+ v5: 'content/query/v5',
86
103
  v4: 'content/query/v4',
87
104
  },
88
105
  },
@@ -91,14 +108,16 @@ projects: {
91
108
  latest: 'content/table/latest',
92
109
  },
93
110
  },
94
- blog: {
95
- sources: {
96
- latest: 'content/blog',
97
- },
98
- },
99
111
  }
100
112
  ```
101
113
 
114
+ ```
115
+ content/
116
+ query/v5/overview.md ← query project, v5
117
+ query/v4/overview.md ← query project, v4
118
+ table/latest/overview.md ← table project
119
+ ```
120
+
102
121
  ### `languages`
103
122
 
104
123
  **Required.** An array of target language codes to translate into. These codes are used as identifiers in the cache and output directories.
@@ -100,11 +100,32 @@ jobs:
100
100
  - run: npx docs-i18n translate --lang zh-hans --dry-run
101
101
  ```
102
102
 
103
- ## Admin Dashboard Deployment
103
+ ## Deployment Architecture
104
104
 
105
- The admin dashboard is a TanStack Start application that runs locally. It is designed for development use, not production deployment.
105
+ ```
106
+ ┌──────────────────────────────────────────────────────────┐
107
+ │ Development (local) │
108
+ │ │
109
+ │ docs-i18n admin ──→ Pre-built Node.js server (port 3456)│
110
+ │ docs-i18n site ──→ Vite dev server (port 3000) │
111
+ │ ↕ reads content/ + .cache/ │
112
+ └──────────────────────────────────────────────────────────┘
113
+
114
+ ┌──────────────────────────────────────────────────────────┐
115
+ │ Production (Cloudflare Workers) │
116
+ │ │
117
+ │ docs-i18n site build ──→ .output/ │
118
+ │ docs-i18n site upload ──→ D1 (translations) │
119
+ │ docs-i18n site deploy ──→ CF Workers (SSR) │
120
+ │ │
121
+ │ Request → Worker → fetch EN content → D1 translate │
122
+ │ → SSR render → response │
123
+ └──────────────────────────────────────────────────────────┘
124
+ ```
106
125
 
107
- To start it:
126
+ ## Admin Dashboard
127
+
128
+ The admin dashboard is pre-built and requires no additional dependencies. It runs as a local Node.js server.
108
129
 
109
130
  ```bash
110
131
  npx docs-i18n admin
@@ -112,15 +133,7 @@ npx docs-i18n admin
112
133
  npx docs-i18n admin --port 4000
113
134
  ```
114
135
 
115
- The dashboard starts a Vite dev server pointed at the `src/admin` directory within the docs-i18n package. It reads your `docs-i18n.config.ts` to discover projects, versions, and languages.
116
-
117
- **Prerequisites:**
118
-
119
- Your project must have `vite` and `@vitejs/plugin-react` installed as dev dependencies:
120
-
121
- ```bash
122
- npm install -D vite @vitejs/plugin-react
123
- ```
136
+ It reads your `docs-i18n.config.ts` to discover projects, versions, and languages.
124
137
 
125
138
  ## Runtime Translation with D1
126
139
 
@@ -18,11 +18,32 @@ Key characteristics:
18
18
  - **Admin dashboard** -- web UI for monitoring progress, managing jobs, and previewing translations.
19
19
  - **Runtime serve** -- optional D1-compatible translator for SSR sites on Cloudflare Workers.
20
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
+
21
43
  ## Requirements
22
44
 
23
45
  - Node.js 20+ or Bun
24
46
  - An API key for an LLM provider (OpenRouter, OpenAI, or Anthropic)
25
- - For the admin dashboard: `vite` and `@vitejs/plugin-react` as dev dependencies
26
47
 
27
48
  ## Installation
28
49