BXUP Track progress & test yourself — free
Frontend & Mobile · Playbook · Core · ~4 min read
FRONTEND & MOBILE · PLAYBOOK · CORE

Externalize i18n Strings Repo-Wide with Convention-Following Keys

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.

USE THIS WHEN

The sweep is the job

  • The product is going international and the codebase was built English-only.
  • i18n exists but coverage is partial — half the app uses t(), half is hardcoded, and nobody knows which half.
  • You are consolidating three ad-hoc translation files into one convention.
SKIP IT WHEN

The sweep misfires

  • No i18n framework is chosen yet — pick i18next or FormatJS first; the sweep needs a target API, not a maybe.
  • The strings are mostly CMS- or server-supplied — externalize at the source instead; wrapping already-dynamic text buys nothing.
  • A design/content overhaul will rewrite the copy next month — sweep after, or you will pay translators twice.

Prerequisites

  • The framework installed and one component hand-migrated end-to-end (the exemplar the agent will imitate).
  • A written key convention — one paragraph is enough, but it must exist: e.g. namespace.component.purpose → billing.invoiceCard.dueDateLabel, camelCase leaves, no abbreviations.
  • Namespace/file layout decided (per-feature JSON vs one big file), and ICU/plural style settled.

The recipe

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

Warning

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.

Tool variations

  • Claude Code: best for the full loop — inventory, batch wrapping, pseudo-locale screenshot pass — with commits per feature as checkpoints.
  • Cursor: strong per-directory wrapping with inline review; paste the convention doc and exemplar into context every session.
  • i18next vs FormatJS: the recipe is identical; only step 4's syntax changes (suffix plurals + <Trans> vs ICU MessageFormat + defineMessages). FormatJS's formatjs extract CLI can also cross-check your final key set against usage.
  • Mobile: the same sweep against Compose (stringResource(R.string.billing_due_date) + plurals.xml) or SwiftUI (String Catalogs with automatic plural variation); hazards and review gates carry over unchanged.

Quality check

  • Inventory is the source of truthevery wrapped string traces to an approved row; scope exclusions (logs, enums, analytics) held.
  • Convention held under samplinga 30-row random audit of final keys shows zero convention violations.
  • Hazards resolved semanticallyno plural expressed as an if/else, no sentence assembled by concatenation, no link split out of its sentence.
  • Pseudo-locale run is cleanzero un-bracketed strings on the audited routes; expansion breakages filed.
  • Lint gate mergednew literal strings fail CI, with a documented exemption comment for the rare legitimate case.
Key Takeaway:

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.

This is the free reading view. Open BXUP to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.
© Anvi Innovate · BXUP — Be Exponentially Upskilled · BXUP is an independent learning platform, not affiliated with or endorsed by AWS, Microsoft, Google, NVIDIA, IBM, Salesforce, Databricks, Oracle or Anthropic. All trademarks belong to their respective owners. · bxup.in