Appstore translate metadata
Skill jimmynotjames/simple-recurring-budgets/.claude/skills/appstore-translate-metadata
Wren: An iOS app for fast, easy budgeting on the go
npx -y skills add jimmynotjames/simple-recurring-budgets --skill appstore-translate-metadataAssembled 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
Transcreate the App Store listing (name, subtitle, keywords, promotional text, description, release notes) from English into all 49 App Store storefront locales under fastlane/metadata/. Invoked via /appstore:translate-metadata. Use after editing any fastlane/metadata/en-US/*.txt, or when check_metadata.py reports gaps. Drives the scripts/translate_metadata/ pipeline in subset mode with parallel per-storefront Opus subagents, then runs an autonomous Opus semantic-audit + remediation refine pass before the gate. Orchestration is model-light enough to run on Sonnet; the Opus subagents do the translation and audit. This is the App-Store-metadata sibling of translate-new-strings (in-app strings) and appstore-generate-screenshot-seeding (screenshot demo data).
SKILL.md
23.3 KB, as published. Nobody here has run it
Translate App Store metadata
Canonical recipe for getting fastlane/metadata/ to a fully-transcreated state
after authoring or editing the English (en-US) listing copy. Drives the
existing scripts/translate_metadata/ pipeline in subset mode.
Definition of done: python3 scripts/translate_metadata/check_metadata.py
exits 0 — every translatable field the English source has authored is populated
and within its character limit across all 49 target storefronts.
This is the metadata pipeline (App Store listing). For in-app UI strings in
Localizable.xcstrings, use the separate translate-new-strings skill. They use
different locale code systems (storefront vs runtime); metadata_locales.py owns
the mapping.
Preflight — orchestrator model (before anything else)
Before any other step, run the orchestrator-model preflight (canonical:
AGENTS.md → "Orchestrator-model preflight"). This skill is tuned to orchestrate on
Sonnet; if the current session model is not Sonnet, stop and confirm
(AskUserQuestion on Claude Code, a markdown block on Cursor) before running
anything — Opus works but is pricier for no quality gain, and a model weaker than
Sonnet may make the Step-5 threshold/loop judgments unreliable. The metadata-locale
and metadata-audit-locale workers stay Opus regardless (pinned in their agent
definitions), so switching the session to Sonnet never weakens the translation or
audit.
Autonomy
Run this whole pipeline autonomously, end to end, without pausing for approval
on mechanical steps — extract, dispatch, fan-out, validate, merge, the semantic
audit + remediation loop (Step 5), and the gate are all routine and pre-approved
in .claude/settings.json. Do not ask "shall I proceed?" between steps, and do
not ask permission to retry a failed locale or to auto-fix audit findings.
There are exactly two deliberate human checkpoints; everything else you decide and execute yourself:
- Up front, before any pipeline step — confirm the English source copy is final and ready to transcreate (the source-ready confirmation below). This is the only gate that blocks the run from starting.
- Mid-run — genuine content questions about the marketing copy itself that the subagents flag (Step 4a). Surface those in a single batch and keep going; they refine specific strings, they don't block the run.
Model roles — orchestrator vs. workers
This skill is built so the orchestrator (the agent running these steps) can be
Sonnet: every step is a script call, a parallel fan-out, or a deterministic
branch on a PASS/FAIL/severity threshold — there are no orchestrator-level
linguistic judgments. All the language quality lives in the Opus worker
subagents: metadata-locale (transcreation) and metadata-audit-locale (quality
audit) are both pinned to model: opus in their agent definitions and must stay
that way — they out-class the copy they produce/grade. So: Sonnet orchestrates,
Opus translates and audits. Keep the thresholds and round caps in Step 5 fixed
rather than "deciding" per run, so the Sonnet orchestrator never has to judge copy
itself.
Hard rules
- Never write ad-hoc Python (
python3 -c, throwawaytmp/*.py) to slice the source, filter outputs, or post-process metadata. Every operation has a flag on one of the existingscripts/translate_metadata/scripts. If you reach for inline Python, stop — extend a script instead so the permission surface stays narrow. - Never skip the gate. Treat anything less than
exit 0fromcheck_metadata.pyas not-done and loop back. - The brand "Wren" stays verbatim in every locale — never translated,
transliterated, or glossed. (This is enforced by the prompt and validated by
validate.py; do not "remind" subagents in the dispatch message — fixPROMPT_TEMPLATE.mdif a rule needs strengthening.) - RTL storefronts (ar, he, ur): never start a line 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 may follow mid-sentence. - All commands run from the repo root.
- Register notes are intentionally duplicated — keep them in sync, don't DRY them.
The per-storefront formality guidance in
CULTURAL_NOTES(scripts/translate_metadata/dispatch_prompts.py) deliberately overlaps with the in-appREGIONAL_NOTESonly on the formality decision per language (issue #173, closed without consolidation). If you change a market's formality decision (e.g. de-DE "du"→"Sie") here, mirror it inREGIONAL_NOTES; wording may differ, the formality call must not. Don't try to merge the two maps.
Source-ready confirmation (human checkpoint — do this first)
The pipeline transcreates whatever non-empty translatable fields exist in the English source into all 49 storefronts, so a typo or an accidentally-blank field here propagates everywhere and costs a full re-run to undo. Before any pipeline step, confirm the source with the human — this is the one gate that blocks the run from starting.
Source location — the only files you edit by hand:
- Folder:
fastlane/metadata/en-US/ - Translatable fields:
name.txt,subtitle.txt,description.txt,keywords.txt,promotional_text.txt,release_notes.txt - The
*_url.txtfiles (marketing_url,privacy_url,support_url) are copied verbatim to every locale, not translated — leave them out of the confirmation.
Read those six files and present, in a single message: the folder path, each field's current authored / blank state, and a short preview of the authored values.
Run the source-voice pre-check first (deterministic — don't eyeball it):
python3 scripts/translate_metadata/check_source_voice.py
It scans the authored en-US fields for brand-voice slips that would otherwise propagate to all 49 storefronts and get re-flagged by every semantic auditor: exclamation marks, ALL-CAPS shouting, and salesy superlatives ("the best", "amazing", "revolutionary"). Exit 0 = clean, continue. Exit 1 = it found something: these are warnings, not hard errors, but you must halt and surface every finding to the human as part of the source-ready prompt below — include the field, the matched token, and the voice rule it breaks, and let them choose to fix the en-US source first or proceed anyway. Do not silently proceed past a non-zero exit, and do not auto-edit the source to "fix" it — the wording is the human's call.
WARNING — blank fields clear all translations. When a translatable field is
left blank in en-US, merge.py erases the content of every existing translated
.txt for that field across all storefronts (it reads the blank state straight
from the en-US folder), and check_metadata.py enforces that those files stay
empty. This is intentional: blank in English = blank everywhere. But it is
destructive if the field is blank by accident.
For any blank field in the summary, display a prominent notice:
[BLANK — will erase all existing translated copies of this field]
Then ask them to confirm the copy is final and ready to transcreate:
- Claude Code:
AskUserQuestion. - Cursor: a short markdown block (mirror the orchestrator-model preflight).
Wait for confirmation before Step 0. If the human wants to edit copy first, let them, then re-read and re-confirm. This upfront prompt is a deliberate exception to "run translation work without prompting" — it is specific to this metadata skill, where the blast radius of a bad source string is all 49 storefronts.
Recipe
0. Pre-flight — clear stale pipeline outputs
Before extracting, clear any per-storefront leftovers from a previous run. validate.py
and merge.py read every file in tmp/metadata-outputs/ (and the audit reads
tmp/metadata-audit-outputs/), not just this run's manifest — so stale files silently
contaminate the run.
python3 scripts/pipeline_tmp.py status metadata # inspect leftovers
python3 scripts/pipeline_tmp.py clean metadata # clear them (allowlisted; no prompt)
1. Detect what needs transcreating
python3 scripts/translate_metadata/extract.py --missing
Writes:
tmp/metadata-inputs/source.json— every authored en-US field with its value and character limit.tmp/metadata-inputs/manifest.json—{storefront: [fields...]}listing the gaps to fill.
--missing only finds absent fields, not stale ones. If you edited an
existing en-US field (e.g. rewrote description.txt) rather than added a new
locale, every storefront already has some content for it, so --missing
reports an empty manifest and nothing re-translates. To force a re-transcreation
of edited field(s) across all 49 storefronts, name them explicitly:
python3 scripts/translate_metadata/extract.py --fields description # or: --fields description keywords
--fields writes the same manifest.json shape (all storefronts × the named
fields) and refuses any field that is blank/unknown in the en-US source. Use
--missing when filling new locales; use --fields after editing source copy.
The rest of the recipe (Steps 2–6) is identical either way.
If the manifest is empty, nothing needs transcreating — skip Steps 2, 3, 4
(validate), 4a, and 5 (including the refine pass; there's nothing new to refine).
But still run merge.py (the merge command from Step 4) before the gate. With
no new translation outputs it merges nothing, but it still scans the en-US source
for blank fields and clears any existing translated copies of them. Skipping merge
here would leave stale translations in place and cause check_metadata.py to fail
with NOTEMPTY errors in a loop. After merge, go to the gate (Step 6).
2. Compose per-storefront prompts
python3 scripts/translate_metadata/dispatch_prompts.py
Writes one ready-to-dispatch prompt per storefront to
tmp/metadata-prompts/{storefront}.md, with {LOCALE_NAME}, {LOCALE_CODE},
{CULTURAL_NOTE}, {BRAND}, and {SOURCE_JSON} (sliced to that storefront's
gaps, annotated with char limits and per-field guidance) substituted. Also clears
stale output files for those storefronts.
3. Transcreate each storefront's slice
Cross-tool execution. On Claude Code, dispatch one
metadata-localesubagent per storefront in parallel (below). On Cursor or any tool without a subagent primitive, run the same step inline and serially: for eachtmp/metadata-prompts/{storefront}.md, read it, produce the transcreation JSON yourself, and writetmp/metadata-outputs/{storefront}.json— then continue. Same scripts, same gates, same result. Canonical:AGENTS.md > Cross-cutting concerns > Running the translation pipelines.
Claude Code — one metadata-locale subagent per storefront, in parallel
For every tmp/metadata-prompts/{storefront}.md that exists, invoke an Agent:
subagent_type:metadata-locale(defined in.claude/agents/metadata-locale.md, Read+Write only, model opus).- A short dispatch prompt naming the input and output paths. Example:
Read
/abs/path/tmp/metadata-prompts/de-DE.mdand follow the rules in it. Write the resulting JSON object (nothing else) to/abs/path/tmp/metadata-outputs/de-DE.json.
Do not pass subagent_type: general-purpose — the narrow agent is what keeps the
dispatches auto-approvable in .claude/settings.json.
Batch the dispatches, and keep agent calls separate from shell/script calls.
Send the subagent calls concurrently in batches (e.g. ~8–12 per message) rather
than all 49 plus shell commands in one giant message. Never mix Agent calls and
Bash calls in the same message: if one tool call errors (a hygiene-blocked
command, a "nothing to commit", etc.) the whole parallel batch is cancelled,
killing in-flight subagents and wasting their work. Run scripts (extract,
dispatch, validate, merge, audit) in their own single-purpose messages, and keep
each Bash message to one command so one failure can't cascade.
The prompt file already contains every rule (brand, char limits, keywords-as-search, tone/register, JSON-only). Do not modify it in the dispatch message.
4. Validate, then merge
python3 scripts/translate_metadata/validate.py --subset
--subset checks only the fields present in each output file and reports one of
three per-storefront states — your retry dashboard:
- PASS — valid, within limits, ready to merge.
- PENDING — empty/missing output: the subagent hasn't run or produced nothing. Action: (re)dispatch that one storefront. Not an error.
- FAIL — produced content but it's broken. Common causes:
- Over the character limit (especially
name/subtitleat 30, andkeywordsat 100 — the single most common failure) — re-dispatch; the prompt tells the model to tighten until it fits. namemissing the brand prefix — re-dispatch.- Keyword hygiene warnings (spaces after commas, dupes) are warnings, not failures, but re-dispatch if egregious.
- Over the character limit (especially
To retry, re-dispatch only the PENDING/FAIL subagents (the prompt files are
still in tmp/metadata-prompts/). Do NOT re-run dispatch_prompts.py to
retry — by default it clears every manifest locale's output, wiping locales that
already succeeded. (Re-running the full extract→dispatch→merge loop is safe
because merged locales drop out of the next manifest; it's only re-running
dispatch_prompts.py mid-fan-out that's destructive.)
Do not write ad-hoc Python/wc/cat/jq to inspect outputs or count
characters. Everything you need is in two pre-approved tools:
validate.py --subset [--json]— pass/pending/fail + every hard error.audit.py [storefront …]— per-field char counts vs. limits with OVER flags, and the consolidated_questionsbatch (see Step 4a). Add--jsonfor a machine-readable summary,--fullfor untruncated values.
Once validate.py --subset exits 0:
python3 scripts/translate_metadata/merge.py
Writes each field to fastlane/metadata/{storefront}/{field}.txt and copies the
URL files verbatim from en-US. Refuses to overwrite an existing non-empty .txt
with an empty translated value, but always clears any field that is blank in the
en-US source (read directly from the source folder, the same signal the gate
enforces) across all storefronts. The _questions array lives only in the
tmp/metadata-outputs/ JSON — merge.py strips _-prefixed keys, so it never
reaches the metadata tree.
4a. Collect and surface content questions (the one human checkpoint)
The subagents are instructed to work autonomously and only attach a top-level
_questions array when they hit a genuine content decision about the marketing
copy (a concept with no natural equivalent, a claim that's culturally/legally
risky in-market, a load-bearing phrase that can't fit a 30-char field, or
genuinely ambiguous source English). They always still write a best-effort
translation, so the pipeline is never blocked.
Collect them with the pre-approved tool — do not hand-roll this with cat/jq/
python3 -c:
python3 scripts/translate_metadata/audit.py --questions
This prints every _questions entry across all locales (locale, field, issue, and
the subagent's default decision) in one batch, or "No content questions raised" if
there are none. Present that batch to the human in a single consolidated
message (group by issue where the same question recurs across locales) so they
can accept each default or override it. Use AskUserQuestion (or a concise
written summary) — do not dribble out one prompt per locale, and do not
stall the rest of the pipeline waiting on answers: the metadata is already merged
and valid; these questions are about improving specific strings, not unblocking
the run.
If there are no _questions, say so briefly and continue — this is the
expected case. Do not invent questions or ask for approval you don't need.
If the human overrides a default, apply the change by editing the relevant
fastlane/metadata/<storefront>/<field>.txt directly (or re-dispatching that one
locale with the added guidance), then re-run validate.py + check_metadata.py.
5. Semantic quality audit + autonomous remediation (the refine pass)
Merging produces valid copy (within limits, brand-correct); it does not mean the
copy is good. This step grades the transcreations with an Opus auditor and
auto-fixes what it flags — the quality counterpart to the structural audit.py.
Run it autonomously: no prompts, fixed thresholds, bounded rounds.
Scope. Audit the storefronts you transcreated this run — the keys of the
initial tmp/metadata-inputs/manifest.json from Step 1. (On a full re-translation
that's all 49; on an incremental run it's just the touched ones.) If Step 1's
manifest was empty (nothing transcreated), skip this step — there is nothing
new to refine — and go to the gate.
Fixed policy (do not vary per run): auto-remediate every finding at severity
medium or high; low findings are advisory only. Cap at 2 remediation
rounds; residual medium+ findings after round 2 are surfaced, not looped on.
Round protocol:
-
Dispatch audit prompts for the in-scope storefronts:
python3 scripts/translate_metadata/audit_semantic.py --dispatch <storefronts>Writes
tmp/metadata-audit-prompts/{sf}.md(en source + current localized fields + cultural note + limits) and clears their stale audit outputs. -
Fan out one
metadata-audit-locale(Opus) subagent per prompt. For eachtmp/metadata-audit-prompts/{sf}.md, dispatchsubagent_type: metadata-audit-localetelling it to read that file and write findings JSON totmp/metadata-audit-outputs/{sf}.json. Batch ~8–12 per message; never mixAgentandBashcalls in one message (a single tool error cancels the whole batch and kills in-flight subagents). -
Triage:
python3 scripts/translate_metadata/audit_semantic.py --report --min-severity medium <storefronts>Exit 0 → no medium+ findings: the audit is clean, go to the gate (Step 6). Exit 1 → there are findings to fix; continue.
-
Build the remediation manifest from the findings:
python3 scripts/translate_metadata/audit_semantic.py --write-manifest --min-severity medium <storefronts>Writes
tmp/metadata-inputs/{manifest,source}.jsonscoped to exactly the flagged (storefront, field) pairs — the same shape Step 1 produces. -
Regenerate prompts for the flagged set:
python3 scripts/translate_metadata/dispatch_prompts.pyIt reads the remediation manifest and slices each prompt to that storefront's flagged fields only.
-
Re-transcreate with the auditor's feedback. Fan out one
metadata-locale(Opus) subagent per flagged storefront. In each dispatch message, point the agent at both files: its task prompttmp/metadata-prompts/{sf}.mdand the auditor's findingstmp/metadata-audit-outputs/{sf}.json. Instruct it to fix each flagged field per the finding'sissue/suggestion, stay within char limits and all prompt rules, and write the corrected JSON (flagged fields only) totmp/metadata-outputs/{sf}.json. Example:Read
/abs/.../tmp/metadata-prompts/de-DE.md(your transcreation task) and/abs/.../tmp/metadata-audit-outputs/de-DE.json(a prior Opus auditor's findings on the current shipping copy). Produce a corrected transcreation that resolves each finding while obeying every rule in the prompt. Write only the JSON object to/abs/.../tmp/metadata-outputs/de-DE.json.The findings file may include findings for fields not in your prompt's
## Source fieldsblock (the auditor grades the whole listing; this remediation slice is narrower). Act only on findings whosefieldis in your source JSON; silently ignore the rest — they're handled by their own slice or were below the remediation threshold. Do not add a field the source JSON doesn't contain, and don't spend output explaining the skipped findings. -
Validate + merge the fixes:
python3 scripts/translate_metadata/validate.py --subset python3 scripts/translate_metadata/merge.pyRe-dispatch any PENDING/FAIL storefront (Step 4 of the main recipe) before merging.
-
Re-audit only the remediated storefronts (back to round step 1 with just those). If
--report --min-severity medium <remediated>exits 0, the refine pass is done. If findings remain and you have done fewer than 2 rounds, loop. After 2 rounds, stop: print a short residual summary (the remaining medium+ findings, grouped) for owner review and continue to the gate — do not loop indefinitely.
6. Authoritative gate
python3 scripts/translate_metadata/check_metadata.py
This walks fastlane/metadata/ directly (not the tmp/ intermediates), so it
catches anything that didn't merge. If it reports gaps, loop back to step 1
(extract.py --missing will re-flag exactly what's left).
7. Ship (when ready)
Upload metadata only (no binary), or include in a full release:
fastlane push_metadata
# or flip skip_metadata:false in the `release` lane and run `fastlane release`
push_metadata / release touch App Store Connect — only run them when you
actually intend to upload. See fastlane/SETUP.md.
Expected on the first app version: deliver loads every release_notes.txt
but then prints Skipping 'release_notes'... this is the first version of the app and does not upload it — App Store Connect has no "What's New" field until
v1.1. This is normal, not an error; the files upload on the next release. To keep
the source consistent with what's actually live, release_notes.txt is left
blank for the first version (blank en-US source ⇒ blank everywhere, per the gate
rule). Author release notes only from the second version onward.
8. Cleanup — offer to clear tmp working files
After the gate is green (and you've shipped or decided not to), offer to clear this
pipeline's gitignored tmp files (this clears both the transcreation and the
metadata-audit-* working dirs). Ask first; on a yes:
python3 scripts/pipeline_tmp.py clean metadata
When NOT to use this skill
- Editing in-app UI strings → use
translate-new-strings. - Generating App Store screenshot seed content → use
/appstore:generate-screenshot-seeding. - Capturing + uploading App Store screenshots → use
/appstore:generate-push-screenshots.