Give the agent the i18n message IDs, not hardcoded English to “fill in later”
The modal works. The CTA says "Save changes". The agent left a comment: // TODO: i18n. There is already a saveChanges id in en.json, and the German file will now drift for a week because nobody wired it.
I’ve started treating hardcoded user-facing English in a localized app as a defect in the PR, not a follow-up. Agents default to literals because that’s the fastest way to make the screenshot look done.
Why “fill in later” never gets filled in
- The TODO is in a file reviewers skim
- The string is almost, but not quite, the existing catalog entry (
SavevsSave changes) - Plurals, ICU, or interpolation (
{count} items selected) get faked with' + n + ' - A second screen copies the literal, and now you have two source-of-truth problems
If the product is already on react-intl, next-intl, vue-i18n, or i18next, the catalog is the contract. The agent should work inside it.
What to hand the agent
Don’t say “use i18n.” Paste the relevant slice:
{ "checkout.payCta": "Pay now", "checkout.payCtaPending": "Paying…", "checkout.itemsSelected": "{count, plural, one {# item selected} other {# items selected}}" } And the hook you actually use:
Use t('checkout.payCta') / the existing useTranslations('checkout') pattern. Do not add hardcoded English for user-visible strings. If you need a new message, add the id to the English catalog in this PR with the same naming prefix, and use ICU for counts. Do not invent a second i18n helper. If you have a typings file or an id enum, paste that instead of the whole locale. The id list is more useful than a style-guide paragraph.
A simple policy
- User-visible strings in a localized surface go through the catalog in the same PR
- Prefer an existing id over a near-duplicate new one
- New ids follow the existing namespace (
screen.elementState), notstring1 - Interpolation and plurals use the project’s ICU / sprintf style, not string concat
- Aria labels and error text count as user-visible
- Developer-only log lines can stay as literals unless you already localize those too
Extract-and-upload pipelines (some TMS setups) can wait for a bot to harvest English — but only if that’s already how your repo works. Don’t assume the agent knows that. If harvest is the rule, say so; if the English file is edited in the PR, say that instead.
Review checklist
-
git difffor quotes in TSX/JSX/Vue templates — new literals in UI paths should be rare - New ids should appear in the catalog, not only in the component
- Check plural / interpolation forms against an existing example in the same namespace
- Search the catalog for near-duplicate English before accepting a new key
A lint rule (@formatjs/no-literal-string-in-jsx, eslint-plugin-i18next, or your equivalent) is the durable version of this review. Until that’s on, the prompt and the diff still matter.
Reject / send back
Reject
-
"Save changes"in the component withTODO: i18n - A new helper like
const t = (s) => s“for now” - Concatenated sentences that can’t be translated (
'Hello, ' + name) - A new id that duplicates
checkout.payCtawith slightly different wording
Accept
- Existing ids, used the way neighboring components already use them
- New catalog entries in the same PR, named consistently, with ICU where counts exist
- A short PR note when a string is intentionally developer-only
Tiny habit
When you ask an agent to add a button or an empty state, paste the message ids (or the namespace) up front. Don’t ask it to “write the copy” in English and promise a localization pass later. Later is how catalogs rot.