Hardcoded UI strings are scattered across JSX text, attributes, toasts, and validation messages — thousands of them, each needing a key that follows your naming convention. AI does the sweep and the wrapping; you own the convention, the pluralization hazards it flags, and the gate that stops new literals from creeping back in.
Tier: Core · Workflow area: Styling, responsive & i18n
An i18n externalization done by brute force takes weeks and produces keys like text_47. Done as an agent sweep against a written convention, it takes days and produces a translations file a localization team can actually work with.
t(), half is hardcoded, and nobody knows which half.namespace.component.purpose → billing.invoiceCard.dueDateLabel, camelCase leaves, no abbreviations.Inventory before wrapping. Extraction without an inventory is unauditable. First pass is read-only:
Sweep src/features/billing/ for user-visible hardcoded strings. Look
beyond JSX text nodes: placeholder, title, alt, aria-label and
aria-description attributes, toast/notification calls, validation and
error messages (including ones built in non-component files like
schemas and utils), document.title / meta calls, and strings in
constants files that end up rendered. EXCLUDE: log messages, error
codes, test files, analytics event names, CSS values, URLs.
Output inventory.md: file:line | string | context (attribute/jsx/
toast/validation) | proposed key per the convention in
docs/i18n-convention.md | hazard flags (see below).
Hazard flags: PLURAL (count-dependent), INTERP (contains a variable),
CONCAT (built by string concatenation or ternary inside a sentence),
GENDER/CASE (words that inflect), RICH (contains markup/links).
Skim the inventory for scope errors — agents love to externalize console.error messages and enum values. Delete bad rows now; each one you miss becomes a translated log line you pay for.
Review keys as a batch, hazards as individuals. Key names are cheap to fix in the table and expensive to fix after translators have started. Check thirty random rows against the convention. Then read every hazard row — they are the ones where mechanical wrapping produces untranslatable output.
Wrap the clean rows mechanically. Point the agent at the exemplar and the approved inventory:
Using src/features/account/ProfileForm.tsx as the exemplar of our
useTranslation pattern, externalize every non-hazard row in
inventory.md: wrap with t('<key>'), add the key/string pair to the
correct namespace JSON keeping alphabetical order, and add the
useTranslation import where missing. Preserve exact string content
including punctuation and casing. One commit per feature directory.
Diff-review a sample per commit: the classic silent error is trimmed whitespace or a "smart-quoted" apostrophe changing rendered copy.
Handle hazards with the framework's semantics, not string surgery. This is the judgment lane. PLURAL: "You have 3 items" becomes an ICU plural — {count, plural, one {# item} other {# items}} — never two keys glued by an if; other languages have up to six plural forms and only ICU (or i18next's _one/_other suffixes) can express them. CONCAT: sentences assembled from fragments ("Deleted " + name + " successfully") must become one interpolated key (t('billing.deleteSuccess', { name })) because word order differs across languages. RICH: links and bold inside sentences use <Trans> (i18next) or rich-text formatters (FormatJS) so the sentence stays one translatable unit. Have the model propose each conversion, but read every one — this is where meaning breaks.
Verify by pseudo-locale, not by faith. Generate a pseudo-translation of the new files (accented, bracketed, 40% longer: [Ïñvöïçé đüé đàté!!]) and run the app in it. Two bug classes fall out immediately: strings still hardcoded (they render un-bracketed — screenshot and fix), and layouts that break under expansion (file for the responsive backlog). An agent driving the browser can do this screenshot pass across your route list in minutes.
Close the door with lint. Add eslint-plugin-i18next (i18next/no-literal-string) scoped to JSX and the attribute list from step 1, as a warning for a week and then an error. Without the gate, the literal count starts climbing again the day the sweep PR merges.
Never let the sweep "improve" copy while externalizing. A model that fixes a typo or rewords a label mid-sweep breaks the review contract — the diff can no longer be trusted as behavior-neutral. Copy fixes are a separate PR with a content owner's sign-off.
<Trans> vs ICU MessageFormat + defineMessages). FormatJS's formatjs extract CLI can also cross-check your final key set against usage.stringResource(R.string.billing_due_date) + plurals.xml) or SwiftUI (String Catalogs with automatic plural variation); hazards and review gates carry over unchanged.The externalization sweep is 90% mechanical and 10% linguistic, and the entire risk lives in the 10%. Let AI do the wrapping at scale, but route every PLURAL, CONCAT, and RICH flag through a human who understands that translation is not string replacement — that routing is what the inventory's hazard column is for.