Architecture
Architecture overview
The codebase is organised in four layers. Use this section for the mental model; open the source tree when you need file-level detail.
How a sync run fits together
sync (and the individual translate commands) run enabled features in order:
| Step | Command | What it does |
|---|---|---|
| 1 | extract → translate-ui | Scan UI sources → update strings.json → fill flat locale JSON (de.json, …) |
| 2 | translate-svg (optional) | Translate SVG text under config.svg |
| 3 | translate-docs | Translate markdown, MDX, .astro pages; Docusaurus catalog JSON; Nextra _meta / dictionary .ts; VitePress theme catalog |
| 4 | translate-json (optional) | Translate nested JSON leaves under json[] |
Every pipeline follows the same core loop: extract segments → protect syntax → batch → cache lookup or LLM call → write output. Shared services in the middle — config, placeholders, cache, glossary, LlmClient — are described under Shared infrastructure.
Module map
| Layer | Folder | Role |
|---|---|---|
| Entry | src/cli/ | CLI commands: init, extract, mark-html, translate-ui, translate-docs, translate-json, translate-svg, sync, status, dashboard, … |
| Pipelines | src/extractors/ | Segment extraction from JS/TS, HTML markers, markdown, JSON, SVG, .astro |
src/processors/ | Placeholder protection, batching, validation, link rewriting | |
| Shared | src/core/ | Config, types, SQLite cache, prompts, output paths, locale utilities |
src/api/ | LlmClient — provider-agnostic chat client (Vercel AI SDK) with model fallback | |
src/glossary/ | Glossary loading and term hints for prompts | |
src/utils/ | Logger, hashing, ignore parser, display-width tables, .env loader | |
| Your app runtime | src/runtime/ | i18next helpers and display utilities — exported as 'ai-i18n-tools/runtime' (Runtime helpers) |
| Tool UI (dogfooding) | src/i18n/, src/dashboard-app/, src/server/ | Localizes this package's own CLI and Translation Dashboard — separate from your project content (Self-localization) |
Everything intended for programmatic use is re-exported from src/index.ts (Programmatic API).
Pipeline summaries
| Pipeline | Section | Input → output |
|---|---|---|
| UI strings | UI strings internals | Source files → strings.json → flat {locale}.json |
| Documents | Documents internals | Markdown / MDX / .astro / Docusaurus JSON → per-locale files under docs[].outputDir |
| JSON bundles | JSON internals | Nested JSON under json[] → per-locale JSON files |
| SVG | Documents internals — extractors | SVG files under config.svg → translated SVG copies |
UI strings internals
| Step | Component | Result |
|---|---|---|
| 1 | Source files (JS/TS; optional .astro / .html) | Files on disk |
| 2 | UIStringExtractor (i18next-scanner; .astro via ui-string-babel.ts) | Segments keyed by MD5 hash |
| 3 | strings.json | Master catalog: { hash: { source, translated, models?, locations? } } |
| 4 | LlmClient.translateUIBatch() | JSON array of source strings → translations (+ model id per batch) |
| 5 | de.json, pt-BR.json, … | Flat maps: source string → translation (no model metadata) |
UIStringExtractor
Uses i18next-scanner's Parser.parseFuncFromString to find t("literal") and i18n.t("literal") calls in JS/TS files. For .astro sources (when listed in ui.uiExtractor.extensions), ui-string-babel.ts parses frontmatter and template {expression} blocks with @babel/parser and applies the same funcNames rules. Function names and file extensions are configurable via ui.uiExtractor (ui.reactExtractor is a supported alias). extract also merges non-scanner inputs into the same catalog: the project package.json description when includePackageDescription is enabled (default), and each englishName from the bundled ui-languages master catalog (built from sourceLocale + targetLocales) when includeUiLanguageEnglishNames is true (strings already found in source keep precedence; does not read languagesManifestPath). extract also regenerates ui-languages.json at languagesManifestPath. Segment hashes are MD5 first 8 hex chars of the trimmed source string — these become the keys in strings.json.
For .html / .htm sources (when listed in ui.uiExtractor.extensions), extract instead routes the file through html-i18n-marks.ts, which scans data-i18n / data-i18n-title / data-i18n-placeholder marker attributes (configurable via ui.uiExtractor.htmlI18nAttributes). A bare marker takes its source text from the element's own textContent / title / placeholder; a valued marker (data-i18n="Key") uses the value. The same module powers the mark-html command, which inserts the bare markers automatically. HTML files never reach the Babel / i18next-scanner passes.
Plain Astro SSG sites can skip i18next: load flat {locale}.json at build time and resolve t('English') by source-text key (see examples/astro-website/src/i18n/t.ts and UI strings — Astro website).
Plain HTML apps follow the same catalog model with marker attributes instead of t() calls — see Marking HTML for translation.
strings.json
The master catalog has the shape:
{
"a1b2c3d4": {
"source": "The English string",
"translated": {
"de": "Der deutsche Text",
"pt-BR": "O texto em português"
},
"models": {
"de": "anthropic/claude-3.5-haiku",
"pt-BR": "openai/gpt-4o"
},
"locations": [{ "file": "src/app/page.tsx", "line": 51 }]
}
}models (optional) — per locale, which model produced that translation after the last successful translate-ui run for that locale (or user-edited if the text was saved from the Translation Dashboard). locations (optional) — where extract found the string (scanner + package description line; bundled-master englishName strings may omit locations).
extract adds new keys and preserves existing translated / models data for keys still present in the scan (scanner literals, optional description, optional bundled-master englishName). translate-ui fills missing translated entries, updates models for locales it translates, and writes flat locale files.
ui-languages.json manifest — JSON array of { code, label, englishName, direction } (BCP-47 code, UI label, reference englishName, "ltr" or "rtl"). Use generate-ui-languages or extract to build a project file from sourceLocale + targetLocales and the bundled master data/ui-languages-complete.json.
Flat locale files
Each target locale gets a flat JSON file (de.json) mapping source string → translation (no models field):
{
"The English string": "Der deutsche Text",
"Save": "Speichern"
}i18next loads these as resource bundles and looks up translations by the source string (key-as-default model).
UI Translation prompts
buildUIPromptMessages constructs system + user messages that:
- Identify the source and target languages (by display name from
localeDisplayNamesorui-languages.json). - Send a JSON array of strings and request a JSON array of translations in return.
- Include glossary hints when available.
LlmClient.translateUIBatch tries each model in order, falling back on parse or network errors. The CLI builds that list per target locale from localeModels, optional uiModels, and translationModels (see Providers and models).
Documents internals
| Step | Component | Result |
|---|---|---|
| 1 | Markdown / MDX / JSON / .astro files (translate-docs) | Source files |
| 2 | MarkdownExtractor / JsonExtractor / AstroTemplateExtractor | segments[] — typed segments with hash + content |
| 3 | PlaceholderHandler | Protected text — HTML, admonitions, anchors, MDX, URLs, inline code, emphasis masked as tokens |
| 4 | splitTranslatableIntoBatches | batches[] — grouped by count + char limit |
| 5 | TranslationCache lookup | Cache hit → skip; miss → LlmClient.translateDocumentBatch |
| 6 | PlaceholderHandler.restoreAfterTranslation | Final text — placeholders restored |
| 7 | resolveDocumentationOutputPath | Output file — Docusaurus layout or flat layout |
Extractors
All extractors extend BaseExtractor and implement extract(content, filepath): Segment[].
MarkdownExtractor- splits markdown into typed segments:frontmatter,heading,paragraph,code,admonition. YAML frontmatter is classified as non-translatable (slug,id, and other routing keys stay stable). Top-levelexport ...blocks (e.g. React component definitions) are classified as non-translatableothersegments alongside existingimport ...handling. Multi-line blocks starting with a capital JSX tag (e.g. a<Tabs>block) are classified as translatable paragraphs. Non-translatable segments (code blocks, raw HTML) are preserved verbatim.AstroTemplateExtractor- parse-and-replace for.astromarketing pages (translate-docsviatranslateAstroFileindoc-translate.ts). Extracts user-facing HTML text nodes and translatable attributes (alt,title,aria-label,placeholder), plus string literals inside template{expression}blocks when user-facing. Skips frontmatter TypeScript,<script>,<style>, protected attribute/key values, and literals insidet('…'). Reassembly adjusts relative imports when output paths are deeper (e.g.src/pages/de/index.astro). See Astro website pages.JsonExtractor- extracts string values from Docusaurus JSON label files (Docusaurus UI catalogs, not MDX body).SvgExtractor- extracts<text>,<title>, and<desc>content from SVG (used bytranslate-svgfor files underconfig.svg, not bytranslate-docs).html-i18n-marks.ts- a focused HTML tag scanner used byextractfor.html/.htmsources and by themark-htmlcommand.collectHtmlI18nStrings/collectHtmlI18nLocationsreaddata-i18n*marker attributes (bare marker → elementtextContent/title/placeholder; valued marker → the value), andmarkHtmlContentinserts bare markers into leaf text / title / placeholder elements (idempotent, honoursdata-i18n-ignore, skips code-like and mixed-content elements). The sharednormalizeI18nTexthelper keeps build-time keys identical to the browser runtime.
Astro hybrid sites (UI + page HTML)
Plain Astro apps often enable both UI strings and documents in one config (reference: examples/astro-website/):
| Layer | Mechanism | Output |
|---|---|---|
| Template HTML | AstroTemplateExtractor + translate-docs | Per-locale .astro under docs[].outputDir |
Frontmatter / t('…') | ui-string-babel.ts + extract + translate-ui | Flat public/locales/{locale}.json (English source as key) |
The sync command runs enabled steps in order: extract then translate-ui (when features.translateUIStrings) → optional translate-svg → translate-docs → optional translate-json (unless skipped with --no-ui, --no-svg, --no-docs, or --no-json). Init template ui-astro-website scaffolds UI strings only; add docs[] and features.translateDocs for page HTML.
Heading anchor insertion (write-heading-ids CLI)
The write-heading-ids command is a local, non-LLM preprocessor for documentation markdown. Implementation: src/cli/write-heading-ids.ts orchestrates file discovery; src/markdown/write-heading-ids-core.ts parses lines and inserts anchors.
It requires a valid config with at least one docs[] block. For each block it gathers .md / .mdx files under contentPaths, applies the project's .translate-ignore rules (same idea as doc translation), and optionally restricts to a subtree with --path / --file. Each file is transformed with applyHeadingAnchorsToMarkdown: for every flat ATX heading (# … through ###### …) outside fenced code blocks, an empty HTML line <a id="slug"></a> is inserted on the line above when missing or outdated. Slug algorithms match common ecosystems — github (default), bitbucket, gitlab, pymdown (optional Unicode normalisation / percent-encoding flags), azure-devops — so anchor IDs stay consistent with existing tooling (doctoc, PyMdown, etc.). --dry-run reports would-be edits without writing.
This command does not run inside translate-docs or sync; run it explicitly when you want stable fragment IDs in source files before translation or publishing.
Placeholder protection
Before translation, sensitive syntax is replaced with opaque tokens to prevent LLM corruption, applied in this order (restore is the reverse):
- HTML tags and comments (
<strong>,<!-- ... -->, etc.) - lowercase HTML tags from a known allowlist are replaced withtokens. Capitalised JSX tags (<Highlight>,<Tabs>,</Tab>) are handled separately by the MDX layer (step 4). - Admonition markers (
:::note,:::) - only the directive prefix on the opening line is replaced with; any same-line title is left for the model to translate. Restored with exact original text. - Doc anchors (HTML
<a id="…">, Docusaurus heading{#…}) - preserved verbatim. - MDX-only constructs (
src/processors/mdx-placeholders.ts):- MDX comments (
{/* … */}, including Docusaurus heading-id form{/* #my-id */}) replaced with. - Capitalised JSX tags (
<Highlight>,<Tabs>,<TabItem>,<TOCInline />,</Highlight>) - preserved aswith translatable string attributes (label,tooltip,aria-label) rewritten toinside the tag unless the attribute name appears indocs[].protectAttributes;label:inside<Tabs values={[ { label: '…' } ]}>object literals (skippable viadocs[].protectKeys) and<TabItem value="…">(when nolabelattribute exists, skipping lowercase slug-like values) are also extracted. Appended to the segment as||JXA_N: …||lines, merged back byrestoreMdx. - MDX brace expressions (
{frontMatter.title},style={{…}}) - depth-aware matching, replaced with.
- MDX comments (
- Markdown URLs (
](url),src="…") - restored from a map after translation. - Inline code spans (
`code`) and bold-wrapped inline code (**code**) - preserved. - Markdown emphasis (optional, auto-enabled for CJK/RTL locales) - emphasis delimiters masked.
Shared attribute/key protection for Astro templates and MDX JSX is implemented in src/processors/expression-attribute-protection.ts and driven per block by docs[].protectAttributes and docs[].protectKeys (see protectAttributes / protectKeys).
Cache (TranslationCache)
SQLite database (via node:sqlite) stores rows keyed by (source_hash, locale) with translated_text, model, filepath, last_hit_at, and related fields. The hash is SHA-256 first 16 hex chars of normalised content (whitespace collapsed).
On each run, segments are looked up by hash × locale. Only cache misses go to the LLM. After translation, last_hit_at is reset for segment rows in the current translate scope that were not hit. Successful cache hits during doc translation clear stale translation_failures rows for that segment. cleanup runs sync --force-update first, then removes stale segment rows (null last_hit_at / empty filepath), prunes file_tracking keys when the resolved source path is missing on disk (doc-block:…, json-block:…, svg-files:…, etc.), removes translation rows whose metadata filepath points at a missing file, prunes orphaned translation_failures rows, prunes orphaned markdown_source_issues rows whose resolved source path is missing on disk, and drops cache rows for locales absent from config (sourceLocale, root targetLocales, and any per-block docs[] / json[] targetLocales; SQLite only — use purge-locale to delete generated files); it does not back up cache.db unless --backup <path> is passed, which writes a backup to that path first.
The translate-docs command also uses file tracking so unchanged sources with existing, up-to-date outputs can skip work entirely. --force-update re-runs file processing while still using segment cache; --force clears file tracking and bypasses segment cache reads for API translation. When every configured model fails AST validation on a markdown segment, translate-docs can progressively split the segment and retry smaller parts (docs[].segmentSplitting.qualityRetrySplit, default on). See Documents — cache behaviour and flags for the full flag table.
Batch prompt format: translate-docs --prompt-format selects XML (<seg> / <t>) or JSON array/object shapes for LlmClient.translateDocumentBatch only; extraction, placeholders, and validation are unchanged. See Batch prompt format.
Output path resolution
resolveDocumentationOutputPath(config, cwd, locale, relPath, kind) maps a source-relative path to the output path:
nestedstyle (default):{outputDir}/{locale}/{relPath}for markdown.doc-systemstyle: underdocsRoot, outputs use{outputDir}/{locale}/[localeSubpath/]{relativeToDocsRoot}; paths outsidedocsRootfall back to the nested layout. Aliases:docusaurus(defaultlocaleSubpath= Docusaurus plugin path),astro-starlight(default emptylocaleSubpath),vitepress(same asdoc-systemwith emptylocaleSubpath; preserves BCP-47 folder casing).flatstyle:{outputDir}/{stem}.{locale}{extension}. WhenflatPreserveRelativeDiristrue, source subdirectories are kept underoutputDir.- Custom
pathTemplate: any markdown layout using{outputDir},{locale},{LOCALE},{relPath},{stem},{basename},{extension},{docsRoot},{relativeToDocsRoot}. - Custom
jsonPathTemplate: separate custom layout for JSON label files, using the same placeholders. linkRewriteDocsRoothelps the flat-link rewriter compute correct prefixes when translated output is rooted somewhere other than the default project root.
Flat link rewriting
When docsOutput.style === "flat", translated markdown files are placed alongside the source with locale suffixes. Relative links between pages are rewritten so that [Guide](./guide.md) in readme.de.md points to guide.de.md. Controlled by rewriteRelativeLinks (auto-enabled for flat style without a custom pathTemplate). The same pass prepends a per-file depth prefix to non-markdown asset URLs before postProcessing.regexAdjustments runs — see Flat link rewriter.
JSON internals
| Step | Component | Result |
|---|---|---|
| 1 | json[].contentPaths | Files resolved (file, directory, or glob) |
| 2 | NestedJsonExtractor | String leaves selected by keyPolicy (dot paths + minimatch) |
| 3 | PlaceholderHandler + batch + TranslationCache | Cache hit → skip; miss → LlmClient.translateDocumentBatch (shared SQLite) |
| 4 | NestedJsonExtractor.reassemble | Output file via expandJsonBlockOutputPath(outputPathTemplate) |
NestedJsonExtractor(src/extractors/nested-json-extractor.ts) walks arbitrary nested JSON and emits one segment per translatable string leaf.keyPolicy.mode(allowlist,denylist, orboth) filters paths with minimatch on dot notation (bare names likeslugmatch the final key segment).- Cache file tracking uses
json-block:{blockIndex}:{projectRelPath}infile_tracking(samecacheDiras docs and SVG). - Not for Docusaurus
write-translationscatalogs ({ message, description }shape) — those use Documents (docs[].docusaurusCatalogDir+JsonExtractorinsidetranslate-docs). - Not for
t()UI strings — UI strings (strings.json+ flat bundles). - CLI:
translate-json; orchestration insrc/cli/translate-json-run.ts. Init template:ui-json-bundles.
Shared infrastructure
LlmClient
Provider-agnostic chat client built on the Vercel AI SDK (ai + @ai-sdk/openai-compatible). It resolves the active provider from provider / providers, builds one OpenAI-compatible client (createOpenAICompatible) for that provider's baseUrl + API key, and routes all calls through generateText. OpenRouterClient is kept as a deprecated alias. Key behaviours:
- Model fallback: tries each model in the resolved list in order; falls back on request or parse failures. Each target locale gets its own resolved chain:
localeModels(locale)first when configured, thenuiModels(UI pipelines only), thentranslationModels. Document, JSON, and SVG translation create a per-locale client with the non-UI chain. Thebench-modelscommand instead builds one single-model client per configured id (union oftranslationModels,uiModels, andlocaleModels;translationModels: [id], no fallback) so it can time and price each model independently. - Request timeout: the active provider's
requestTimeoutMs(default 30 seconds) aborts each request viaAbortSignal.timeout. The same value applies toGET /modelswhen the CLI loads a provider's model list forcheck-models(any provider). The optional pre-flight filter that drops unknown model ids runs only when the active provider is OpenRouter. - OpenRouter extras (only when
openrouteris active): throughput routing via theproviderrequest field,HTTP-Referer/X-Titleheaders, and exact USD cost read fromusage.cost. Token usage is reported for every provider; exact cost only when the provider returns it. - Debug traffic log: if
debugTrafficFilePathis set, appends request and response JSON to a file.
Config loading
loadI18nConfigFromFile(configPath, cwd) pipeline:
- Read and parse
ai-i18n-tools.config.json(JSON). mergeWithDefaults- deep-merge withdefaultI18nConfigPartial, and merge anydocs[].sourceFilesentries intocontentPaths.expandTargetLocalesFileReferenceInRawInput- coercetargetLocalesto an array and reject path-like entries (must be BCP-47 codes, not a path toui-languages.json);languagesManifestPathdefaults to{ui.flatOutputDir}/ui-languages.jsonduringmergeWithDefaults.expandDocumentationTargetLocalesInRawInput- same for eachdocs[].targetLocalesentry.expandJsonTargetLocalesInRawInput- same for eachjson[].targetLocalesentry.parseI18nConfig- Zod validation +validateI18nBusinessRules.applyProviderOverrideToRawInput- when-P/--provideris passed on the CLI.applyEnvOverrides- applyOPENROUTER_BASE_URL,OLLAMA_BASE_URL,I18N_SOURCE_LOCALE, andI18N_TARGET_LOCALESwhen set (API keys are resolved separately per provider insideLlmClient).augmentConfigWithUiLanguagesMaster- attach manifest display names from the bundled master catalog.assertEffectiveLocalesInUiLanguagesMaster- validate locale codes against the master catalog when applicable.
init writes starter configs from initConfigTemplates: ui-markdown (UI + optional app markdown), ui-docusaurus, ui-starlight, ui-vitepress (VitePress docs + vitepressThemeCatalog), ui-nextra (Nextra docs + nextraDictionaryPath), ui-astro-website (plain Astro UI; add docs[] for .astro page translation), ui-json-bundles (JSON json[] only). See Quick start — Initialise.
Logger
Logger supports debug, info, warn, error levels with ANSI colour output. Verbose mode (-v) enables debug. When logFilePath is set, log lines are also written to that file.
Self-localization (tool UI)
The tool localizes its own UI — CLI help, high-traffic log/summary/error messages, and the Translation Dashboard — separately from the content it translates for you.
- Locale resolution (
resolveUiLocaleinsrc/core/ui-locale.ts): picks the UI locale from-L/--ui-lang>AI_I18N_LANG> configuiLanguage> host OS locale (Intl.DateTimeFormat().resolvedOptions().locale). The candidate is normalized and matched against the shipped bundle set exactly or by closest variation (e.g.pt-PT→pt-BR,en-US→en-GB), falling back to the source locale (en-GB). The CLI resolves once before help is built (pre-parse argv scan) and again after config load souiLanguageapplies (the flag and env var still win). - Runtime (
src/i18n/index.ts): a minimalt(source, vars)withinterpolation, keyed by the English source string against flat per-locale bundles insrc/i18n/locales/<code>.json(copied todist/i18n/localesat build). Missing keys or bundles return the source text. This is the same key-as-default model as UI strings — there is no hash lookup. - Dashboard: the server exposes
GET /api/ui-i18nreturning{ locale, dir, bundle }for the resolved UI locale; the frontend sets<html lang>/dirand localizes static markup viadata-i18n*attributes. - Dogfooding: the bundles are produced by running the package's own extract →
translate-uipipeline againstai-i18n-self.config.json(pnpm i18n:self). Catalog keys come fromt()calls acrosssrc/cli/andsrc/i18n/plus the dashboard'sdata-i18n*markers insrc/dashboard-app/index.html.
Extension points
Custom function names (UI extraction)
Add non-standard translation function names via config:
{
"ui": {
"uiExtractor": {
"funcNames": ["t", "i18n.t", "translate", "i18n.translate"],
"extensions": [".js", ".jsx", ".ts", ".tsx", ".astro", ".html"],
"htmlI18nAttributes": ["data-i18n", "data-i18n-title", "data-i18n-placeholder"]
}
}
}(ui.reactExtractor is a fully supported alias for ui.uiExtractor.)
Add .html / .htm to extensions to scan HTML marker attributes during extract. ui.uiExtractor.htmlI18nAttributes is optional and defaults to ["data-i18n", "data-i18n-title", "data-i18n-placeholder"]; data-i18n maps to the element textContent and data-i18n-<attr> maps to that attribute's value (e.g. data-i18n-aria-label).
Custom extractors
Implement ContentExtractor from the package:
import { BaseExtractor, type Segment } from 'ai-i18n-tools';
class MyExtractor extends BaseExtractor {
readonly name = 'my-format';
canHandle(filepath: string) { return filepath.endsWith('.myext'); }
extract(content: string, filepath: string): Segment[] { /* … */ }
reassemble(segments: Segment[], translations: Map<string, string>): string { /* … */ }
}Register custom extractors by extending the public extractor classes exported from 'ai-i18n-tools' (for example subclass MarkdownExtractor). The CLI wires built-in extractors internally; there is no supported deep import of doc-translate.ts.
Custom output paths
Use docsOutput.pathTemplate for any file layout:
{
"docs": [
{
"docsOutput": {
"pathTemplate": "{outputDir}/{locale}/{relativeToDocsRoot}"
}
}
]
}Source tree
Full src/ layout (file-level reference)
src/
├── index.ts Public API re-exports
│
├── cli/
│ ├── index.ts CLI entry point (commander)
│ ├── extract-strings.ts `extract` command implementation
│ ├── mark-html.ts `mark-html` command (insert bare `data-i18n*` markers into HTML)
│ ├── translate-ui-strings.ts `translate-ui` command implementation
│ ├── doc-translate.ts `translate-docs` command (documentation files only)
│ ├── translate-json-run.ts `translate-json` command (`json[]` nested locale bundles)
│ ├── translate-svg.ts `translate-svg` command (SVG files from `config.svg`)
│ ├── write-heading-ids.ts `write-heading-ids` command (markdown heading anchors)
│ ├── bench-models.ts `bench-models` command (per-model translate latency/token/cost benchmark)
│ ├── helpers.ts Shared CLI utilities
│ └── file-utils.ts File collection helpers
│
├── markdown/
│ └── write-heading-ids-core.ts Slug styles + `<a id="…">` insertion for `write-heading-ids`
│
├── core/
│ ├── types.ts Zod schemas + TypeScript types for all config shapes
│ ├── config.ts Config loading, merging, validation, init templates
│ ├── cache.ts SQLite translation cache (node:sqlite)
│ ├── prompt-builder.ts LLM prompt construction for docs and UI strings
│ ├── output-paths.ts Docusaurus / flat output path resolution
│ ├── ui-languages.ts ui-languages.json loading and locale resolution
│ ├── ui-locale.ts Resolve the tool's own UI locale (flag/env/config/OS → shipped bundle)
│ ├── locale-utils.ts BCP-47 normalisation, locale list parsing, script/Han-variant validation
│ └── errors.ts Typed error classes
│
├── extractors/
│ ├── base-extractor.ts Abstract base class for all extractors
│ ├── ui-string-extractor.ts JS/TS source scanner (i18next-scanner + Babel for `.astro`)
│ ├── ui-string-babel.ts Babel-based `t()` discovery in `.astro` frontmatter and `{expression}` blocks
│ ├── ui-string-locations.ts Source locations for extracted UI strings
│ ├── html-i18n-marks.ts HTML `data-i18n*` marker scanner + `mark-html` annotator
│ ├── classify-segment.ts Heuristic segment type classification
│ ├── markdown-extractor.ts Markdown / MDX segment extraction
│ ├── markdown-segment-split.ts Optional segment splitting for long markdown blocks
│ ├── frontmatter-fields.ts Selective YAML front matter field translation
│ ├── astro-template-extractor.ts `.astro` parse-and-replace (HTML + template expressions; used by `translate-docs`)
│ ├── json-extractor.ts Docusaurus catalog JSON extraction (`translate-docs`)
│ ├── nested-json-extractor.ts Arbitrary nested JSON leaves (`translate-json`, `json[]`)
│ └── svg-extractor.ts SVG text extraction
│
├── processors/
│ ├── placeholder-handler.ts Chain: HTML → admonitions → anchors → MDX → URLs → emphasis
│ ├── expression-attribute-protection.ts Shared protected attribute/key lists (Astro + MDX JSX)
│ ├── url-placeholders.ts Markdown URL protection/restore
│ ├── admonition-placeholders.ts Docusaurus admonition protection/restore
│ ├── anchor-placeholders.ts HTML anchor / heading ID protection/restore
│ ├── html-tag-placeholders.ts Lowercase HTML tag / comment protection ({{HTM_N}})
│ ├── mdx-placeholders.ts MDX comments, JSX tags, brace expressions, JSX attribute extraction
│ ├── batch-processor.ts Segment → batch grouping (count + char limits)
│ ├── validator.ts Post-translation structural checks
│ └── flat-link-rewrite.ts Relative link rewriting for flat output
│
├── api/
│ ├── llm-client.ts LlmClient: provider-agnostic chat client (AI SDK) with model fallback chain
│ └── provider-models-catalog.ts Fetch/parse any provider's OpenAI-compatible GET /models catalog
│
├── glossary/
│ ├── glossary.ts Glossary loading (CSV + auto-build from strings.json)
│ └── matcher.ts Term hint extraction for prompts
│
├── runtime/
│ ├── index.ts Runtime re-exports
│ ├── template.ts interpolateTemplate, flipUiArrowsForRtl
│ ├── ui-language-display.ts getUILanguageLabel, getUILanguageLabelNative
│ └── i18next-helpers.ts RTL detection, i18next setup factories
│
├── i18n/ Self-localization runtime for the tool's own UI
│ ├── index.ts t(source, vars) + bundle/manifest loaders (keyed by English source string)
│ └── locales/ Shipped UI bundles (de.json, es.json, …; generated by `pnpm i18n:self`)
│
├── dashboard-app/
│ ├── index.html Translation Dashboard static UI (HTML/CSS/JS)
│ ├── app.js
│ └── styles.css
│
├── server/
│ └── translation-dashboard.ts Express app for Translation Dashboard (cache / strings.json / glossary)
│
└── utils/
├── logger.ts Leveled logger with ANSI support
├── hash.ts Segment hash (SHA-256 first 16 hex)
├── table.ts Display-width aware table rendering (CJK/emoji column alignment)
├── load-dotenv.ts Auto-load `.env` from the cwd at CLI startup (never overrides existing env)
└── ignore-parser.ts .translate-ignore file parser