Translation Sync Checker
SkillAI & modelsDiff English source strings against each translation, report stale/missing keys, and optionally update translations using parallel agents
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Translation Sync Checker skill
What this skill tells your AI
The instructions your AI receives, as published by itmaybejj/editoria11y in .agents/skills/check-translations/SKILL.md and read by ahel’s review.
Uses src/lang/TRANSLATION_MANIFEST.json to track which English commit each translation was last synced to, then generates targeted diffs to identify what changed.
Scope
- We translate:
src/lang/*.jsfiles only (editoria11y's own strings) - We do NOT translate:
src/sa11y-lang/*.jsfiles (managed in the Sa11y repo) - We do report: changes in
src/sa11y-lang/en.jsfor cross-repo sync review
Two modes of work
This skill runs in two modes depending on the state of each translation file:
-
Sync mode (the common case): a translation already exists for a language; the skill diffs the English source between the translation's recorded commit and HEAD, and patches each language. This is described in Steps 1–2 below.
-
First-pass mode: a stub file exists in
src/lang/but the editoria11y-specific objects (testNames,tips,interfaceStrings) are empty. Stubs carry an// UNTRANSLATED STUBheader and are listed underpendingTranslationsinTRANSLATION_MANIFEST.json. They need a complete first translation pass, not a diff, and live inscripts/build.jsunder a separatependingLangsarray (not built). See First-pass translations below. As of this writing there are no pending languages, but the workflow is documented for the next batch.
Manifest location
src/lang/TRANSLATION_MANIFEST.json — contains:
englishSources: the editoria11y English base files (baseAll.js,baseEnglishOnly.js)sa11yEnglishSource: the Sa11y English source (src/sa11y-lang/en.js) — monitored for changes but not translated herebaseline: the commit all translations were initially baselined totranslations: map of{ "src/lang/xx.js": "<commit>" }for each translation file
Step 1: Identify what changed in English
Diff from the oldest translation commit to HEAD:
git diff <oldest_commit> HEAD -- src/lang/baseAll.js src/lang/baseEnglishOnly.js
If no diff, all translations are up to date — skip to Step 3.
If there is a diff, summarize:
- Added keys (new strings that need translation)
- Removed keys (strings that should be deleted from translations)
- Changed values (English wording changed — translation needs update)
Step 2: Update translations (parallel agents)
Launch one Agent per language in parallel (use a single message with multiple Agent tool calls). Each agent receives the same English diff context.
Deploy the first 8 agents in one batch, then pipeline: launch another agent as soon as one finishes, rather than waiting for a full batch to complete. 8 is the user-confirmed safe ceiling on this account (2026-05-10) — earlier 16-in-flight attempts hit transient server-side rate limits.
Shared spec file (recommended when 3+ languages are affected)
Instead of inlining the full English diff in every agent prompt, write the diff once to a shared file (e.g., /tmp/ed11y-translation-spec.md) and point each agent at it. This keeps per-prompt tokens low and guarantees consistency across languages. The spec file should contain:
- The OLD/NEW English for every changed key
- Recurring patterns to watch for (e.g., "all
Alt text:prefixes are now wrapped in<strong>...</strong>") - The critical rules block
Critical agent instructions
Include ALL of the following rules in every agent prompt — these were learned from production failures:
CRITICAL RULES FOR WRITING TRANSLATION FILES:
1. NEVER use curly/smart quotes (' ' " ") as JavaScript string delimiters.
Only use straight single quotes ('), straight double quotes ("), or backticks (`).
2. When a translated string contains an apostrophe (e.g., Italian "l'immagine",
French "l'image"), you MUST either:
- Use backtick delimiters: `L'immagine è marcata...`
- Or escape the apostrophe: 'L\'immagine è marcata...'
NEVER use curly quotes to work around apostrophes.
3. Use the Edit tool with targeted old_string/new_string replacements.
Do NOT use the Write tool to rewrite the entire file — this risks
corrupting unchanged content or introducing encoding issues.
4. After the last edit is made, verify the file parses by running:
node -c <filepath>
If it fails, fix the syntax error before finishing. Each node command pauses for user error, so try to group this into as few commands as possible.
5. Preserve the exact indentation style of the file (tabs, not spaces).
6. Keep all HTML markup, ${why.fix}, %(NAME) placeholders, and URLs
exactly as-is — only translate the human-readable text.
7. INVISIBLE UNICODE HAZARD (especially fr.js): the Edit tool does byte-exact
matching on old_string. French uses U+202F (narrow no-break space) and
sometimes U+00A0 (no-break space) before ":", ";", "?", "!". These look
identical to regular spaces when displayed. Other languages may use
U+2011 (non-breaking hyphen) or U+2019 (curly apostrophe) inside words.
If Edit fails with "String to replace not found" on text that looks
correct, suspect invisible characters.
8. ESCAPE HATCH: if Edit fails twice on what looks like the same string,
STOP LOOPING. Report back to the main process that Edit cannot match
the target — include the target text and the error. The main process
can inspect raw bytes via Bash + python3 and do the surgical edit.
Do not invent XXX/YYY marker strategies or try to guess Unicode
codepoints — those have historically left files corrupted.
Agent prompt pattern
Update the [Language] translation file at [path].
English changes since last sync:
- ADDED testName: KEY = "English string"
- ADDED tip: KEY = `<p>English tip...</p>`
- CHANGED tip KEY — new English: `<p>New English...</p>`
- REMOVED: OLD_KEY
CRITICAL RULES FOR WRITING TRANSLATION FILES:
[paste the rules block above]
Read the file first. Use the Edit tool for targeted replacements.
Insert new keys alphabetically among existing keys.
Match the existing translation style and tone.
Run `node -c [filepath]` when done to verify syntax.
After all agents complete
- Run
npm run buildto verify everything compiles - Fix any syntax errors (most likely: unescaped apostrophes or curly quotes)
- Run the markup linter (see below) and fix anything it flags
- Update each translation's commit in
TRANSLATION_MANIFEST.jsonto current HEAD
Markup validation (REQUIRED after any translation change)
node .agents/skills/check-translations/check-markup.mjs # all locales
node .agents/skills/check-translations/check-markup.mjs src/lang/fr.js # one file
This linter renders each locale's tooltip strings (expanding ${why.*}) and reports
unbalanced/misnested HTML tags and bare (unwrapped) URLs. It imports each file in
its own child process so the shared Sa11y-state mutations between en/en-us/en-ca/en-gb
can't cross-contaminate. Exit code is non-zero on any finding (CI-friendly). It is NOT run by
npm run build, so run it explicitly.
Two recurring corruption classes it catches (both seen in the machine-translated bases — they do NOT come from this skill's diff-driven Edits, but a sync is the natural time to find them):
- Stripped anchors: an
<a href="URL">opening tag is lost, leaving the bareURLglued to the (translated) link text and an orphan</a>. Fix: the bare URL is the English canonicalhrefas a prefix, so re-wrap it: replace the canonical URL (when NOT preceded byhref=") with<a href="CANONICAL">. Collect canonicals fromsrc/lang/baseAll.js+src/sa11y-lang/en.js; process longest-first to avoid prefix collisions (e.g.…/link_textvs…/link_text#alt_link). Watch two edge cases the bulk pass misses and must be hand-fixed: a bare URL sitting right after a literal text quote ("), and a URL whose final path segment was itself machine-translated (e.g. pt-br turned…/Elements/titleinto…/Elements/títuloand glued the link text on). - Unescaped literal tags:
<code><title></code>/<code><head></code>should be<code><title></code>etc. — the raw<title>/<head>open real elements that never close. Fix: escape them (<title>→<title>).
Also watch for stray/misnested <p>/<span> (e.g. a string that starts with text then </p>
needs a leading <p>; a garbled <span style="</span> should be removed). Reconstruct the
intended structure from a clean sibling locale (de/es) or the English source.
Agent output-budget limits (relevant for large diffs)
Cross-repo lesson from the editoria11y-csa WordPress port and the
Drupal po-translations skill (both maintain ~140 KB .po files of
roughly the same content). The same sub-agent infrastructure powers
all three workflows, so these limits apply here too:
- 32 K output-token cap per response. A sub-agent that tries to
Writea single file ≥30 KB hits the cap mid-stream and fails. - 600 s stream watchdog. Even within the token cap, the
Writetool'scontentparameter streams character-by-character at the model's generation rate. A 30 KB UTF-8 payload in CJK/Cyrillic takes minutes; if no token reaches the parent within 600 s the watchdog kills the agent.
For this skill's typical workflow (diff-driven Edit calls, each
touching 5–50 lines), these limits don't bite — Edit calls are
small and the total tool_input streamed per agent is well under
the cap. The current Edit-based pattern is the correct choice
for this skill.
If a future first-pass translation of a brand new language is needed (a stub file becomes a complete translation in one agent run), the agent may need to produce 300+ string edits, which can approach the limits. In that case use the Python-dict-via-Bash escape hatch proven on the WP port:
- Agent writes
scripts/scratch/translate_<LANG>.pywith a flatT = {'KEY_NAME': 'translated string', ...}dict + amain()that readssrc/lang/<lang>.js, regex-replaces values inside thetestNames/tips/interfaceStringsobjects by key, and writes the file back. - Agent runs the script via
Bash(node)orBash(python3).
This dodges both limits: the script is ~half the size of the
equivalent edited .js file, and the actual file mutation happens
server-side (invisible to the agent's output token budget). Use
single-quoted Python strings (T['KEY'] = 'value') so curly
typographic quotes in translations don't conflict with delimiters.
Local fill vs agent dispatch — the break-even point
For diffs with ≤15 changed keys per language, fill them locally in the parent rather than dispatching an agent. Agent overhead is ~150 K input tokens (file + diff context) + ~30 K output (Edit calls
- reasoning) ≈ $2–4 at Sonnet rates. For ~10 keys that's $0.20–0.40 per key — orders of magnitude worse than the parent's incremental cost in-context.
For ~50+ changed keys per language, an agent amortizes the input cost and is the cheaper path.
Step 3: Report Sa11y English changes
Always check for changes in the Sa11y English source, even if editoria11y strings haven't changed:
git diff <oldest_commit> HEAD -- src/sa11y-lang/en.js
If there are changes, print a structured report for human review:
## Sa11y English String Changes (for cross-repo sync)
These changes are in `src/sa11y-lang/en.js` and need to be synced
to the Sa11y repo and other consuming projects.
### New keys
| Key | Value |
|-----|-------|
| NEW_KEY | "New English string" |
### Changed keys
| Key | Old value | New value |
|-----|-----------|-----------|
| CHANGED_KEY | "Old text" | "New text" |
### Removed keys
| Key | Old value |
|-----|-----------|
| OLD_KEY | "Was this" |
This report is for the developer to manually sync across repos — do NOT attempt to translate or modify src/sa11y-lang/ files.
English dialect variants
src/lang/en-gb.js (British) and src/lang/en-ca.js (Canadian) are thin
dialect overlays over baseAll.js. They do NOT re-translate everything —
they import baseAll.js and override only the keys that differ in spelling
or word choice.
When baseAll.js gains a new string that contains dialect-sensitive words
(e.g. color, colorblind, organize, visualize, emphasize,
capitalize, -ize/-ise verbs, -or/-our nouns), both dialect files
may need a matching override entry:
- en-gb (British): override
color→colour,organize→organise,visualize→visualise,emphasize→emphasise,capitalize→capitalise, and any other-ize/-ourdifferences. - en-ca (Canadian): override
-ourwords only (color→colour,colorblind→colourblind). Canadian English keeps American-izeendings, so most other words stay as-is.
Tips that embed ${why.headings} need the whole tip rewritten in
en-gb.js because the embedded block contains "organise". The list of
affected tips is in en-gb.js — look at britishTips.
First-pass translations
When a src/lang/<code>.js file is a stub (// UNTRANSLATED STUB header, empty testNames/tips/interfaceStrings), the language needs a complete first translation pass instead of a diff. Such stubs should be enumerated in TRANSLATION_MANIFEST.json under a pendingTranslations block, and the language code should live in scripts/build.js under a separate pendingLangs array (not in the active langs array — building a stub would ship a half-Sa11y, no-editoria11y bundle).
Workflow for a first-pass translation
- Pick a fully-translated reference file in
src/lang/whose tone you want to match (e.g.da.js,de.js,es.js). This is the structural model — same key set, same use of${why.fix},%(EL), etc. - Write a shared spec to
/tmp/claude/ed11y-translation-spec.mdwith the workflow, file paths, and the critical-rules block. Per-language prompts can then be tiny (just "your CODE is X; read the spec; tone notes for this language"). - For each pending language, launch one agent. Cap parallel dispatch at 8 agents — the harness has a parallel-tool-call limit. Pipeline; as agents finish dispatch the next agent.
- Each translator agent:
- Reads the stub file,
baseAll.js,baseEnglishOnly.js, and one reference translation. - Translates every key from
baseAll.js(NOTbaseEnglishOnly.js) into the target language. - Uses targeted
Editcalls to populatetestNames,why,tips, andinterfaceStrings, matching the alphabetization in the reference file. - Removes the
// UNTRANSLATED STUBcomment block once the file is complete. - Runs
node -c src/lang/<code>.jsto verify syntax. - Ends the translation pass, then proofreads using
/tmp/claude/ed11y-proofread-spec.md. Proofreaders polish for native-speaker naturalness, terminology consistency, grammar, and punctuation conventions of the target language. Each proofreader edits in place and re-runsnode -c.
- Reads the stub file,
- Promote the languages:
- Move each code from
pendingLangstolangsinscripts/build.js(alphabetical order). - In
TRANSLATION_MANIFEST.json, delete the entry frompendingTranslationsand add it totranslationswith the current HEAD commit. - Remove the
void pendingLangs;line fromscripts/build.jsifpendingLangsis now empty. - Run
npm run buildand verifydist/js/lang/<code>.jsand<code>.umd.jsbundles appear.
- Move each code from
Tamil (ta) — special handling whenever a Tamil stub is created
Tamil in Sa11y is human-translated, not machine-translated. Treat the human translator's voice as authoritative.
If you ever create a new Tamil stub (or re-translate the existing src/lang/ta.js from scratch), the dispatching prompt for Tamil — and ONLY Tamil — must:
-
Include a copy of the full contents of
src/sa11y-lang/ta.jsinlined in the prompt (read it and inline it; do not just reference the path). -
Include a vocabulary-anchor table mapping common accessibility concepts (alt text, heading, link, screen reader, accessible name, label, input field, image, button, element, attribute, contrast, etc.) to the human translator's chosen Tamil terms. Pull these from
src/sa11y-lang/ta.js. -
Include this directive verbatim:
TAMIL-SPECIFIC: src/sa11y-lang/ta.js was written by a human translator, not by machine translation. Defer to its style, register, terminology, sentence rhythm, and word choice. Match how that file phrases analogous accessibility concepts rather than inventing fresh terms or copying tone from machine-translated sibling files. When in doubt, mirror the Sa11y wording even if it would read differently in machine translation. -
Do NOT include the standard "match the existing translation style and tone of sibling files" instruction for Tamil — the directive above takes precedence.
-
The Tamil proofreader prompt must apply the same directive: verify the translator deferred to the Sa11y voice and align any drifted terms back to it.
-
All other rules in the critical-rules block still apply (no curly quotes, escape apostrophes, preserve placeholders, run
node -c, etc.).
Operational notes from the last batch
- Each translator + proofreader pair costs roughly 60k–180k tokens depending on language complexity. The Lithuanian and Slovak proofreaders ran 100+ tool calls — that's normal for languages with rich case morphology.
- Cyrillic (
bg), Tamil script (ta), and accented Latin scripts all save fine as UTF-8 insrc/lang/. If any character looks garbled ingit diff, runfile src/lang/<code>.jsto confirm encoding. - The dev report and consuming CMS plugins (Drupal, WordPress) discover languages via the built
dist/js/lang/files, NOT viasrc/lang/. So a stub inpendingLangsis invisible to end users until it's promoted tolangs.
Files to skip
These are NOT translation targets — never modify them:
src/lang/en.js— English bundle assemblersrc/lang/en-us.js— English US variant assemblersrc/lang/_template.js— template filesrc/lang/baseAll.js— English source (testNames, interfaceStrings, tips)src/lang/baseEnglishOnly.js— English-only overridessrc/sa11y-lang/*.js— all Sa11y lang files (managed externally; exception: readsrc/sa11y-lang/ta.jsto feed the Tamil first-pass agent — see Tamil-specific handling above)
File structure reference
Each src/lang/*.js translation file contains:
- An import of its corresponding
src/sa11y-lang/*.jsfile - A
testNamesobject (short alert titles) - A
tipsobject (detailed tooltip HTML using template literals with${why.fix},%(placeholder)syntax) - An
interfaceStringsobject (UI labels) - An export combining everything with Sa11y strings via
Object.assign
Signals
- GitHub stars
- 54
- Forks
- 12
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
check-translations- Source
- github.com/itmaybejj/editoria11y