Node.js CLI tool for managing i18n translation workflows in Angular projects using LLMs (Large Language Models).
- Description
- Features
- Prerequisites
- Installation
- Project Structure
- Configuration
- Quick Start
- Commands
- Manual Steps (Recovery)
- Recommended Setup: DeepSeek
- Other LLM Providers
- Manual Editing
- Validation
- Security
- Development
- Troubleshooting
Angular i18n Translator automates the translation of XLIFF 1.2 (XLF) files used by Angular's internationalization system. It converts the XLF file to CSV, translates it through any OpenAI-compatible LLM API, and converts the result back to one XLF file per language — while respecting interpolations, placeholders, and ICU message formats.
The CSV file is a first-class citizen of the workflow: it is intentionally easy to open, review, and edit by hand before or after the automatic translation.
messages.xlf --> messages.csv --> batches/*.csv --> LLM translation
--> batches/translated/<lang>/*.csv --> messages.translated.csv --> dist-i18n/*.xlf
- XLF 1.2 to CSV conversion with one column per target language
- Placeholder-safe round-trip: inline placeholders (
<x id="..."/>,<g>, ...) and ICU formats survive the full pipeline as real markup, never as escaped text - Strict input handling: malformed XML, XLIFF 2.0 files, and empty files fail fast with clear errors instead of producing corrupt output
- LLM translation through any OpenAI-compatible API, with retries and exponential backoff
- Batch processing with configurable size and concurrency; languages are processed sequentially, batches in parallel
- Id-based result matching: translations are matched back to records by
id, so rows dropped or reordered by the model are reported as failures instead of silently corrupting other records - Resume support: already translated batches are skipped unless
--forceis used - Honest reporting: the exit code reflects real failures, and untranslated rows are listed explicitly
- Validation of interpolations
{{var}}, inline placeholders, ICU structure, duplicate IDs, and translation coverage
- Node.js >= 18.0.0 (requires native
fetch) - An API key from an OpenAI-compatible LLM provider (DeepSeek, OpenAI, Groq, Ollama, ...)
- Angular CLI to extract i18n strings (
ng extract-i18n)
cd angular-i18n-translator
npm install
# Configure environment variables
cp .env.example .env
# Edit .env and set LLM_API_KEY (and optionally LLM_MODEL / LLM_BASE_URL)Then either run the interactive wizard or edit i18n.config.json manually:
npm run initangular-i18n-translator/
├── src/
│ ├── index.js # CLI entry point and command handlers
│ ├── config.js # Configuration loading (.env resolution, caching)
│ ├── config-schema.js # Zod schema for i18n.config.json
│ ├── csv-converter.js # XLF parsing/generation and XLF <-> CSV conversion
│ ├── batch-manager.js # Split / translate / merge of CSV batches
│ ├── llm-client.js # HTTP client for OpenAI-compatible LLM APIs
│ ├── validator.js # CSV validation (interpolations, ICU, IDs, coverage)
│ ├── cleaner.js # Cleanup of generated artifacts
│ ├── languages.js # Language code/name mappings
│ ├── errors.js # Typed errors with suggestions
│ ├── cli/ui.js # Colors and spinners
│ └── commands/init.js # Interactive configuration wizard
├── test/ # Automated tests (node --test)
├── batches/ # Generated: pending and translated batches
├── dist-i18n/ # Generated: translated XLF files
├── .env.example # Environment variables template
├── i18n.config.json # Main configuration
├── messages.xlf # Source XLF file (from Angular)
└── package.json
{
"languages": [
{ "code": "en", "name": "English", "file": "messages.en.xlf" },
{ "code": "es", "name": "Spanish", "file": "messages.es.xlf" }
],
"sourceLanguage": "en",
"sourceFile": "messages.xlf",
"csvOutput": "messages.csv",
"outputDir": "dist-i18n",
"batchDir": "batches",
"llm": {
"baseURL": "${LLM_BASE_URL}",
"apiKey": "${LLM_API_KEY}",
"model": "${LLM_MODEL}",
"batchSize": 50,
"concurrency": 5,
"timeoutMs": 300000,
"requestExtra": { "thinking": { "type": "disabled" } }
}
}Values like ${LLM_API_KEY} are resolved from the environment (.env). If a referenced variable is not set, the tool fails immediately and tells you exactly which variable is missing.
| Field | Type | Description |
|---|---|---|
languages |
Array | Supported languages: code, name, and output file |
sourceLanguage |
String | Source language code (must be present in languages) |
sourceFile |
String | Source XLF file extracted from Angular (default messages.xlf) |
csvOutput |
String | Intermediate CSV file (default messages.csv) |
outputDir |
String | Directory for translated XLF files (default dist-i18n) |
batchDir |
String | Directory for translation batches (default batches) |
llm.baseURL |
String | Base URL of the OpenAI-compatible API |
llm.apiKey |
String | API key (prefer ${LLM_API_KEY}) |
llm.model |
String | Model identifier |
llm.batchSize |
Number | Records per batch (default 50) |
llm.concurrency |
Number | Parallel batches per language (default 5) |
llm.timeoutMs |
Number | Request timeout in ms (default 300000) |
llm.requestExtra |
Object | Extra fields merged into the API request body (provider-specific) |
llm.systemPrompt |
String | Optional custom system prompt. If omitted, a built-in CSV-aware prompt is used that always names the target language |
Note on
requestExtra: it is merged verbatim into the request body. Use it for provider-specific options such as DeepSeek's thinking mode (see below). Most other providers need no extra fields.
After installation, the whole workflow is two commands:
npm run init # first time only: creates i18n.config.json interactively
npm run translate # does everything: XLF -> CSV -> LLM -> merged CSV -> XLFThe full sequence is:
-
Extract i18n strings from Angular and copy the file next to this tool:
ng extract-i18n --output-path src/locale --out-file messages.xlf
Only XLIFF 1.2 is supported (the Angular CLI default); XLIFF 2.0 files are rejected with a clear error.
-
Run the pipeline:
npm run translate
One command runs all five steps: XLF to CSV, batch splitting, LLM translation, merge, and CSV to XLF. Languages are processed one after another (to avoid saturating the provider's rate limits), while the batches of each language run in parallel according to
llm.concurrency. If a step fails, the pipeline stops with a clear error telling you what to fix. -
Review and validate:
npm run validate
-
Copy the translated files to your Angular project:
cp dist-i18n/*.xlf your-angular-project/src/locale/
You normally never need the individual step commands: they exist for recovery when something fails. See Manual Steps (Recovery).
npm run translate is safe to re-run: already translated batches are skipped. Use npm run translate -- --force to re-translate everything. If the source CSV shrinks between runs, stale batch files are removed automatically.
| Command | Description |
|---|---|
npm run init |
Interactive configuration wizard (first time only) |
npm run translate |
Full pipeline (recommended): XLF to CSV, split, translate, merge, CSV to XLF |
npm run validate |
Validate the translated CSV |
npm run clean |
Remove batches, output directory, and CSV files |
npm test |
Run the automated test suite |
Global options: --quiet, --verbose, --version, --help.
The pipeline stops with an actionable error when something fails. If you need to inspect or repeat a single step, these are the individual commands it runs internally, in order:
| Command | Step | Description |
|---|---|---|
npm run xlf-to-csv |
1 | Convert the XLF file to CSV |
npm run translate:split |
2 | Split the CSV into batches |
npm run translate:run |
3 | Translate batches with the LLM (-- --force to re-translate) |
npm run translate:merge |
4 | Merge translated batches into messages.translated.csv |
npm run csv-to-xlf |
5 | Convert the translated CSV to one XLF per language |
Typical recovery flows:
# The LLM step failed (e.g. rate limit). Fix the cause and resume:
npm run translate:run # skips batches already translated
npm run translate:merge
npm run csv-to-xlf
# Or simply re-run the pipeline; it resumes where it left off:
npm run translateDeepSeek's V4 models use thinking mode by default: for a 50-record batch, reasoning can consume ~20k extra tokens and several minutes per request. Translation does not need it, so disable it:
{
"llm": {
"baseURL": "https://api.deepseek.com",
"model": "deepseek-v4-flash",
"apiKey": "${LLM_API_KEY}",
"requestExtra": { "thinking": { "type": "disabled" } }
}
}Measured impact on a 49-record batch with deepseek-v4-flash:
| Setting | Time | Total tokens |
|---|---|---|
| thinking enabled (default) | ~150 s | ~23,800 |
| thinking disabled | ~10 s | ~4,000 |
The init wizard applies this setting automatically when you select DeepSeek.
Any provider implementing the OpenAI chat completions format works (POST {baseURL}/chat/completions with Bearer auth):
| Provider | baseURL |
|---|---|
| DeepSeek | https://api.deepseek.com |
| OpenAI | https://api.openai.com/v1 |
| Groq | https://api.groq.com/openai/v1 |
| OpenRouter | https://openrouter.ai/api/v1 |
| Ollama (local) | http://localhost:11434/v1 |
Anthropic's native API is not OpenAI-compatible and is not supported directly; use an OpenAI-compatible gateway if needed.
The CSV is designed to be edited by hand at any point:
messages.csvis generated from the XLF with every language column pre-filled with the source text. Edit it before translating if you want to fix source strings.messages.translated.csvcontains the merged translations. Edit it freely, then runnpm run csv-to-xlfto regenerate the XLF files.- Quoting is standard RFC 4180: fields containing commas, quotes, or newlines are quoted, and embedded quotes are doubled (
"").
Rows whose translation is identical to the source are flagged by validate and by the merge step, so loanwords and genuinely identical translations never hide silently.
npm run validate checks the translated CSV and reports, per language:
- Interpolations:
{{variable}}present in the source must appear in the translation - Inline placeholders:
<x id="..."/>,<g>, etc. must be preserved - ICU formats:
{var, plural, ...}structure and brace balance - Duplicate IDs
- Coverage: rows that are empty or identical to the source are reported as untranslated
The exit code is non-zero when errors are found, so it can be used in CI.
NEVER commit the
.envfile or hardcode API keys ini18n.config.jsonif you version that file.
.envis excluded from Git via.gitignore- Reference variables in config with the
${LLM_API_KEY}syntax - Rotate any key that was committed by accident
- Use API keys with the minimum required permissions
npm testThe test suite (test/) runs with node --test and covers the XLF/CSV round-trip, batch processing against a mock LLM server, configuration loading, validation rules, and the CLI end-to-end (no network access required).
The core modules (csv-converter.js, batch-manager.js, llm-client.js, config.js, validator.js) are plain importable ES modules decoupled from the CLI, so they can be reused from other frontends.
A ${VAR} placeholder in i18n.config.json has no value. Create/complete your .env file (see .env.example).
The message lists every invalid field with its path (e.g. sourceLanguage: ...). Fix the reported fields in i18n.config.json.
Re-extract with the Angular CLI default format: ng extract-i18n --format xlf.
The XLF file is malformed. Regenerate it with ng extract-i18n; the tool refuses to process broken XML to avoid corrupt output.
Invalid or expired API key. Check .env.
Too many concurrent requests. Reduce llm.concurrency or llm.batchSize. The tool honors Retry-After automatically.
Increase llm.timeoutMs. Reasoning models on large batches can need several minutes; consider disabling thinking mode (DeepSeek) or reducing batchSize.
The model dropped rows. The batch is retried automatically; if it keeps failing, the batch fails without corrupting other records. Re-run translate:run (translated batches are skipped) or use a more capable model.
Run npm run validate to get the exact rows, fix them in messages.translated.csv, and regenerate with npm run csv-to-xlf.
npm run translate:run -- --force # re-translate everything
npm run clean # or start from scratchWhen passing flags through npm, use
--before the flag:npm run translate:run -- --force.
MIT License - Free for personal and commercial projects.