translate
The single entry point for translating anything — a whole codebase's i18n, a WordPress
theme/plugin's gettext catalog, a folder of Markdown docs, a batch of JSON files, a subtitle
file — into any set of target languages. It drives the three-tier translator panel and
applies a domain specialization so terminology is right for the material.
The panel (why three tiers)
- Lead (
translate-lead, Opus) — orchestrates the run, dispatches each batch to a worker,
and adversarially reviews every result against a fixed C1–C7 checklist before signing off.
Runs the final blind-spot sweeps. Loads the specialization module so its terminology check is
domain-aware.
- Senior (
translate-senior, Sonnet) — translates domain-prose and any substantive surface.
Spawned by the Lead, not directly by this skill.
- Junior (
translate-junior, Haiku) — translates only low-risk UI chrome. Spawned by the Lead.
This skill computes the scope + per-batch tier classification, then spawns the Lead once; the
Lead handles all worker spawns and reviews. The single final build/verify runs here (Step 6).
Invocation
/translate # translate the changed-since-last-run set into the configured targets
/translate --path <dir> # point at a project/folder on disk, e.g. --path C:\Projects\MyApp
/translate <path> # shorthand for --path <path> (a folder) or --files <path> (a file/glob)
/translate --to <langs> # e.g. --to de,fr,pt-BR (overrides configured targets for this run)
/translate --from <lang> # override the detected/configured source language
/translate --domain <name> # specialization: general | technical | marketing | legal | finance | medical | ecommerce | travel | government | scientific | <custom> (default from config, else general). Layer with a comma-list: --domain technical,finance (first = primary)
/translate --formality <f> # register for the whole run: formal | informal | auto (overrides config.formality; default auto)
/translate --files <glob|paths> # translate an explicit set (a folder, a glob, named files)
/translate --out <mode> # output layout: inplace (default) | tree | catalog (see Step 0.5)
/translate --full # translate the ENTIRE translatable surface, not just the diff
/translate --no-gates # skip the project gates (config.gates) for this run (see Step 6.7)
Arguments compose: /translate --path C:\Projects\MyApp --to de,fr --domain technical.
Settings & the specialization setting
Configuration is resolved in this order (later wins):
translation.config.json at the project root (defaults — see below).
- Any target-project convention file it points to.
- Flags on this invocation.
translation.config.json (all fields optional):
{
"sourceLang": "en",
"targetLangs": ["de", "fr", "es"],
"specialization": "general",
"formality": "auto",
"glossary": "glossary.csv",
"doNotTranslate": ["Colour/hex codes and size tokens like 42x2 are pass-through data — leave verbatim.", "Keep placeholders {name}, {id}, {remote} intact."],
"include": ["src/i18n/**", "content/**", "languages/**"],
"exclude": ["**/node_modules/**", "**/*.min.*"],
"verifyCmd": "npx tsc --noEmit",
"buildCmd": "",
"creditInCommit": false,
"wordpress": { "textdomain": "", "makeJson": false, "makeMo": false }
}
Specialization is a per-run setting, not a hardcoded domain. If the user names one
(--domain technical), use it. Otherwise use translation.config.json → specialization.
Otherwise default to general. The chosen module lives at specializations/<name>.md and is
passed to the Lead + Senior so their terminology (C2) and framing (C6) checks match the material.
If --domain <name> names a module that doesn't exist, list the available modules and ask which to
use (or offer to run general).
Layering (2+ domains). The specialization may be a list — a comma-list on the flag
(--domain technical,finance) or a JSON array in config ("specialization": ["technical", "finance"]).
Resolve it to an ordered list of module names (flag wins over config, as usual; a single name stays a
one-element list). Then:
- Validate every named module exists (
specializations/<name>.md); if any is missing, list the
available modules and ask — don't silently drop it.
- The first module is primary; the rest are secondary layers.
- Pass the whole ordered list to the Lead (
specialization = the list, specialization_path = the
ordered list of module paths — see Step 3). The Lead concatenates the modules into one layered brief,
prefixed with a precedence preamble: "You are operating under N layered specializations, primary
first: [names]. The primary owns register/framing (C6) on any conflict. Every layer's terminology
(C2) and verbatim/do-not-translate (C3) rules apply — union them. If two layers give directly
conflicting framing that the primary order doesn't settle, translate to the safer reading and log a
query (high-stakes)."
- Keep it to compatible domains. If the user layers opposites (e.g.
marketing + legal), warn
once that their framing rules conflict and suggest separate scoped runs, then proceed primary-first if
they confirm.
What this skill does NOT do
- Push, or touch a protected branch. It stages/edits files and (optionally) commits to the
working branch; pushing and any deploy stay human.
- Change source facts or add features. The panel translates/repairs copy only — it never adds
keys/components/logic, and never edits numbers, dates, names, or citations. A string missing
because a key is missing is an implementation bug — flag it, don't paper over it.
- Add a brand-new UI locale to a codebase from scratch. That's the sibling
/translate-add-locale skill
(scaffold the language wiring), which then hands off here for the real translation.
Bash discipline (HARD RULES — every step)
- Each command is its own Bash call. Never chain with
&&/;/||, never pipe with |,
never cd <dir> && …, never cosmetic echo separators. Independent calls run in parallel in
one message.
- No shell for output processing or control flow. No
python -c/awk/jq/sed pipelines,
no > /tmp/file && parse-back, no heredocs for logic, no shell loops/branches. Iterate and
branch in context — Glob/Grep/Read once, walk the result in your head. To check "does
<lang>/<file> exist for every language?" → ONE Glob call; compare against the language set.
- File searches/counts use
Grep/Glob, never grep | wc, ls | grep, find | head,
git … | grep -c.
The one legitimate && is a HEREDOC commit at Step 7 (git commit -m "$(cat <<'EOF' … EOF)") — a
single command with quoted content, not a chain.
Step 0 — Preflight
- Locate the project root. Resolve in this order:
a.
--path <dir> / a bare <path> argument (an absolute computer path like C:\Projects\MyApp
is fine — accept it as given).
b. Else, if the current folder has a translation.config.json, use the current folder.
c. Else offer a project picker from the registry: read the toolkit's projects/registry.json
(git-ignored/local — treat a missing file the same as an empty one).
If it lists projects, AskUserQuestion "Which project should I translate?" with each registered
project as an option (label = name, description = path + langs + last run), plus an "Other
(enter a path)" path. If exactly one project is registered, offer it as the default. If the
registry is empty, ask for a path, or suggest running /translate-init first to set one up.
All scope globs (include/exclude, --files) are relative to the chosen root. Read that root's
translation.config.json; if none exists, detect below and offer to write a starter config (or to
run /translate-init) at the end.
1b. Load project memory. Read the toolkit's projects/registry.json and find the entry whose
path matches this project root. If found, read its projects/<slug>/notes.md — the terminology
decisions, do-not-translate list, format quirks, and "what done means here" are run context; pass
the relevant parts to the Lead in its brief (as project_conventions alongside any in-project
contract). Collect the do-not-translate rules from config.doNotTranslate plus any
do-not-translate items in notes.md, and pass the merged list to the Lead as do_not_translate
(the brief field in Step 3) — these are the manual pass-through/verbatim instructions the panel
enforces. If there's no registry entry, note it — you'll offer to register the project at the
end (Step 9), and continue this run using config + flags. (Setup via /translate-init is the
normal way to register, but a /translate run on an unregistered project still works.)
- If the root is a git repo AND output mode is
inplace (the default) or catalog: these
write into the working tree, so note the branch and any uncommitted changes (git -C <root> status --porcelain, one call). If dirty, list the files and ask whether to proceed, stash, or abort — so
staging by name at Step 7 doesn't entangle unrelated WIP. A clean tree needs no prompt.
- Output mode
tree writes into a fresh translations/<lang>/ subtree and never edits
originals, so it needs no dirty-tree prompt even in a git repo. Plain non-git folders skip git
entirely.
Step 0.5 — Resolve the output layout
Resolve --out, else config.output.mode, else default inplace:
inplace (default): write each translation as a sibling next to its source. If the source
filename encodes the source language, swap that code for the target (en.json → de.json,
messages.en.ts → messages.de.ts, in the same folder); otherwise append the code before the
extension (guide.md → guide.de.md). Originals are never overwritten (the target is a different
filename). Good for document and catalog sets where each language file lives beside its source.
tree (the right choice when you want translated copies isolated from the originals): for each
target language, copy every in-scope source file to
<root>/<config.output.dir>/<lang>/<relative-path> (default dir translations, so
<root>/translations/de/…), then translate the copy in place. Originals are never touched.
Always add <dir>/** to exclude so a re-run doesn't translate its own output. Do the copy with
Read+Write (or a single cp Bash call per file — no chaining); create parent dirs as needed.
The Lead/Senior then edit the copied files.
- Rewrite the source-language code in the path to the target language as you copy — otherwise
you'd leave
en.json sitting inside a de/ folder. Rewrite an exact language-code filename
stem/suffix and an exact path segment, only where it stands alone as the language code
(never inside another word like content or engine):
en.json → de.json; messages.en.ts → messages.de.ts; guide.en.md → guide.de.md
- a
…/en/… path segment → …/de/… (e.g. content/en/home.md → content/de/home.md)
strings-en.xml / app-en.strings → strings-de.xml / app-de.strings
- WordPress:
<textdomain>-en_US.po → <textdomain>-de_DE.po (use the target's WP locale form)
A file whose name carries no language code (e.g. guide.md, README.md) keeps its name —
the parent <lang>/ folder already marks the language. Record, per copied file, both the target
path (renamed) and the source path it came from, so the workers read the source and write the
renamed copy.
catalog: for an existing i18n message-catalog project, edit the per-language files that
already exist (messages.<lang>.ts, locales/<lang>.json, languages/<textdomain>-<locale>.po)
in place — fill missing keys, fix leftovers, correct terminology; don't create copies. This is the
mode for a codebase whose language files are already wired into the build.
When --out isn't given, use config.output.mode, else the default inplace — with one
smart exception: if the project already has a per-language catalog/tree (e.g. de.json already
sits next to en.json, or messages.de.ts exists), prefer catalog so you edit the real files
the build uses instead of writing de.de.json siblings. State which mode you chose in Step 9.
Step 1 — Detect source language, formats, and the translatable surface
Use Glob/Grep/Read (never shell loops). Determine:
Source language. From config sourceLang, else --from, else infer from the file layout
(messages.en.ts, en/, -en.po, .en.md) or a quick content sample. Confirm via
AskUserQuestion only if genuinely ambiguous.
Target languages. From --to, else config targetLangs, else (for an existing i18n tree)
every non-source language already present, else ask.
Formats present, and how each fans out:
| Format |
Detect |
Fan-out shape |
| JS/TS message catalog |
messages.<lang>.ts, locales/<lang>.json |
one file per language, mirrored keys |
JSON / .arb / i18next |
<lang>.json, translation.json |
one file per language |
| WordPress gettext |
languages/*.pot, *-<locale>.po |
.pot template → one <textdomain>-<locale>.po per language |
| WordPress JSON |
*-<locale>-<md5>.json |
regenerated from the .po (see Step 6) |
.po / .pot (generic gettext) |
*.po, *.pot |
one .po per language from the .pot |
| Markdown / MDX tree |
content/en/**, *.en.md |
one file per language, mirrored path |
| HTML / XML / XLIFF |
*.html, *.xlf |
<target> filled per language |
| Subtitles |
*.srt, *.vtt |
one file per language |
| Spreadsheet / CSV |
*.csv, *.xlsx |
designated text columns per language |
| Standalone docs |
*.md, *.docx, *.txt |
one output file per language |
Record the detected formats; the Lead/Senior apply the matching file-format rules.
Scope:
--full → the entire translatable surface: every target language × every translatable
file. Never silently narrow it to one language/file. This is the expensive, from-scratch or
drift-catching case.
--files/<path> → exactly that set (× the target languages).
- Default (incremental) → what changed since the last run. In a git repo, that's
<marker>..HEAD committed and uncommitted, where the marker is the untracked file
.translate-last-review at the project root (git -C <repo> diff --name-only <base> +
git -C <repo> ls-files --others --exclude-standard, two parallel calls). No marker (first
run) or stale SHA → default the base to HEAD (review only uncommitted work); if the tree is
also clean, tell the user and offer --full or a base ref. For non-git sets, "incremental"
isn't available — translate --files/whole folder.
- Fan-out completeness (don't rely on the diff alone). For mirrored-tree formats (Markdown/
MDX/HTML/
.po), a plain diff only surfaces a changed source. Adding a new target language
changes no existing source file, so also include every (language, file) where the source
exists but the target is missing or carries a stale source_hash. Derive this with one
Glob per format and compare the returned target list against the language set in context.
This is what makes a run after /translate-add-locale fill the new language's whole existing page set.
If the translatable set is empty (nothing changed, or only non-translatable files): say so in
one line, skip the panel and the commit, and still advance the marker (Step 8) so this range
isn't re-scanned.
Step 2 — Order into batches and classify each tier
Split the translatable set into ordered batches of at most 3 files each, one target language
per batch. In tree and inplace modes the batch's files are the renamed target paths the skill pre-created
(the copy under translations/<lang>/… for tree, or the sibling next to the source for inplace),
with the matching source path recorded so the worker reads the source and overwrites the copy. In
catalog mode the files are the existing per-language files. Order deterministically (by format,
then path) so a restart is reproducible. A
fan-out source (an MDX/HTML/.po template that produces one output per language) counts as one
batch item per (source, language) — never split one source's languages across batches
arbitrarily; keep a source's set together where practical.
Classify each batch's suggested_tier:
- Domain-prose surfaces (documents, marketing/legal/medical copy, MDX/HTML bodies, message
values with substantive strings) → Senior.
- Message-catalog batches → inspect the diff. If every touched entry is pure chrome (nav,
footer, buttons, generic errors, format fields) with no domain term / citation / identity token →
Junior. Otherwise → Senior. A brand-new/entirely-untranslated file → Senior regardless.
- Fail-safe: anything ambiguous → Senior.
Step 2.5 — Terminology research (conditional)
Decide whether to run the translate-researcher before translating. Run it when any holds:
--research was passed (force a refresh), OR
config.research is always, OR
config.research is first-run (default) AND this project/target-language pair has no glossary
yet (no projects/<slug>/glossary.csv, or it lacks rows for a target language in scope).
Skip it when config.research is off, or when first-run and a glossary already covers every
target language (reuse the saved glossary — research is a once-per-language cost, not per-run).
When running it, spawn ONE translate-researcher (Agent tool) with a brief: project_root, slug,
the context (from config.context — inline text or the contents of the file it points to),
source_lang, target_langs, specialization_path, glossary_path (projects/<slug>/glossary.csv),
queries_path (projects/<slug>/queries-<date>.md), a content_sample (you pick the high-signal
files — headings, catalogs, nav/labels — not the whole surface), and formats. It writes/merges the
glossary and logs low-confidence terms to the queries file, then returns a summary. Pass the resulting
glossary_path to the Lead in Step 3. If the researcher can't resolve a language at all, note it and
continue — the panel still runs against the specialization.
Step 3 — Spawn the Lead once
Agent({ subagent_type: "translate-lead", prompt: <brief> })
Brief:
run_id: <e.g. tr-2026-07-31-1>
mode: incremental | full | files
project_root: <absolute path, e.g. C:\Projects\MyApp>
output_mode: tree | inplace | catalog
output_dir: translations # for tree mode
source_lang: <lang>
target_langs: [<...>]
specialization: <name> # or an ORDERED list [primary, ...] when layering
specialization_path: specializations/<name>.md # or the ordered list of module paths when layering;
# the Lead loads all, primary first (see "Layering" in Step 1)
context: <config.context — inline text or the contents of translation-context.md; the product's
purpose/audience/register, so the panel picks the right sense of each word>
formality: { <lang>: formal | informal | auto, ... } # resolved PER target language (see below)
do_not_translate: [<config.doNotTranslate rules, verbatim — the manual pass-through/verbatim
instructions the panel must treat as absolute and add to the C1/C3 exempt list>]
glossary_path: projects/<slug>/glossary.csv # the research pass's output (or config.glossary)
queries_mode: report | high-stakes | off # from config.queries (default report)
queries_path: projects/<slug>/queries-<date>.md
project_conventions: <target project's CLAUDE.md / i18n contract path, or "">
verify_cmd: <config.verifyCmd or "">
batch_list:
- id: B1
files: [<target path to edit — the copy in tree mode>]
source_files: [<the source path to read from>] # tree/inplace: differs from files
target_lang: <lang>
suggested_tier: Junior | Senior
diff_keys_touched: [<...>] # for incremental code batches
- ...
report_path: .translate-report-<run_id>.json
Resolve formality per target language before building the brief. For each target language,
pick its register in this order (later wins): the --formality <formal|informal|auto> flag (global —
applies to every language this run) → config.formality (if it's a string, that value for every
language; if it's an object, config.formality[<lang>], else config.formality.default, else
auto) → auto. Pass the result as the formality map in the brief (one entry per target
language). auto — the default and the meaning when the field is absent — tells the panel to use the
language's conventional register for this product/context (today's behavior), so an absent/auto
setting changes nothing. Where a language has no T–V distinction (e.g. English), formal/informal
is interpreted as overall tone, never a forced construct.
(In tree/inplace mode you copy each source file to its renamed target path before spawning
the Lead, so files already exist as source-language copies for the workers to overwrite. In
catalog mode files and source_files are the existing per-language and source-language catalog
files.)
The Lead confirms/overrides each tier, spawns the workers, reviews against C1–C7, inline-fixes
small correction sets (returns large ones to Senior, cycle-cap 2), runs the final sweeps, writes
report_path, and returns. You do not drive batch-by-batch or spawn Senior/Junior yourself.
Step 4 — Watchdog
- Lead returns successfully → Step 5.
- Lead surfaces
NEEDS ATTENTION (a worker pool was unavailable across retries) → STOP, do not
commit, do not advance the marker, surface to the user.
- The Lead spawn itself dies (API overload) → respawn once with the same brief; dies again → STOP
and surface.
Step 5 — Read the Lead's report
i18n OK / i18n OK — N corrections applied → Step 6.
i18n NEEDS ATTENTION → STOP. Print the open questions; do not commit; leave the marker unadvanced.
Step 6 — Final verify / build (this skill owns it — runs once)
- If
verifyCmd is set, the workers already ran it per batch and the Lead confirmed it stayed
green — trust the report for that; don't re-run it per file.
- Run the project's
buildCmd once if set (e.g. npm run build) as one Bash call — must exit clean.
- WordPress post-processing (if
wordpress.makeMo / wordpress.makeJson): after the .po
files are translated, compile artifacts as single Bash calls — wp i18n make-mo languages (or
msgfmt) for .mo, wp i18n make-json languages --no-purge for the JS/Gutenberg JSON. If WP-CLI
isn't available, say so and deliver the .po files (the user can compile on their side).
- For standalone-document runs, "build" = whatever produces the deliverable (e.g. render the
translated Markdown to
.docx via the docx skill if the user asked for that format).
- STOP and surface, do not commit, if the report flags a broken source file, a suspicious
number/date, an inline hard-coded string that should be a key, or any open question that hit the
cycle cap. Those go back to the user; the marker stays put.
Step 6.5 — Completeness done-gate (runs every pass)
Before finishing, positively confirm coverage — don't assume it. Confirm it on three axes, all
deterministic (Glob/Read + compare in context, no shell loops): the target files exist, they
carry every source key (key-set parity), and their values aren't untranslated placeholders.
- File coverage (all formats). For each target language, verify every translatable file that has
a source counterpart now exists. For mirrored-tree/
.po formats, one Glob per format compared
against the language set catches missing target files.
- Key-set parity (catalog formats —
.json/.ts/.js/.arb/.yaml/.resx/.strings, and
.po/.pot). A catalog file existing and passing the Lead's leftover sweep does not prove
it is complete: a key that is entirely absent from the target carries no value, so the C1 /
leftover ("Cyrillic value") sweep — which only inspects values that are present — structurally
cannot see it. Close that gap here. For each target catalog:
Read the source catalog and the target catalog and reduce each to its set of dotted key
paths (for .po, the unit is the msgid, keeping msgctxt where present; keep array indices in
the path — steps.0.title — so array-length drift also surfaces).
- Compare the two sets in context. Any key in the source set but absent from the target set is a
missing-key gap — the translation is incomplete even though the file exists and has zero
leftovers. Also treat a key whose target value is empty/whitespace as a gap.
- This is source→target only, matching the gate's contract (it checks the run's languages
against the source). The inverse — a key present in a target but not the source — is an
orphan/source gap
/translate can't fill (it never edits the source); note it if you notice it,
but don't try to auto-fix it, and point to /translate-audit for the symmetric union check.
- Placeholders (catalogs). The Lead's leftover sweep already covers present-but-untranslated
values; trust the report for that.
Then act by scope:
--full → any missing file, missing/empty key, or placeholder is IN SCOPE: build the
affected (language, file, keys) into more batches and loop Step 3→6 until parity holds, or report
NEEDS ATTENTION with the exact missing-key list per language (and don't advance the marker). A
--full run must never finish with a silent gap — including a silently dropped key.
- Incremental/
--files → legitimately scoped; don't block on out-of-scope gaps, but surface
every still-incomplete language prominently in the Step 9 report (e.g. ⚠ Still partial (not in this run's scope): pt-BR — 12 keys missing in locales/pt-BR.json, 3 content files missing. Run /translate --full to clear.).
Step 6.7 — Project gates (conditional)
Project-owned quality gates that run on the final, complete translated output (after the
completeness gate, before commit/deliver). Skip this whole step if config.gates is empty/absent or
the run passed --no-gates. Gates are distinct from verifyCmd/buildCmd (which only prove the
project still builds) — a gate judges the content (e.g. plagiarism) and can drive a remediation
loop. Never commit/deliver a gate-failing result silently.
The file ledger. Every gate acts on the exact set of files this run wrote (the Lead's report file
list = the Step 7 staging set), grouped per target language. That same ledger is what a revert
restores. Ensure you have the pre-run state of those files available for revert: in a git run,
git restore handles tracked files and created files are deleted; in a non-git run, rely on the
Step 1 snapshot of the run's output paths.
For each gate in config.gates, then for each target language in the run:
- Resolve substitutions:
{files} = this run's output files for that language (space-joined),
{lang} = the language code, {out} = the gate's out (default .translate-gate/{lang}.json).
- Execute the check (exactly one is set):
run → one Bash call with the substituted command. The gate writes the verdict JSON to
{out} (or prints it to stdout).
skill → invoke it via the Skill tool, passing the substituted with args (plus files/lang/
out). The skill writes the verdict to {out} or returns it as its structured result. (Skill
gates need the agent — in a headless run, treat a skill gate as unrunnable and apply fallback.)
- Read the verdict (the findings contract):
{ "status": "pass" | "fail",
"disposition": "refine" | "retry" | "revert", // optional hint from the gate
"findings": [ { "file": "translations/de/article-1.md",
"passage": "…the flagged text…",
"issue": "92% similar to source X",
"suggestion": "rephrase to reduce overlap; keep meaning" } ] }
A missing/invalid verdict is itself a gate failure (surface it, don't silently pass).
- On
status: pass → record and continue to the next gate/language.
- On
status: fail → apply the gate's onFail (default ask), scoped to the failing
language(s):
ask (default) — present the findings and let the operator choose, in ONE consolidated
AskUserQuestion: show the gate name, which language(s) failed (and which passed), and each
finding (file · passage · issue · suggestion). Offer: Refine in place / Revert & re-run
fresh / Revert & stop / Accept anyway (override), plus a scope sub-choice just
<lang> vs the whole run when other languages passed. Execute the chosen disposition below.
refine — build a remediation batch from the findings: the flagged files, with each
suggestion/issue added as an extra constraint ("reduce overlap, preserve meaning, keep
placeholders/identity intact"). Re-dispatch that batch through the Lead (Step 3) — so it re-runs
C1–C7 and any verifyCmd on the refined output — then re-run this gate on the new output.
Loop up to maxRetries (default 1). If it still fails after the budget, fall back to ask (or
fallback when headless).
retry — revert the run's files (whole run) via the ledger, then do a fresh run
(Step 2→6.5) with the findings folded in as run-level guidance (append to the notes/queries
context so the panel avoids the flagged overlap this time). Loop up to maxRetries. On exhaustion,
fall back to ask (or fallback when headless).
revert — restore the run's files from the ledger and stop: do not build, commit, or
advance the marker. Report the findings and why it stopped.
- Headless / non-interactive (no operator to prompt):
ask cannot run, so apply the gate's
fallback — revert (default: undo + report) or accept (ship + flag in the report) — and say so.
Same fallback when an auto mode (refine/retry) exhausts maxRetries with no operator available.
- Accept / override (operator chose it, or
fallback: accept) → proceed to commit/deliver, but
the Step 9 report MUST flag the gate as overridden with its findings.
A refine/retry loop that changes files re-enters the completeness gate (6.5) for the affected scope
before this step is considered passed. Record every gate's final outcome (pass / refined-then-passed /
reverted / overridden, with retry count) for the Step 9 report.
Step 7 — Commit or deliver
The translated files are already written on disk (in tree mode under
<root>/translations/<lang>/…; in inplace/catalog mode at their target paths). Then:
tree mode (the point-at-a-path case): the output subtree is the deliverable. Tell the user
where it is (<root>/translations/<lang>/). If the root is a git repo and the user wants it
tracked, offer to stage the translations/ subtree by name and commit (never git add -A); it's
a fresh subtree so it won't entangle WIP. If the session reached this project over the device
bridge, the files were written on the user's disk directly (device paths) — confirm the location;
otherwise deliver key outputs via SendUserFile.
inplace / catalog mode in a git repo, files changed: show git -C <root> status and
git -C <root> diff (two parallel calls). Stage by name — never git add -A/. — only the
files the panel wrote. The commit must contain ONLY files this run produced — nothing else.
- The authority for that set is the Lead's report — its "Files written this run" list, not a
git status scan and not a directory or glob. Stage exactly those paths: one git -C <root> add -- <path> … with every path named. Never stage a directory, a glob, or "everything that
changed".
- Assume the working tree changed under you during the run. A parallel agent or process can
finish and leave an unrelated file staged, modified, or newly ready between Step 1's dirty
check and this commit — Step 1's snapshot does not protect the commit. That is precisely
how an unrelated, half-ready file gets swept into a translation commit and can break the app.
- Verify before committing (hard gate). After staging, run
git -C <root> diff --cached --name-only and confirm the staged set is exactly equal to the panel's file list — no
extra paths, no missing ones. If anything extra appears (a concurrent agent's file, stray WIP),
do not commit: unstage the extras by name (git -C <root> restore --staged -- <path>),
re-verify, and only then commit. If you cannot reconcile the staged set to the panel list, STOP
and surface it to the user rather than committing a superset.
- Skip pre-existing WIP the user excluded at Step 1. Commit with a HEREDOC, repo-conventional
message (scope
i18n or the dominant language code). If config.creditInCommit is true,
append an attribution trailer as the last line of the commit message: Translated-with: Translation Agency <version> (https://github.com/roshaw/claude-translation-agency) (read
<version> from the toolkit's VERSION file). If creditInCommit is falsy (the default), add
no trailer. Never write attribution into the translated files themselves. Pre-commit hook
fails → fix the cause, re-stage, NEW commit — never --amend/--no-verify. Do NOT push.
- Non-git set: the outputs are on disk; deliver each via SendUserFile so the user can download them.
Step 8 — Advance the marker (git incremental runs)
After a successful commit (or a no-op pass), write the current HEAD to .translate-last-review at
the project root so the next run starts clean:
git -C <repo> rev-parse HEAD
(then printf 'commit=%s\nreviewed=%s\n' <sha> <iso-date> > <repo>/.translate-last-review as one
call). Do NOT advance if the run blocked (Step 6 STOP) or a --full run left a language incomplete
(Step 6.5 NEEDS ATTENTION). Add .translate-last-review and .translate-report-*.json to the
project's ignore file if not already ignored.
Step 8.5 — Update project memory
Maintain the toolkit's registry + per-project memory so the next run benefits:
- In
projects/registry.json (git-ignored/local — create it as { "version": 1, "projects": [] }
if missing), set the project's lastRunAt to today (date Bash call) and adjust status if it
changed. If the project had no entry, offer to add one now (same shape as /translate-init
Step 3.5) and create projects/<slug>/notes.md from the template.
- Append one line to the top of the
notes.md Run log: <date> — <scope> → <verdict> (<languages>, <n> fixes). If the run made a durable decision (a terminology call, a new
do-not-translate item, a format quirk discovered), add it to the relevant notes section — keep it
to decisions, not a transcript.
Skip only if the user explicitly asked not to use project memory.
Step 9 — Report
Print a tight summary from the Lead's report + gates + commit:
- Verdict, Specialization used, Source → Targets, Scope (base→HEAD / full / files, N files).
- Panel routing: K Junior / M Senior batches; any tier overrides the Lead applied.
- Per-batch outcomes (condensed): ACCEPTED / ACCEPTED WITH N INLINE FIXES / RETURNED+re-reviewed / OPEN QUESTION.
- Sweeps S1/S2/S3 totals (or n-a for plain text).
- Flags by category C1–C7 + S1–S3.
- Completeness gate (MANDATORY line, even on a clean run — covers file coverage and catalog
key-set parity; state "all target languages complete (key-set parity + file coverage)" positively,
or
⚠ Still partial: <lang> — <what's missing>).
- Terminology research: ran (N terms — X high / Y medium / Z low confidence) / reused saved
glossary / skipped (state which). Glossary:
projects/<slug>/glossary.csv.
- Queries (async, non-blocking): N items logged to
projects/<slug>/queries-<date>.md for your
review whenever you want — or "none". Never block the run on these.
- Open questions to the user (cycle-capped residuals, broken source, suspicious data).
- Gates: verify / build result. Project gates (if any ran): per gate + language, the outcome
—
pass / refined→pass (N retries) / reverted / ⚠ overridden (findings shipped) / skipped (--no-gates). A reverted or unresolved gate means the run did not commit — say so.
- Commit
<hash> <subject> (not pushed) or "delivered N files" / "no changes".
- Marker advanced/left.
Always end the report with these two footer lines, verbatim:
- Attribution (always present, every run — not configurable):
Translated with Translation Agency <version> — https://github.com/roshaw/claude-translation-agency
(read <version> from the toolkit's VERSION file). This credits the tool in the operator-facing
report only; it is independent of config.creditInCommit, which controls whether the same credit
also appears as a commit trailer (Step 7).
- Disclaimer (point-of-use reminder, not optional):
⚠ AI-generated translation — review before publishing; human sign-off recommended for legal/medical/financial/safety-critical content. If the
run's specialization is a high-stakes / safety-critical domain — legal, finance, or medical,
or any custom module of comparable stakes (health, financial, legal, safety) — make it a full
sentence and bold it, since the stakes are higher.
Reference
- Panel:
.claude/agents/translate-lead.md (Lead), translate-senior.md (Senior),
translate-junior.md (Junior). Terminology research: translate-researcher.md.
- Per-project glossary + queries:
projects/<slug>/glossary.csv, projects/<slug>/queries-<date>.md.
- Specializations:
specializations/<name>.md (default general.md); how they work:
specializations/README.md.
- Config:
translation.config.json (source/target langs, specialization, formats, verify/build).
- Siblings:
/translate-init (generate a project's config + register it), /translate-add-locale (scaffold a
brand-new UI language into a codebase, then hand off here), /translate-audit (read-only coverage
audit — which languages are missing what, and how to fix it).
- Projects registry + per-project memory:
projects/registry.json, projects/<slug>/notes.md
(contract in projects/README.md).
- Marker (untracked, per-machine):
.translate-last-review.
1---2name: translate3description: Translate any project or set of files into one or more target languages using the three-tier translator panel (Lead → Senior/Junior with adversarial QA). Works on codebases (i18n message catalogs, WordPress .po/.pot/.json, MDX/HTML content trees) and standalone documents (Markdown, JSON, docx, txt, subtitles, CSV). Detects the source language and file formats, computes the translation scope (whole project, changed-since-last-run, or an explicit file/glob set), classifies each batch as low-risk (Junior) or domain-prose (Senior), and applies the domain SPECIALIZATION setting (default `general`; e.g. technical, marketing, legal). Runs one final verify/build at the end and writes a report. Use when the user says "translate this", "translate into <language>", "localize the project", "run the translation pass", "translate the WordPress strings", "translate these files", or "i18n sweep".4---56# translate78The single entry point for translating **anything** — a whole codebase's i18n, a WordPress9theme/plugin's gettext catalog, a folder of Markdown docs, a batch of JSON files, a subtitle10file — into **any** set of target languages. It drives the three-tier translator panel and11applies a domain **specialization** so terminology is right for the material.1213## The panel (why three tiers)1415- **Lead** (`translate-lead`, Opus) — orchestrates the run, dispatches each batch to a worker,16 and **adversarially reviews** every result against a fixed C1–C7 checklist before signing off.17 Runs the final blind-spot sweeps. Loads the specialization module so its terminology check is18 domain-aware.19- **Senior** (`translate-senior`, Sonnet) — translates domain-prose and any substantive surface.20 Spawned by the Lead, not directly by this skill.21- **Junior** (`translate-junior`, Haiku) — translates only low-risk UI chrome. Spawned by the Lead.2223This skill computes the scope + per-batch tier classification, then spawns the **Lead once**; the24Lead handles all worker spawns and reviews. The single final build/verify runs here (Step 6).2526## Invocation2728```29/translate # translate the changed-since-last-run set into the configured targets30/translate --path <dir> # point at a project/folder on disk, e.g. --path C:\Projects\MyApp31/translate <path> # shorthand for --path <path> (a folder) or --files <path> (a file/glob)32/translate --to <langs> # e.g. --to de,fr,pt-BR (overrides configured targets for this run)33/translate --from <lang> # override the detected/configured source language34/translate --domain <name> # specialization: general | technical | marketing | legal | finance | medical | ecommerce | travel | government | scientific | <custom> (default from config, else general). Layer with a comma-list: --domain technical,finance (first = primary)35/translate --formality <f> # register for the whole run: formal | informal | auto (overrides config.formality; default auto)36/translate --files <glob|paths> # translate an explicit set (a folder, a glob, named files)37/translate --out <mode> # output layout: inplace (default) | tree | catalog (see Step 0.5)38/translate --full # translate the ENTIRE translatable surface, not just the diff39/translate --no-gates # skip the project gates (config.gates) for this run (see Step 6.7)40```4142Arguments compose: `/translate --path C:\Projects\MyApp --to de,fr --domain technical`.4344### Settings & the specialization setting4546Configuration is resolved in this order (later wins):471. `translation.config.json` at the project root (defaults — see below).482. Any target-project convention file it points to.493. Flags on this invocation.5051`translation.config.json` (all fields optional):52```json53{54 "sourceLang": "en",55 "targetLangs": ["de", "fr", "es"],56 "specialization": "general",57 "formality": "auto",58 "glossary": "glossary.csv",59 "doNotTranslate": ["Colour/hex codes and size tokens like 42x2 are pass-through data — leave verbatim.", "Keep placeholders {name}, {id}, {remote} intact."],60 "include": ["src/i18n/**", "content/**", "languages/**"],61 "exclude": ["**/node_modules/**", "**/*.min.*"],62 "verifyCmd": "npx tsc --noEmit",63 "buildCmd": "",64 "creditInCommit": false,65 "wordpress": { "textdomain": "", "makeJson": false, "makeMo": false }66}67```6869**Specialization is a per-run setting, not a hardcoded domain.** If the user names one70(`--domain technical`), use it. Otherwise use `translation.config.json → specialization`.71Otherwise default to **`general`**. The chosen module lives at `specializations/<name>.md` and is72passed to the Lead + Senior so their terminology (C2) and framing (C6) checks match the material.73If `--domain <name>` names a module that doesn't exist, list the available modules and ask which to74use (or offer to run `general`).7576**Layering (2+ domains).** The specialization may be a **list** — a comma-list on the flag77(`--domain technical,finance`) or a JSON array in config (`"specialization": ["technical", "finance"]`).78Resolve it to an ordered list of module names (flag wins over config, as usual; a single name stays a79one-element list). Then:80- Validate every named module exists (`specializations/<name>.md`); if any is missing, list the81 available modules and ask — don't silently drop it.82- The **first** module is **primary**; the rest are secondary layers.83- Pass the whole ordered list to the Lead (`specialization` = the list, `specialization_path` = the84 ordered list of module paths — see Step 3). The Lead concatenates the modules into one layered brief,85 prefixed with a precedence preamble: *"You are operating under N layered specializations, primary86 first: [names]. The primary owns register/framing (C6) on any conflict. Every layer's terminology87 (C2) and verbatim/do-not-translate (C3) rules apply — union them. If two layers give directly88 conflicting framing that the primary order doesn't settle, translate to the safer reading and log a89 query (high-stakes)."*90- Keep it to **compatible** domains. If the user layers opposites (e.g. `marketing` + `legal`), warn91 once that their framing rules conflict and suggest separate scoped runs, then proceed primary-first if92 they confirm.9394## What this skill does NOT do9596- **Push, or touch a protected branch.** It stages/edits files and (optionally) commits to the97 working branch; pushing and any deploy stay human.98- **Change source facts or add features.** The panel translates/repairs copy only — it never adds99 keys/components/logic, and never edits numbers, dates, names, or citations. A string missing100 because a *key* is missing is an implementation bug — flag it, don't paper over it.101- **Add a brand-new UI locale to a codebase from scratch.** That's the sibling `/translate-add-locale` skill102 (scaffold the language wiring), which then hands off here for the real translation.103104---105106## Bash discipline (HARD RULES — every step)1071081. **Each command is its own Bash call.** Never chain with `&&`/`;`/`||`, never pipe with `|`,109 never `cd <dir> && …`, never cosmetic `echo` separators. Independent calls run in parallel in110 one message.1112. **No shell for output processing or control flow.** No `python -c`/`awk`/`jq`/`sed` pipelines,112 no `> /tmp/file && parse-back`, no heredocs for logic, no shell loops/branches. Iterate and113 branch in context — `Glob`/`Grep`/`Read` once, walk the result in your head. To check "does114 `<lang>/<file>` exist for every language?" → ONE `Glob` call; compare against the language set.1153. **File searches/counts use `Grep`/`Glob`**, never `grep | wc`, `ls | grep`, `find | head`,116 `git … | grep -c`.117118The one legitimate `&&` is a HEREDOC commit at Step 7 (`git commit -m "$(cat <<'EOF' … EOF)"`) — a119single command with quoted content, not a chain.120121---122123## Step 0 — Preflight1241251. **Locate the project root.** Resolve in this order:126 a. `--path <dir>` / a bare `<path>` argument (an absolute computer path like `C:\Projects\MyApp`127 is fine — accept it as given).128 b. Else, if the current folder has a `translation.config.json`, use the current folder.129 c. Else **offer a project picker** from the registry: read the toolkit's `projects/registry.json`130 (git-ignored/local — treat a **missing** file the same as an empty one).131 If it lists projects, AskUserQuestion "Which project should I translate?" with each registered132 project as an option (label = name, description = path + langs + last run), plus an "Other133 (enter a path)" path. If exactly one project is registered, offer it as the default. If the134 registry is empty, ask for a path, or suggest running `/translate-init` first to set one up.135 All scope globs (`include`/`exclude`, `--files`) are relative to the chosen root. Read that root's136 `translation.config.json`; if none exists, detect below and offer to write a starter config (or to137 run `/translate-init`) at the end.1381b. **Load project memory.** Read the toolkit's `projects/registry.json` and find the entry whose139 `path` matches this project root. If found, read its `projects/<slug>/notes.md` — the terminology140 decisions, do-not-translate list, format quirks, and "what done means here" are run context; pass141 the relevant parts to the Lead in its brief (as `project_conventions` alongside any in-project142 contract). **Collect the do-not-translate rules** from `config.doNotTranslate` plus any143 do-not-translate items in `notes.md`, and pass the merged list to the Lead as `do_not_translate`144 (the brief field in Step 3) — these are the manual pass-through/verbatim instructions the panel145 enforces. If there's **no** registry entry, note it — you'll offer to register the project at the146 end (Step 9), and continue this run using config + flags. (Setup via `/translate-init` is the147 normal way to register, but a `/translate` run on an unregistered project still works.)1482. **If the root is a git repo AND output mode is `inplace` (the default) or `catalog`:** these149 write into the working tree, so note the branch and any uncommitted changes (`git -C <root> status150 --porcelain`, one call). If dirty, list the files and ask whether to proceed, stash, or abort — so151 staging by name at Step 7 doesn't entangle unrelated WIP. A clean tree needs no prompt.1523. **Output mode `tree`** writes into a fresh `translations/<lang>/` subtree and never edits153 originals, so it needs no dirty-tree prompt even in a git repo. Plain non-git folders skip git154 entirely.155156## Step 0.5 — Resolve the output layout157158Resolve `--out`, else `config.output.mode`, else default **`inplace`**:159160- **`inplace`** (default): write each translation as a sibling next to its source. If the source161 filename encodes the source language, **swap that code** for the target (`en.json` → `de.json`,162 `messages.en.ts` → `messages.de.ts`, in the same folder); otherwise append the code before the163 extension (`guide.md` → `guide.de.md`). Originals are never overwritten (the target is a different164 filename). Good for document and catalog sets where each language file lives beside its source.165- **`tree`** (the right choice when you want translated copies isolated from the originals): for each166 target language, **copy** every in-scope source file to167 `<root>/<config.output.dir>/<lang>/<relative-path>` (default dir `translations`, so168 `<root>/translations/de/…`), then translate the **copy in place**. Originals are never touched.169 Always add `<dir>/**` to `exclude` so a re-run doesn't translate its own output. Do the copy with170 `Read`+`Write` (or a single `cp` Bash call per file — no chaining); create parent dirs as needed.171 The Lead/Senior then edit the copied files.172 - **Rewrite the source-language code in the path to the target language** as you copy — otherwise173 you'd leave `en.json` sitting inside a `de/` folder. Rewrite an exact language-code **filename174 stem/suffix** and an exact **path segment**, only where it stands alone as the language code175 (never inside another word like `content` or `engine`):176 - `en.json` → `de.json`; `messages.en.ts` → `messages.de.ts`; `guide.en.md` → `guide.de.md`177 - a `…/en/…` path segment → `…/de/…` (e.g. `content/en/home.md` → `content/de/home.md`)178 - `strings-en.xml` / `app-en.strings` → `strings-de.xml` / `app-de.strings`179 - WordPress: `<textdomain>-en_US.po` → `<textdomain>-de_DE.po` (use the target's WP locale form)180 A file whose name carries **no** language code (e.g. `guide.md`, `README.md`) keeps its name —181 the parent `<lang>/` folder already marks the language. Record, per copied file, both the target182 path (renamed) and the source path it came from, so the workers read the source and write the183 renamed copy.184- **`catalog`**: for an existing i18n message-catalog project, edit the per-language files that185 already exist (`messages.<lang>.ts`, `locales/<lang>.json`, `languages/<textdomain>-<locale>.po`)186 in place — fill missing keys, fix leftovers, correct terminology; don't create copies. This is the187 mode for a codebase whose language files are already wired into the build.188189When `--out` isn't given, use `config.output.mode`, else the default **`inplace`** — with one190smart exception: if the project **already has a per-language catalog/tree** (e.g. `de.json` already191sits next to `en.json`, or `messages.de.ts` exists), prefer **`catalog`** so you edit the real files192the build uses instead of writing `de.de.json` siblings. State which mode you chose in Step 9.193194## Step 1 — Detect source language, formats, and the translatable surface195196Use `Glob`/`Grep`/`Read` (never shell loops). Determine:197198- **Source language.** From config `sourceLang`, else `--from`, else infer from the file layout199 (`messages.en.ts`, `en/`, `-en.po`, `.en.md`) or a quick content sample. Confirm via200 AskUserQuestion only if genuinely ambiguous.201- **Target languages.** From `--to`, else config `targetLangs`, else (for an existing i18n tree)202 every non-source language already present, else ask.203- **Formats present**, and how each fans out:204205 | Format | Detect | Fan-out shape |206 |---|---|---|207 | JS/TS message catalog | `messages.<lang>.ts`, `locales/<lang>.json` | one file per language, mirrored keys |208 | JSON / `.arb` / i18next | `<lang>.json`, `translation.json` | one file per language |209 | **WordPress gettext** | `languages/*.pot`, `*-<locale>.po` | `.pot` template → one `<textdomain>-<locale>.po` per language |210 | **WordPress JSON** | `*-<locale>-<md5>.json` | regenerated from the `.po` (see Step 6) |211 | `.po` / `.pot` (generic gettext) | `*.po`, `*.pot` | one `.po` per language from the `.pot` |212 | Markdown / MDX tree | `content/en/**`, `*.en.md` | one file per language, mirrored path |213 | HTML / XML / XLIFF | `*.html`, `*.xlf` | `<target>` filled per language |214 | Subtitles | `*.srt`, `*.vtt` | one file per language |215 | Spreadsheet / CSV | `*.csv`, `*.xlsx` | designated text columns per language |216 | Standalone docs | `*.md`, `*.docx`, `*.txt` | one output file per language |217218 Record the detected formats; the Lead/Senior apply the matching file-format rules.219220- **Scope:**221 - `--full` → the entire translatable surface: **every target language × every translatable222 file.** Never silently narrow it to one language/file. This is the expensive, from-scratch or223 drift-catching case.224 - `--files`/`<path>` → exactly that set (× the target languages).225 - **Default (incremental)** → what changed since the last run. In a git repo, that's226 `<marker>..HEAD` committed *and* uncommitted, where the marker is the untracked file227 `.translate-last-review` at the project root (`git -C <repo> diff --name-only <base>` +228 `git -C <repo> ls-files --others --exclude-standard`, two parallel calls). No marker (first229 run) or stale SHA → default the base to `HEAD` (review only uncommitted work); if the tree is230 also clean, tell the user and offer `--full` or a base ref. For non-git sets, "incremental"231 isn't available — translate `--files`/whole folder.232 - **Fan-out completeness (don't rely on the diff alone).** For mirrored-tree formats (Markdown/233 MDX/HTML/`.po`), a plain diff only surfaces a *changed source*. Adding a new target language234 changes no existing source file, so also include every `(language, file)` where the source235 exists but the target is **missing or carries a stale `source_hash`**. Derive this with one236 `Glob` per format and compare the returned target list against the language set in context.237 This is what makes a run after `/translate-add-locale` fill the new language's whole existing page set.238239- **If the translatable set is empty** (nothing changed, or only non-translatable files): say so in240 one line, skip the panel and the commit, and still advance the marker (Step 8) so this range241 isn't re-scanned.242243## Step 2 — Order into batches and classify each tier244245Split the translatable set into ordered batches of **at most 3 files each**, one target language246per batch. **In `tree` and `inplace` modes the batch's files are the renamed target paths the skill pre-created**247(the copy under `translations/<lang>/…` for `tree`, or the sibling next to the source for `inplace`),248with the matching source path recorded so the worker reads the source and overwrites the copy. In249`catalog` mode the files are the existing per-language files. Order deterministically (by format,250then path) so a restart is reproducible. A251fan-out source (an MDX/HTML/`.po` template that produces one output per language) counts as **one252batch item per (source, language)** — never split one source's languages across batches253arbitrarily; keep a source's set together where practical.254255Classify each batch's `suggested_tier`:256- **Domain-prose surfaces** (documents, marketing/legal/medical copy, MDX/HTML bodies, message257 values with substantive strings) → **Senior**.258- **Message-catalog batches** → inspect the diff. If every touched entry is pure chrome (nav,259 footer, buttons, generic errors, format fields) with no domain term / citation / identity token →260 **Junior**. Otherwise → **Senior**. A brand-new/entirely-untranslated file → **Senior** regardless.261- Fail-safe: anything ambiguous → **Senior**.262263## Step 2.5 — Terminology research (conditional)264265Decide whether to run the `translate-researcher` before translating. Run it when **any** holds:266- `--research` was passed (force a refresh), OR267- `config.research` is `always`, OR268- `config.research` is `first-run` (default) AND this project/target-language pair has **no glossary269 yet** (no `projects/<slug>/glossary.csv`, or it lacks rows for a target language in scope).270271Skip it when `config.research` is `off`, or when `first-run` and a glossary already covers every272target language (reuse the saved glossary — research is a once-per-language cost, not per-run).273274When running it, spawn ONE `translate-researcher` (Agent tool) with a brief: `project_root`, `slug`,275the `context` (from `config.context` — inline text or the contents of the file it points to),276`source_lang`, `target_langs`, `specialization_path`, `glossary_path` (`projects/<slug>/glossary.csv`),277`queries_path` (`projects/<slug>/queries-<date>.md`), a **content_sample** (you pick the high-signal278files — headings, catalogs, nav/labels — not the whole surface), and `formats`. It writes/merges the279glossary and logs low-confidence terms to the queries file, then returns a summary. Pass the resulting280`glossary_path` to the Lead in Step 3. If the researcher can't resolve a language at all, note it and281continue — the panel still runs against the specialization.282283## Step 3 — Spawn the Lead once284285```286Agent({ subagent_type: "translate-lead", prompt: <brief> })287```288289Brief:290```291run_id: <e.g. tr-2026-07-31-1>292mode: incremental | full | files293project_root: <absolute path, e.g. C:\Projects\MyApp>294output_mode: tree | inplace | catalog295output_dir: translations # for tree mode296source_lang: <lang>297target_langs: [<...>]298specialization: <name> # or an ORDERED list [primary, ...] when layering299specialization_path: specializations/<name>.md # or the ordered list of module paths when layering;300 # the Lead loads all, primary first (see "Layering" in Step 1)301context: <config.context — inline text or the contents of translation-context.md; the product's302 purpose/audience/register, so the panel picks the right sense of each word>303formality: { <lang>: formal | informal | auto, ... } # resolved PER target language (see below)304do_not_translate: [<config.doNotTranslate rules, verbatim — the manual pass-through/verbatim305 instructions the panel must treat as absolute and add to the C1/C3 exempt list>]306glossary_path: projects/<slug>/glossary.csv # the research pass's output (or config.glossary)307queries_mode: report | high-stakes | off # from config.queries (default report)308queries_path: projects/<slug>/queries-<date>.md309project_conventions: <target project's CLAUDE.md / i18n contract path, or "">310verify_cmd: <config.verifyCmd or "">311batch_list:312 - id: B1313 files: [<target path to edit — the copy in tree mode>]314 source_files: [<the source path to read from>] # tree/inplace: differs from files315 target_lang: <lang>316 suggested_tier: Junior | Senior317 diff_keys_touched: [<...>] # for incremental code batches318 - ...319report_path: .translate-report-<run_id>.json320```321322**Resolve `formality` per target language before building the brief.** For each target language,323pick its register in this order (later wins): the `--formality <formal|informal|auto>` flag (global —324applies to every language this run) → `config.formality` (if it's a string, that value for every325language; if it's an object, `config.formality[<lang>]`, else `config.formality.default`, else326`auto`) → `auto`. Pass the result as the `formality` map in the brief (one entry per target327language). `auto` — the default and the meaning when the field is absent — tells the panel to use the328language's conventional register for this product/context (today's behavior), so an absent/`auto`329setting changes nothing. Where a language has no T–V distinction (e.g. English), `formal`/`informal`330is interpreted as overall tone, never a forced construct.331332(In `tree`/`inplace` mode you copy each source file to its **renamed** target path **before** spawning333the Lead, so `files` already exist as source-language copies for the workers to overwrite. In334`catalog` mode `files` and `source_files` are the existing per-language and source-language catalog335files.)336337The Lead confirms/overrides each tier, spawns the workers, reviews against C1–C7, inline-fixes338small correction sets (returns large ones to Senior, cycle-cap 2), runs the final sweeps, writes339`report_path`, and returns. You do **not** drive batch-by-batch or spawn Senior/Junior yourself.340341## Step 4 — Watchdog342343- Lead returns successfully → Step 5.344- Lead surfaces `NEEDS ATTENTION` (a worker pool was unavailable across retries) → STOP, do not345 commit, do not advance the marker, surface to the user.346- The Lead spawn itself dies (API overload) → respawn once with the same brief; dies again → STOP347 and surface.348349## Step 5 — Read the Lead's report350351- `i18n OK` / `i18n OK — N corrections applied` → Step 6.352- `i18n NEEDS ATTENTION` → STOP. Print the open questions; do not commit; leave the marker unadvanced.353354## Step 6 — Final verify / build (this skill owns it — runs once)355356- If `verifyCmd` is set, the workers already ran it per batch and the Lead confirmed it stayed357 green — **trust the report** for that; don't re-run it per file.358- Run the project's `buildCmd` once if set (e.g. `npm run build`) as one Bash call — must exit clean.359- **WordPress post-processing** (if `wordpress.makeMo` / `wordpress.makeJson`): after the `.po`360 files are translated, compile artifacts as single Bash calls — `wp i18n make-mo languages` (or361 `msgfmt`) for `.mo`, `wp i18n make-json languages --no-purge` for the JS/Gutenberg JSON. If WP-CLI362 isn't available, say so and deliver the `.po` files (the user can compile on their side).363- For standalone-document runs, "build" = whatever produces the deliverable (e.g. render the364 translated Markdown to `.docx` via the docx skill if the user asked for that format).365- **STOP and surface, do not commit, if** the report flags a broken source file, a suspicious366 number/date, an inline hard-coded string that should be a key, or any open question that hit the367 cycle cap. Those go back to the user; the marker stays put.368369## Step 6.5 — Completeness done-gate (runs every pass)370371Before finishing, positively confirm coverage — don't assume it. Confirm it on **three** axes, all372deterministic (`Glob`/`Read` + compare **in context**, no shell loops): the target files exist, they373carry **every source key** (key-set parity), and their values aren't untranslated placeholders.3743751. **File coverage (all formats).** For each target language, verify every translatable file that has376 a source counterpart now exists. For mirrored-tree/`.po` formats, one `Glob` per format compared377 against the language set catches missing target files.3782. **Key-set parity (catalog formats — `.json`/`.ts`/`.js`/`.arb`/`.yaml`/`.resx`/`.strings`, and379 `.po`/`.pot`).** A catalog file existing and passing the Lead's leftover sweep does **not** prove380 it is complete: a key that is *entirely absent* from the target carries no value, so the C1 /381 leftover ("Cyrillic value") sweep — which only inspects values that are present — structurally382 cannot see it. Close that gap here. For each target catalog:383 - `Read` the **source** catalog and the **target** catalog and reduce each to its set of dotted key384 paths (for `.po`, the unit is the `msgid`, keeping `msgctxt` where present; keep array indices in385 the path — `steps.0.title` — so array-length drift also surfaces).386 - Compare the two sets in context. **Any key in the source set but absent from the target set is a387 missing-key gap** — the translation is incomplete even though the file exists and has zero388 leftovers. Also treat a key whose target value is empty/whitespace as a gap.389 - This is **source→target only**, matching the gate's contract (it checks the run's languages390 against the source). The inverse — a key present in a *target* but not the source — is an391 orphan/source gap `/translate` can't fill (it never edits the source); note it if you notice it,392 but don't try to auto-fix it, and point to `/translate-audit` for the symmetric union check.3933. **Placeholders (catalogs).** The Lead's leftover sweep already covers present-but-untranslated394 values; trust the report for that.395396Then act by scope:397- **`--full`** → any missing file, **missing/empty key**, or placeholder is IN SCOPE: build the398 affected `(language, file, keys)` into more batches and loop Step 3→6 until parity holds, or report399 `NEEDS ATTENTION` with the exact missing-key list per language (and don't advance the marker). A400 `--full` run must never finish with a silent gap — **including a silently dropped key**.401- **Incremental/`--files`** → legitimately scoped; don't block on out-of-scope gaps, but **surface402 every still-incomplete language prominently** in the Step 9 report (e.g. `⚠ Still partial (not in403 this run's scope): pt-BR — 12 keys missing in locales/pt-BR.json, 3 content files missing. Run404 /translate --full to clear.`).405406## Step 6.7 — Project gates (conditional)407408Project-owned quality gates that run on the **final, complete** translated output (after the409completeness gate, before commit/deliver). Skip this whole step if `config.gates` is empty/absent or410the run passed `--no-gates`. Gates are **distinct from** `verifyCmd`/`buildCmd` (which only prove the411project still builds) — a gate judges the *content* (e.g. plagiarism) and can drive a remediation412loop. Never commit/deliver a gate-failing result silently.413414**The file ledger.** Every gate acts on the exact set of files this run wrote (the Lead's report file415list = the Step 7 staging set), grouped per target language. That same ledger is what a revert416restores. Ensure you have the pre-run state of those files available for revert: in a git run,417`git restore` handles tracked files and created files are deleted; in a non-git run, rely on the418Step 1 snapshot of the run's output paths.419420For each gate in `config.gates`, then for each target language in the run:4214221. **Resolve substitutions**: `{files}` = this run's output files for that language (space-joined),423 `{lang}` = the language code, `{out}` = the gate's `out` (default `.translate-gate/{lang}.json`).4242. **Execute the check** (exactly one is set):425 - **`run`** → one Bash call with the substituted command. The gate writes the verdict JSON to426 `{out}` (or prints it to stdout).427 - **`skill`** → invoke it via the Skill tool, passing the substituted `with` args (plus files/lang/428 out). The skill writes the verdict to `{out}` or returns it as its structured result. (Skill429 gates need the agent — in a headless run, treat a `skill` gate as unrunnable and apply `fallback`.)4303. **Read the verdict** (the findings contract):431 ```json432 { "status": "pass" | "fail",433 "disposition": "refine" | "retry" | "revert", // optional hint from the gate434 "findings": [ { "file": "translations/de/article-1.md",435 "passage": "…the flagged text…",436 "issue": "92% similar to source X",437 "suggestion": "rephrase to reduce overlap; keep meaning" } ] }438 ```439 A missing/invalid verdict is itself a gate failure (surface it, don't silently pass).4404. **On `status: pass`** → record and continue to the next gate/language.4415. **On `status: fail`** → apply the gate's `onFail` (default `ask`), scoped to the failing442 language(s):443 - **`ask`** (default) — present the findings and let the operator choose, in ONE consolidated444 `AskUserQuestion`: show the gate name, which language(s) failed (and which passed), and each445 finding (file · passage · issue · suggestion). Offer: **Refine in place** / **Revert & re-run446 fresh** / **Revert & stop** / **Accept anyway (override)**, plus a scope sub-choice **just447 `<lang>`** vs **the whole run** when other languages passed. Execute the chosen disposition below.448 - **`refine`** — build a remediation batch from the findings: the flagged files, with each449 `suggestion`/`issue` added as an extra constraint ("reduce overlap, preserve meaning, keep450 placeholders/identity intact"). Re-dispatch that batch through the Lead (Step 3) — so it re-runs451 **C1–C7** and any `verifyCmd` on the refined output — then re-run **this gate** on the new output.452 Loop up to `maxRetries` (default 1). If it still fails after the budget, fall back to `ask` (or453 `fallback` when headless).454 - **`retry`** — revert the run's files (whole run) via the ledger, then do a **fresh** run455 (Step 2→6.5) with the findings folded in as run-level guidance (append to the notes/queries456 context so the panel avoids the flagged overlap this time). Loop up to `maxRetries`. On exhaustion,457 fall back to `ask` (or `fallback` when headless).458 - **`revert`** — restore the run's files from the ledger and **stop**: do not build, commit, or459 advance the marker. Report the findings and why it stopped.4606. **Headless / non-interactive** (no operator to prompt): `ask` cannot run, so apply the gate's461 `fallback` — `revert` (default: undo + report) or `accept` (ship + flag in the report) — and say so.462 Same fallback when an auto mode (`refine`/`retry`) exhausts `maxRetries` with no operator available.4637. **Accept / override** (operator chose it, or `fallback: accept`) → proceed to commit/deliver, but464 the Step 9 report MUST flag the gate as **overridden** with its findings.465466A refine/retry loop that changes files re-enters the completeness gate (6.5) for the affected scope467before this step is considered passed. Record every gate's final outcome (pass / refined-then-passed /468reverted / overridden, with retry count) for the Step 9 report.469470## Step 7 — Commit or deliver471472The translated files are already written on disk (in `tree` mode under473`<root>/translations/<lang>/…`; in `inplace`/`catalog` mode at their target paths). Then:474475- **`tree` mode** (the point-at-a-path case): the output subtree is the deliverable. Tell the user476 where it is (`<root>/translations/<lang>/`). If the root is a git repo and the user wants it477 tracked, offer to stage the `translations/` subtree by name and commit (never `git add -A`); it's478 a fresh subtree so it won't entangle WIP. If the session reached this project over the device479 bridge, the files were written on the user's disk directly (device paths) — confirm the location;480 otherwise deliver key outputs via SendUserFile.481- **`inplace` / `catalog` mode in a git repo, files changed:** show `git -C <root> status` and482 `git -C <root> diff` (two parallel calls). **Stage by name — never `git add -A`/`.`** — only the483 files the panel wrote. **The commit must contain ONLY files this run produced — nothing else.**484 - **The authority for that set is the Lead's report — its "Files written this run" list**, not a485 `git status` scan and not a directory or glob. Stage exactly those paths: one `git -C <root>486 add -- <path> …` with every path named. Never stage a directory, a glob, or "everything that487 changed".488 - **Assume the working tree changed under you during the run.** A parallel agent or process can489 finish and leave an unrelated file staged, modified, or newly ready between Step 1's dirty490 check and this commit — Step 1's snapshot does **not** protect the commit. That is precisely491 how an unrelated, half-ready file gets swept into a translation commit and can break the app.492 - **Verify before committing (hard gate).** After staging, run `git -C <root> diff --cached493 --name-only` and confirm the staged set is **exactly equal** to the panel's file list — no494 extra paths, no missing ones. If anything extra appears (a concurrent agent's file, stray WIP),495 **do not commit**: unstage the extras by name (`git -C <root> restore --staged -- <path>`),496 re-verify, and only then commit. If you cannot reconcile the staged set to the panel list, STOP497 and surface it to the user rather than committing a superset.498 - Skip pre-existing WIP the user excluded at Step 1. Commit with a HEREDOC, repo-conventional499 message (scope `i18n` or the dominant language code). **If `config.creditInCommit` is `true`**,500 append an attribution trailer as the last line of the commit message: `Translated-with:501 Translation Agency <version> (https://github.com/roshaw/claude-translation-agency)` (read502 `<version>` from the toolkit's `VERSION` file). If `creditInCommit` is falsy (the default), add503 **no** trailer. Never write attribution into the translated files themselves. Pre-commit hook504 fails → fix the cause, re-stage, NEW commit — never `--amend`/`--no-verify`. Do NOT push.505- **Non-git set:** the outputs are on disk; deliver each via SendUserFile so the user can download them.506507## Step 8 — Advance the marker (git incremental runs)508509After a successful commit (or a no-op pass), write the current HEAD to `.translate-last-review` at510the project root so the next run starts clean:511```bash512git -C <repo> rev-parse HEAD513```514(then `printf 'commit=%s\nreviewed=%s\n' <sha> <iso-date> > <repo>/.translate-last-review` as one515call). Do NOT advance if the run blocked (Step 6 STOP) or a `--full` run left a language incomplete516(Step 6.5 NEEDS ATTENTION). Add `.translate-last-review` and `.translate-report-*.json` to the517project's ignore file if not already ignored.518519## Step 8.5 — Update project memory520521Maintain the toolkit's registry + per-project memory so the next run benefits:522- In `projects/registry.json` (git-ignored/local — create it as `{ "version": 1, "projects": [] }`523 if missing), set the project's `lastRunAt` to today (`date` Bash call) and adjust `status` if it524 changed. If the project had **no** entry, offer to add one now (same shape as `/translate-init`525 Step 3.5) and create `projects/<slug>/notes.md` from the template.526- Append one line to the top of the `notes.md` **Run log**: `<date> — <scope> → <verdict>527 (<languages>, <n> fixes)`. If the run made a durable decision (a terminology call, a new528 do-not-translate item, a format quirk discovered), add it to the relevant notes section — keep it529 to decisions, not a transcript.530Skip only if the user explicitly asked not to use project memory.531532## Step 9 — Report533534Print a tight summary from the Lead's report + gates + commit:535- **Verdict**, **Specialization** used, **Source → Targets**, **Scope** (base→HEAD / full / files, N files).536- **Panel routing**: K Junior / M Senior batches; any tier overrides the Lead applied.537- **Per-batch outcomes** (condensed): ACCEPTED / ACCEPTED WITH N INLINE FIXES / RETURNED+re-reviewed / OPEN QUESTION.538- **Sweeps** S1/S2/S3 totals (or n-a for plain text).539- **Flags by category** C1–C7 + S1–S3.540- **Completeness gate** (MANDATORY line, even on a clean run — covers file coverage **and** catalog541 key-set parity; state "all target languages complete (key-set parity + file coverage)" positively,542 or `⚠ Still partial: <lang> — <what's missing>`).543- **Terminology research**: ran (N terms — X high / Y medium / Z low confidence) / reused saved544 glossary / skipped (state which). Glossary: `projects/<slug>/glossary.csv`.545- **Queries (async, non-blocking)**: N items logged to `projects/<slug>/queries-<date>.md` for your546 review whenever you want — or "none". Never block the run on these.547- **Open questions** to the user (cycle-capped residuals, broken source, suspicious data).548- **Gates**: verify / build result. **Project gates** (if any ran): per gate + language, the outcome549 — `pass` / `refined→pass (N retries)` / `reverted` / `⚠ overridden (findings shipped)` / `skipped550 (--no-gates)`. A reverted or unresolved gate means the run did **not** commit — say so.551- **Commit** `<hash> <subject>` (not pushed) or "delivered N files" / "no changes".552- **Marker** advanced/left.553554Always end the report with these two footer lines, verbatim:5551. **Attribution (always present, every run — not configurable):**556 `Translated with Translation Agency <version> — https://github.com/roshaw/claude-translation-agency`557 (read `<version>` from the toolkit's `VERSION` file). This credits the tool in the operator-facing558 report only; it is independent of `config.creditInCommit`, which controls whether the same credit559 also appears as a commit trailer (Step 7).5602. **Disclaimer** (point-of-use reminder, not optional): `⚠ AI-generated translation — review before561 publishing; human sign-off recommended for legal/medical/financial/safety-critical content.` If the562 run's specialization is a high-stakes / safety-critical domain — `legal`, `finance`, or `medical`,563 or any custom module of comparable stakes (health, financial, legal, safety) — make it a full564 sentence and bold it, since the stakes are higher.565566## Reference567568- Panel: `.claude/agents/translate-lead.md` (Lead), `translate-senior.md` (Senior),569 `translate-junior.md` (Junior). Terminology research: `translate-researcher.md`.570- Per-project glossary + queries: `projects/<slug>/glossary.csv`, `projects/<slug>/queries-<date>.md`.571- Specializations: `specializations/<name>.md` (default `general.md`); how they work:572 `specializations/README.md`.573- Config: `translation.config.json` (source/target langs, specialization, formats, verify/build).574- Siblings: `/translate-init` (generate a project's config + register it), `/translate-add-locale` (scaffold a575 brand-new UI language into a codebase, then hand off here), `/translate-audit` (read-only coverage576 audit — which languages are missing what, and how to fix it).577- Projects registry + per-project memory: `projects/registry.json`, `projects/<slug>/notes.md`578 (contract in `projects/README.md`).579- Marker (untracked, per-machine): `.translate-last-review`.