Translate new strings
Skill jimmynotjames/simple-recurring-budgets/.cursor/skills/translate-new-strings
Wren: An iOS app for fast, easy budgeting on the go
npx -y skills add jimmynotjames/simple-recurring-budgets --skill translate-new-stringsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Translate newly-added or stale keys in Localizable.xcstrings to all 49 App Store storefront locales. Use after adding any LocalizedStringResource / String(localized:) / Text("…") key, or when check_translations.py reports issues. Drives the scripts/translate_catalog/ pipeline in subset mode with parallel per-locale subagents.
SKILL.md
18.3 KB, as published. Nobody here has run it
Translate new strings
Canonical recipe for getting Localizable.xcstrings back to a fully-translated state
after adding, changing, or invalidating keys. This skill drives the existing
scripts/translate_catalog/ pipeline in subset mode.
Definition of done: python3 scripts/check_translations.py exits 0. That is the same
check lefthook.yml runs on pre-push, so a clean exit here guarantees the push will
pass the translation gate.
Hard rules
- Never write ad-hoc Python (
python3 -c "..."heredocs, throwawaytmp/*.py) to slicesource.json, filter outputs, add catalog keys, remove catalog keys, or post-process anything. Every operation in this pipeline has a flag on one of the existing scripts. If you find yourself reaching forpython3 -c, stop — the right move is to extend one of the existing scripts (or add a new primitive toscripts/translate_catalog/) so the workflow stays disciplined and the permission surface stays narrow. Treat the urge to write inline Python as a signal that this skill is missing a primitive.- Adding keys →
add_keys.py(see Step 0a) - Updating English source text →
update_keys.py(see Step 0b) - Renaming keys →
rename_keys.py(see Step 0c)
- Adding keys →
- Never skip the pre-push checks at the end. Treat anything less than
exit 0fromcheck_translations.pyas not-done and loop back. - All commands run from repo root. Paths in this skill are repo-relative.
- Register notes are intentionally duplicated — keep them in sync, don't DRY them.
The per-locale formality guidance in
REGIONAL_NOTES(scripts/translate_catalog/dispatch_prompts.py) deliberately overlaps withCULTURAL_NOTESin the metadata pipeline only on the formality decision per language (issue #173, closed without consolidation). If you change a language's formality decision (e.g. de "du"→"Sie") here, mirror it inCULTURAL_NOTES; wording may differ, the formality call must not. Don't try to merge the two maps.
Recipe
Pre-flight — clear stale pipeline outputs
Before anything else, clear any per-locale leftovers from a previous run. validate.py
and merge.py read every file in tmp/translate-outputs/ (not just this run's
manifest), so stale files silently contaminate the run — dispatch_prompts.py only
cleans the manifest's locales, this clears any others too.
python3 scripts/pipeline_tmp.py status translate # inspect leftovers
python3 scripts/pipeline_tmp.py clean translate # clear them (allowlisted; no prompt)
0. Catalog maintenance (before translating)
Run whichever of these applies before Step 1. All three scripts are pre-approved in
.claude/settings.json and will not prompt. Do not use inline Python for any of
these operations.
Both invocation forms shown below work without prompting: the heredoc form (no temp
file, preferred for small batches) and the temp-file form (useful for large batches).
For the temp-file form, write JSON to tmp/<anything>.json — the Write(tmp/**) and
Bash(python3 scripts/translate_catalog/<script>:*) permissions are both pre-approved.
0a. Add new keys
Use when String(localized: "key.name") exists in Swift but the key is not yet in
Localizable.xcstrings. Inserts the key with en.state: translated; leaves all other
locales absent so extract.py --missing flags them automatically.
python3 scripts/translate_catalog/add_keys.py - << 'EOF'
{
"my.feature.title": {
"comment": "Title shown on the My Feature screen",
"value": "My Feature"
}
}
EOF
Input shape: { "key": { "comment": "...", "value": "English string" } }.
Existing keys are skipped unless --force is passed.
0b. Update English source text
Use when the displayed English text for an existing key changes (copy edit, wording
change, etc.). Updates the en value and marks every existing non-en translation
needs_review so the full re-translation round-trip runs automatically via Step 1.
python3 scripts/translate_catalog/update_keys.py - << 'EOF'
{
"my.feature.title": {
"value": "Revised English text",
"comment": "Updated hint (optional — omit to keep existing comment)"
}
}
EOF
Input shape: { "key": { "value": "New English string", "comment": "..." } }.
comment is optional. Keys absent from the catalog are skipped with a warning.
0c. Rename keys
Use when a key identifier changes (e.g. a UI element moved to a different screen or
section and the key path should reflect that) but the displayed text and all translations
stay exactly the same. Copies the full entry — comment, extractionState, every locale
translation — from the old key to the new key, then removes the old key. No
re-translation is needed after a pure rename.
python3 scripts/translate_catalog/rename_keys.py - << 'EOF'
{
"old.screen.featureName": "new.screen.featureName"
}
EOF
Input shape: { "old.key": "new.key" }. Supports --dry-run to preview changes
and --force to overwrite an existing new key.
After renaming, update the Swift call sites to use the new key, then run
check_source_strings.py (Step 5) to confirm no stale references remain. No need to
run extract.py --missing unless you also changed the English text.
0d. Convert count strings to plurals
Use when a string interpolates a count (%lld) and reads wrong at 1 (e.g. "1 logged
expenses"). Converts the key's English to a CLDR variations.plural block and clears the
non-en locales so they re-translate as plurals (each locale gets the categories it needs).
python3 scripts/translate_catalog/pluralize_keys.py - << 'EOF'
{ "my.count.key": { "one": "%lld item", "other": "%lld items" } }
EOF
other is required. Then run Step 1 onward — the pipeline emits/validates/merges plural keys
end to end (subagents return a { "key": {"one": …, "other": …} } object for plural keys per
rule 7 in the template). The pre-push gate (check_translations.py) already validates plural
variations.
0e. Force re-translation when a glossary term changed (English unchanged)
Use when the English source text is unchanged but the translation must change — most
commonly when a glossary term was re-translated (e.g. "Carry-Over" stopped being a protected
English term and gained per-locale renderings), so every key that embeds that term must flow
through translation again. update_keys.py is the wrong tool here (it's for English changes and
rewrites the en value); use invalidate_keys.py, which marks the keys' non-en locales
needs_review and never touches en.
python3 scripts/translate_catalog/invalidate_keys.py --keys-file tmp/keys.txt
# or
python3 scripts/translate_catalog/invalidate_keys.py --keys k1,k2,...
Supports --dry-run. Then run Step 1 onward — extract.py --missing re-emits the invalidated
keys and the normal dispatch → validate → merge round-trip re-translates them with the updated
glossary term injected.
1. Detect what needs translating
python3 scripts/translate_catalog/extract.py --missing
This writes:
tmp/translate-inputs/source.json— the union of all keys that are missing or stale in at least one locale, with English source + comment + format specifiers.tmp/translate-inputs/manifest.json—{locale: [keys...]}telling you exactly which locales need work and for which keys.
If the script reports "No missing or stale translations found", you are done — skip to step 5 to confirm.
2. Compose per-locale prompts
python3 scripts/translate_catalog/dispatch_prompts.py
This writes one ready-to-dispatch prompt per locale to
tmp/translate-prompts/{locale}.md. Each prompt has {LOCALE_NAME}, {LOCALE_CODE},
{REGIONAL_NOTE}, {GLOSSARY}, and {SOURCE_JSON} (sliced to just that locale's missing keys)
already substituted.
{GLOSSARY} is filled automatically from scripts/translate_catalog/glossary.json: for each
locale, dispatch_prompts.py injects the agreed translations of any glossary term that appears in
that slice, so new strings stay terminologically consistent with the rest of the app (rule 6 in the
template tells the subagent how to use them, including for sub-parts of a string). Nothing to do
here — it just works when the glossary is populated.
By default this also deletes any stale tmp/translate-outputs/{locale}.json files
for the locales in the manifest, so step 3's subagents start from a clean slate and
step 4's validate.py --subset doesn't trip on leftover keys from a prior run on a
different branch. Pass --no-clean only if you're manually iterating on one locale's
output and want to preserve the others.
3. Translate each locale's slice
Cross-tool execution. On Claude Code, dispatch one
translation-localesubagent per locale in parallel (below). On Cursor or any tool without a subagent primitive, run the same step inline and serially: for eachtmp/translate-prompts/{locale}.md, read it, produce the translation JSON yourself, and writetmp/translate-outputs/{locale}.json— then continue to step 4. Same scripts, same gates, same result. Canonical:AGENTS.md > Cross-cutting concerns > Running the translation pipelines.
Claude Code — one translation-locale subagent per locale, in parallel
For every tmp/translate-prompts/{locale}.md that exists, invoke an Agent with:
subagent_type:translation-locale(defined in.claude/agents/translation-locale.md)- A short dispatch prompt that tells the subagent which prompt file to read and where
to write its JSON output. Example:
Read
/abs/path/tmp/translate-prompts/ja.mdand follow the rules in it. Write the resulting JSON object (nothing else) to/abs/path/tmp/translate-outputs/ja.json.
The subagent definition restricts the subagent to Read + Write only, defaults to
haiku, and encodes the "JSON only, this file only" contract. Do not pass
subagent_type: general-purpose — the narrower agent is what makes the dispatches
auto-approvable in this project's .claude/settings.json.
Send all subagent calls in a single message so they run concurrently. With 49 locales × small key counts this typically finishes in well under a minute.
The subagent's prompt already contains every translation rule — placeholder preservation, tone, regional dialect notes, JSON-only output. Do not modify it.
4. Validate, then merge
python3 scripts/translate_catalog/validate.py --subset
--subset mode only checks the keys actually present in each output file (the manifest
slice), so partial outputs are first-class. If any locale fails:
- Re-read its prompt, re-dispatch a fresh subagent (often the same prompt works on retry), then re-validate.
- Format-specifier mismatches are the most common failure — the subagent's translation
dropped or duplicated a
%@/%lld. Retry usually fixes it.
Once validate.py --subset exits 0:
python3 scripts/translate_catalog/merge.py
Merge only writes catalog entries for keys present in the output files, and refuses to
overwrite a non-empty entry with an empty value, so existing translations are safe.
For each key it merges, merge.py also promotes that key's English source from
state: "new" → "translated" (Xcode auto-extraction lands new keys as "new"). This
means a key that went through this pipeline will not trip the NEW [en] (source)
gate in step 5 — the previous manual add_keys.py --force + re-merge dance is no longer
needed for pipeline keys.
Fast path — steps 4 and 5 in one shot. Once the subagents have written
tmp/translate-outputs/, run the deterministic tail as a single command (every
sub-command is individually allowlisted, so it never prompts):
python3 scripts/translate_catalog/validate.py --subset \
&& python3 scripts/translate_catalog/merge.py \
&& python3 scripts/check_translations.py \
&& python3 scripts/check_source_strings.py
If validate.py --subset fails for a locale, re-dispatch that locale's subagent and
re-run the chain. Don't split these into four separate tool calls — that's slower and
was the source of past friction.
4b. Keep the glossary current (after merge)
New features often introduce terminology that now recurs across keys. Keep the glossary
(scripts/translate_catalog/glossary.json) in step with it:
python3 scripts/translate_catalog/glossary_sync.py --detect
This is deterministic (no LLM). It detects high-confidence new terms — an English string now
reused across ≥2 keys but not yet in the glossary — and writes them to tmp/glossary/terms.json;
more ambiguous frequent words go to tmp/glossary/candidates_review.json for you to skim. If it
reports new high-confidence terms, add them to the glossary (Opus):
python3 scripts/translate_catalog/glossary_build.py --dispatch
# fan out one glossary-locale (Opus) agent per tmp/glossary/prompts/{locale}.md, in a single
# message; each writes tmp/glossary/outputs/{locale}.json (see the audit/glossary docs)
python3 scripts/translate_catalog/glossary_build.py --merge
--merge appends to glossary.json (it never drops existing terms). Skip this step entirely if
--detect reports nothing new. The fuller glossary build/rebuild flow lives in
scripts/translate_catalog/README.md.
5. Authoritative pre-push gate
python3 scripts/check_translations.py
python3 scripts/check_source_strings.py
Both are run by lefthook.yml on pre-push.
check_translations.pyis the definition of done for this skill. It walks the catalog (not the per-locale output files), so it catches anything that didn't actually merge in correctly. If it reports missing/stale(key, locale)pairs:- Run
extract.py --keys k1,k2,...for just those keys, or re-run--missing. - Loop back to step 2.
- Run
check_source_strings.pycatches hard-codedText("English string")in Swift files. A failure here means there's an un-keyed string in production code — fix that first (add to xcstrings, then re-run this skill), since untranslated keys are downstream of un-keyed strings.NEW [en] '<key>' (source)means the key's English source state is still"new"(Xcode-extracted, not yet reviewed). For keys you just translated this is now auto-resolved bymerge.py(see step 4). It can still surface for a key whose 49 locales were already complete (so nothing merged) but whose en stayed"new"— in that rare case runextract.py --keys <key>then the step-4 fast-path chain, or re-runmerge.py(the existingtmp/translate-outputs/<locale>.jsonare reused and the en promotion fires). Do not hand-edit the catalog.
If check_translations.py reports keys that exist in the catalog but no longer have a
Swift reference (i.e. the code dropped the String(localized: ...) call), those keys
are orphaned. Remove them with:
python3 scripts/translate_catalog/remove_keys.py --keys k1,k2,...
# or
python3 scripts/translate_catalog/remove_keys.py --keys-file path/to/keys.txt
This deletes every locale entry for those keys cleanly. Use --dry-run first to see
what would be removed.
6. Build + test
Standard four-step from AGENTS.md:
make format && make lint-fix && make build && make test
7. Cleanup — offer to clear tmp working files
Once check_translations.py is green, offer to clear this pipeline's gitignored tmp
files. Ask first (the intermediate JSON is handy if the user wants to inspect a locale);
on a yes:
python3 scripts/pipeline_tmp.py clean translate
Subagent prompt invariants
The prompt file generated by dispatch_prompts.py is the single source of truth.
Subagents are told to:
- Preserve every format specifier exactly (count and form; order may change for grammar).
- Use the
commentfield for context. - Match Apple's first-party iOS app voice for the target locale.
- Leave
Wren,iCloud, and other Apple-untranslated proper nouns in English. - RTL locales (ar, he, ur): never start a sentence or UI string with a Latin brand token (
Wren,iCloud). In RTL rendering a Latin-initial string pops to the visual end of the line. Lead with a native word; the brand can follow mid-sentence. - Output only the JSON object — no markdown fences, no prose.
If you find yourself wanting to "remind" the subagent of one of these rules in the
dispatch message, instead fix PROMPT_TEMPLATE.md so the rule survives across runs.
When to fall back to full backfill
This skill is the right tool when keys have been added/changed for a feature. It is not the right tool when:
- Changing the translation model and wanting to regenerate every existing translation.
- Seeding a brand-new catalog from scratch.
- Recovering from a corrupted catalog.
For those, see "Full backfill" in scripts/translate_catalog/README.md.
File layout reference
scripts/translate_catalog/
locales.py # LOCALES list + LOCALE_NAMES
add_keys.py # Step 0a: register new en keys; JSON file or stdin
update_keys.py # Step 0b: update en source text + invalidate translations; JSON file or stdin
rename_keys.py # Step 0c: rename keys preserving all translations; --dry-run; JSON file or stdin
extract.py # --missing | --keys | --keys-file | (no flags = all)
dispatch_prompts.py # manifest + template → tmp/translate-prompts/; cleans stale outputs by default
validate.py # --subset for partial outputs
merge.py # --keys filter; rejects empty values
remove_keys.py # --keys | --keys-file; deletes orphaned keys with all locale entries
PROMPT_TEMPLATE.md # subagent prompt template
README.md # script reference
scripts/
check_translations.py # authoritative pre-push gate
check_source_strings.py # hard-coded Text("...") detector
.claude/agents/
translation-locale.md # subagent definition used by step 3 (Read+Write only)
tmp/
translate-inputs/source.json
translate-inputs/manifest.json
translate-prompts/{locale}.md
translate-outputs/{locale}.json