| name | flutter-l10n-translate |
| description | Translate untranslated ARB keys via ChatGPT API with parallel agents. Use when adding translations for new locales, translating new strings, or running translations as part of the l10n pipeline. Incremental.
|
| user-invocable | true |
| allowed-tools | Read Write Edit Bash Grep Glob |
| paths | ["**/pubspec.yaml","**/l10n/*.arb"] |
Flutter L10n Translate — ChatGPT Translation Runner
You generate and maintain a Python script (scripts/flutter_translate.py) that
translates Flutter ARB locale files using the OpenAI ChatGPT API.
When to Use
/flutter-l10n translate [--locale=ja] [--all] [--force] [--dry-run]
- Part of the full
/flutter-l10n pipeline (step 4)
- When user says "translate locales", "run translation", "translate to Japanese"
Default Supported Locales (20)
en, vi, ja, ko, zh-Hans, zh-Hant, th, id, ms, fil,
hi, fr, es, pt, de, it, ru, ar, tr, uk
Configured in l10n.yaml. The en locale is always the source — never translated.
Execution Steps
Step 1: Ensure Python Script Exists
Check if scripts/flutter_translate.py exists. If not, generate it.
If it exists, verify it's up to date with the current spec.
The script should:
- Read
app_en.arb as the source of truth for all keys and English values
- Read product context — the script builds a product context block by scanning
for all available product documentation. Read each file that exists and extract
relevant context for translators:
docs/proposal.md — product vision, problem statement, target users.
Extract: app name, one-line description, core value proposition, target audience.
docs/product-guide.md — end-user documentation describing features and
flows in plain language. Extract: feature descriptions, user-facing terminology,
how the app is described to users.
docs/design.md — theme name, mood, visual identity, tone descriptors.
Extract: theme name, mood line, any tone/voice guidance, UI vocabulary.
docs/glossary.md — domain terms with definitions and tier tags.
Extract: term definitions for translator context (e.g., what is a "Star"?
what is "The Constellation"?). Also used for glossary tiers below.
docs/tone.md (optional) — dedicated tone/voice guide if it exists.
docs/spec/*.md — feature specifications. Scan for any that contain
user-facing terminology, screen names, or domain concepts that translators
need to understand. Do not include implementation details.
CLAUDE.md (fallback) — if the above docs are sparse or missing, read
the project overview section for basic product context.
The script should glob for docs/**/*.md and read any product-relevant files
it finds, not just a hardcoded list. New docs added to the project should
automatically contribute context without requiring script changes.
The product context block is injected into every translation prompt so the AI
understands what the app is, who it's for, and what tone to use. Without this
context, translations will be generic and miss domain-specific nuance. If zero
product context files are found, stop and warn the user before proceeding — the
quality difference between contextualized and uncontextualized translations is large
enough that running without context is effectively wasted API spend.
- Read
docs/glossary.md to build term lists by tier:
[keep] terms → pass as "DO NOT translate" list
[translate] terms → pass as "MUST translate natively" list with hints
[technical] terms → excluded (never in user-facing text)
- Terms without a tier tag → default to
[translate]
- Read
scripts/l10n_term_map.json for per-locale canonical native equivalents of [translate] terms. Inject the target locale's terms into the prompt so the AI uses the exact word agreed upon (e.g., French "project" = "projet"). If a new [translate] term has empty locale entries, the script should still instruct the AI to translate it natively — the AI's choice becomes the new canonical entry (update the term map after the run).
- Read each target
app_<locale>.arb to find untranslated keys:
- Keys where
@key metadata has "x-translated": false
- Keys present in
app_en.arb but missing entirely from the target file
- Skip keys where
"x-translated": true (unless --force is used)
Step 2: For New Locales — Clone Structure
When a target locale file doesn't exist yet (e.g., after add-locale):
- Clone
app_en.arb structure to app_<locale>.arb
- Copy all keys with their English values as placeholders
- Copy all
@key metadata blocks
- Set
"@@locale": "<locale_code>"
- Set ALL
"x-translated": false in metadata
- Remove
"x-source" from metadata (only belongs in en)
Step 3: Translate Locales (Parallel)
When --all is used, translate locales in parallel using concurrent agents.
- Load shared context once:
app_en.arb, glossary terms, tone/design docs
- Identify which locales have untranslated keys (skip locales at 100% coverage)
- Group locales into batches of 4-5 for parallel execution
- Launch one agent per locale in each batch — each agent:
- Receives the shared context (English keys, glossary, tone)
- Calls the Python translate script for its assigned locale
- Validates results and writes to the locale's ARB file
- Returns a per-locale report (translated count, skipped, failed, cost)
- Wait for the batch to complete, then launch the next batch
- Merge all per-locale reports into the final report
Why batch in groups of 4-5: Avoids overwhelming the ChatGPT API rate limits
while still achieving significant speedup over sequential (19 locales → 4 batches
instead of 19 sequential runs).
When --locale=ja is used: Single locale — no parallelism needed, run inline.
When --dry-run is used: No API calls — can run all locales in parallel since
it's just file reads and counting.
Step 4: Call ChatGPT API (Per Agent)
Each agent translates its assigned locale:
Batch size: 50 keys per API call (to stay within token limits)
System prompt for ChatGPT (TWO-PASS workflow):
The prompt instructs the model to do two internal passes inside a single API call —
PASS 1 produces a draft, PASS 2 silently reviews and rewrites every string against 6
explicit criteria, and only the reviewed output is returned. Naming the passes
explicitly (rather than appending a soft "self-check" note) gives reasoning models
like gpt-5-mini a clearer steer to actually review before committing output.
You are a NATIVE SPEAKER of {target_language_name} who has lived your entire life
in a region where {target_language_name} is the primary language. You also have
professional experience localizing mobile apps. Your job is to translate UI strings
from English so they read as if they were originally written by another native
speaker — not as a translation.
You will use a TWO-PASS workflow internally before returning your final output:
PASS 1 — TRANSLATE: Produce an initial translation of every key, applying the rules
below.
PASS 2 — REVIEW (silent, internal): Re-read every translation you just produced as
if you were a different native speaker reviewing a colleague's draft. For EACH
string, ask yourself:
a. "Does this sound like something a real {target_language_name} speaker would
actually say in this context, on a mobile screen?" If it sounds translated,
stiff, awkward, or like a literal calque from English — REWRITE it.
b. "Is the tone consistent with the product (see PRODUCT CONTEXT)?" If too
formal, too casual, or off-brand — adjust.
c. "Are domain terms rendered in their natural native equivalent (per the
[translate] list)? Are any English words leaking through that shouldn't be?"
If yes — fix.
d. "Does it preserve ALL ICU syntax and ALL placeholders exactly as in the
English source?" If broken — restore.
e. "Is it concise enough for a mobile screen (≤ 1.5x English length where
possible)?" If too long — tighten without losing meaning.
f. "Is it consistent with terminology already used in the 'For reference'
section of the user prompt?" If divergent — align.
Only after PASS 2 produces a translation you would personally publish in a shipped
product, include it in your output JSON. Do not output the PASS 1 draft. Do not
include the review notes in the response — only the final, reviewed translations.
PRODUCT CONTEXT (from docs/proposal.md, docs/design.md, docs/glossary.md):
{product_context_block}
This context tells you what the app is, who uses it, and what tone to match.
Use it to make translations feel native to the product — not generic.
CRITICAL RULES:
1. Match the product's tone: {tone_description}
2. KEEP these terms exactly as-is in English (brand/product names):
{comma_separated_keep_terms}
3. TRANSLATE these terms into their natural {target_language_name} equivalent
(do NOT keep the English word — use the native word):
{comma_separated_translate_terms_with_hints}
4. Preserve ALL ICU MessageFormat syntax exactly as-is:
- Plural: {count, plural, one{...} other{...}}
- Select: {gender, select, male{...} female{...} other{...}}
- Nested: any combination of the above
5. Preserve ALL placeholders exactly: {userName}, {count}, {date}, etc.
6. Keep translations concise — these appear on mobile screens with limited space.
7. Translations must sound natural in {target_language_name}, not like literal
word-for-word translation from English. If a literal translation sounds
awkward to a native ear, REPHRASE freely while preserving meaning and intent.
8. NEVER mix English nouns into {target_language_name} sentences unless the term
is in the [keep] list above. If unsure of the native equivalent for a domain
term, use the most common {target_language_name} word used in mobile/app
contexts for the project's domain.
9. For formal/informal address: use {formality_level} register.
10. Stay consistent with terminology in the "For reference" section of the user
prompt — do not introduce synonyms for terms already translated in prior batches.
OUTPUT FORMAT:
Return a JSON object mapping each key to its FINAL, REVIEWED translated string
(the output of PASS 2, not the PASS 1 draft).
Do not include metadata keys (starting with @).
Do not add any explanation, review notes, or PASS 1 drafts — return ONLY the JSON
object containing the reviewed translations.
User prompt per batch:
{
"screenTitle": "My Tasks",
"saveChangesButton": "Save Changes",
"itemCount": "{count, plural, one{1 item} other{{count} items}}"
}
API parameters:
- Model:
gpt-5-mini (default) or gpt-5 (via --model flag)
- Temperature: conditional — see "Reasoning vs sampling models" below
- Response format: JSON object
Reasoning vs sampling models (CRITICAL):
The OpenAI gpt-5 family is a reasoning model family. It accepts only the default
temperature value (1) — passing any other value (e.g. 0.3) returns a
400 BadRequestError from the API, with a message indicating the unsupported
temperature value and that only the default (1) is accepted. This applies to both
--model=gpt-5-mini (default) and --model=gpt-5.
The script MUST conditionally include temperature only when the model supports it:
def call_openai(client, model, system_prompt, user_payload):
kwargs = {
"model": model,
"response_format": {"type": "json_object"},
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": json.dumps(user_payload, ensure_ascii=False)},
],
}
if not model.startswith("gpt-5"):
kwargs["temperature"] = 0.3
return client.chat.completions.create(**kwargs)
Why temperature=0.3 was chosen historically: deterministic-ish translations across
batches so the same English string maps to the same target string. With reasoning
models, that goal is met by the model's internal determinism plus the "For reference"
consistency context — no sampling knob needed.
Note: the same startswith("gpt-5") guard also covers any future gpt-5 variants
(gpt-5-pro, etc.). If/when adding o-series support (o1, o3, o4), extend the
guard — those are also reasoning models with the same temperature restriction.
Consistency context:
- Before each batch, include up to 20 existing translations from the locale's ARB file
in the user prompt as a "For reference" section. This prevents the AI from using
synonyms across batches (e.g., "projet" in batch 1, "programme" in batch 2).
Retry logic:
- Up to 3 retries per batch with exponential backoff (2s, 4s, 8s)
- If a batch fails after 3 retries, log the failed keys and continue with next batch
- Report all failed keys at the end
Post-batch validation:
- After each successful batch, validate that no
[translate] domain terms were kept
as English (word-boundary regex match against the term map)
- Keys that fail this check are retried individually with an enhanced prompt:
"IMPORTANT: Replace '{english_term}' with '{native_equivalent}'."
- If retry also fails, the key is marked as failed in the report
Step 5: Write Translations (Per Agent)
For each successfully translated key:
- Write the translated value to
app_<locale>.arb
- Set
"x-translated": true in the @key metadata
- Preserve all other metadata (
description, placeholders)
Step 6: Merge Reports and Present Results
Collect per-locale reports from all agents and present:
## Translation Report
**Model:** gpt-5-mini
**Locales translated:** 19 (4 parallel batches)
**Keys per locale:** 12 new, 3 updated
| Locale | Translated | Skipped | Failed | Cost (est.) |
|--------|-----------|---------|--------|-------------|
| vi | 15 | 127 | 0 | $0.002 |
| ja | 15 | 127 | 0 | $0.003 |
| ... | ... | ... | ... | ... |
**Total estimated cost:** $0.04
**Failed keys:** none
Run `flutter gen-l10n` to regenerate Dart classes.
Run `/flutter-l10n status` to verify coverage.
Python Script Specification
CLI Interface
python scripts/flutter_translate.py --all
python scripts/flutter_translate.py --locale=ja
python scripts/flutter_translate.py --all --dry-run
python scripts/flutter_translate.py --locale=ja --force
python scripts/flutter_translate.py --all --model=gpt-5
python scripts/flutter_translate.py --all --api-key=sk-...
Required Python Packages
openai>=1.0.0
The script should have a requirements.txt or use only the openai package.
No other external dependencies.
Script Structure
"""
Flutter ARB Translation Script
Translates untranslated keys in Flutter ARB locale files using ChatGPT API.
Incremental: only processes keys marked x-translated:false or missing.
"""
API Key Resolution
The script resolves the OpenAI API key in this order:
--api-key CLI argument (highest priority)
OPENAI_API_KEY environment variable
.env file in the project root (read manually, no python-dotenv dependency)
For .env fallback: the script reads the .env file line by line, looks for
OPENAI_API_KEY=sk-..., strips quotes and whitespace, and uses it. This avoids
requiring python-dotenv as a dependency. Example .env line:
OPENAI_API_KEY=sk-proj-abc123...
The script must never log or print the API key value — only confirm that a key was found
and its source (e.g., "Using API key from .env file"). API keys in logs create a permanent
credential exposure risk that outlasts the run.
Error Handling
- Missing
OPENAI_API_KEY (not in CLI args, env, or .env) → clear error message with instructions
- Missing
app_en.arb → error: "Source locale file not found"
- Missing
docs/glossary.md → warning, proceed without glossary protection
- Missing ALL product context files (
docs/proposal.md, docs/design.md,
docs/glossary.md, docs/tone.md, AND CLAUDE.md) → ERROR: refuse to
translate. Print: "No product context found. Translations require at least one
of: docs/proposal.md, docs/design.md, docs/glossary.md, docs/tone.md, or
CLAUDE.md. Without product context, translations will be generic and miss
domain-specific tone and vocabulary."
- Missing some but not all product context files → warning listing which are missing,
proceed with what's available
- API rate limit → exponential backoff retry
- Invalid JSON response from ChatGPT → retry with explicit JSON instruction
- Placeholder corruption in translation → validate before writing, reject and retry
Validation (Post-Translation)
Before writing any translation:
- Placeholder check: All
{placeholders} from English must exist in translation
- ICU syntax check: If English has
{x, plural, ...}, translation must too
- Length check: Warn if translation is >2x the English length (mobile UI concern)
- Glossary check: No-translate terms must appear unchanged in translation
ICU Placeholder Validation Pitfalls:
The placeholder validator must only match ASCII identifiers ([a-zA-Z_][a-zA-Z0-9_]*)
as placeholder names — this is a load-bearing constraint, not a style preference. Common bugs:
\w+ in regex matches Unicode characters — Japanese, Chinese, Arabic text inside
ICU plural braces will be falsely flagged as "placeholders." Use [a-zA-Z_] instead.
- Content words inside ICU branches are NOT placeholders.
one{yesterday} contains
the word "yesterday" as display text, not a {yesterday} placeholder. The validator
must only extract names at the top level of the ICU expression, not inside
nested branch content.
- Best approach: parse brace depth. Only extract identifiers at depth 0 (top-level
{name} or {name, plural, ...}). Ignore identifiers inside nested braces.
Script Runtime Notes
- Run with
PYTHONUNBUFFERED=1 or python3 -u to see progress in real-time.
Without this, output is buffered and invisible during long background runs.
- The script should use
print(..., flush=True) for all progress output.
If validation fails for a key, skip it and add to the "failed keys" report.
Incremental Guarantees
- Running translate twice with no changes = 0 API calls (all keys already translated)
- Adding 5 new keys to
app_en.arb → translate processes only those 5 keys × N locales
- Changing 1 key's value in
app_en.arb (after extract resets x-translated) →
translate re-processes only that 1 key × N locales
- Adding a new locale → translate processes all keys for that locale only
--force re-translates everything (use sparingly — costs money)