CLI / Data-Driven Content

Data-Driven Content

Most apps keep a lot of user-facing copy in data — feature lists, pricing tables, persona pages, nav config. Polyglot internationalizes that copy too, and it does it safely: it never ships code that breaks, and it tells you exactly what it couldn't do.

Two kinds of strings

polyglot wrap handles two very different cases:

  • JSX text (<h1>Welcome</h1>) — the string and where it renders are the same place, so it's wrapped in place with {t("…")}.
  • Data-object strings ( const plan = { blurb: "…" }) — the string lives in a data structure and is rendered somewhere else entirely. You can't call a translation hook from module scope, so a different transform is needed.

The pattern: keys in data, strings in catalog

This is the canonical approach (used by react-intl, next-intl, i18next, Lingui): the data holds keys, the catalog holds the strings, and the component resolves them at render time. Polyglot applies it for you.

Before:

// personas.ts
export const engineers = {
  blurb: "i18n that lives in your terminal.",
};

// a page
<p>{engineers.blurb}</p>

After:

// personas.ts — the value is now a catalog key
export const engineers = {
  blurb: "lib.personas.engineers.blurb",
};

// messages/en.json
{ "lib": { "personas": { "engineers": { "blurb": "i18n that lives in your terminal." } } } }

// the page — resolved at render time
const t = useTranslations();
<p>{t(engineers.blurb)}</p>

What Polyglot resolves automatically

Polyglot statically traces where your data is consumed and wraps the render sites it can prove are correct — across files:

  • Direct references{data.field}, including imported data from another module.
  • Iterationitems.map((i) => i.title).
  • Prop-drilling — when typed data flows into a component (<Card item={item} /> where the prop is annotated with the data's type), Polyglot follows the type and wraps the consumers inside that component.

The safety guarantee

Finding every consumer of a value in a dynamic language is undecidable in general — so Polyglot is built to never guess wrong:

  • A data value is rewritten to a key only if every consumer of it is provably wrapped. If any use escapes static analysis, the whole value is left untouched.
  • Every file is re-parsed before it's written — if a transform would produce invalid syntax, the file is skipped, not shipped.
  • wrap is idempotent — running it twice is a no-op, so it's safe in CI and on repeat runs.

Every run ends with a one-line summary, e.g. ✓ 1104 strings auto-wrapped · 15 flagged for manual review · 0 unsafe transforms . The 0 unsafe is a guarantee, not a measurement.

The report

Anything Polyglot can't safely wrap is written to polyglot-i18n-report.md (and .json) at your project root — grouped by category, with the file, line, reason, and a suggested fix. It's your manual-i18n worklist. (Add it to .gitignore, or commit it as a tracked TODO — your call.)

Wrapping the residual by hand

The flagged cases are the genuinely-ambiguous ones. Each maps to a small manual fix:

  • Function indirection ( getPersona(slug).blurb) — the value already carries its full key, so just wrap at the render site: {t(persona.blurb)} with a root useTranslations().
  • Dynamic access (data[expr]) — wrap the expression where it's rendered, the same way.
  • Unresolved prop type — annotate the receiving component prop with the data's type (e.g. { item }: { item: Feature }) so Polyglot can drill it on the next run, or wrap the consumer manually.
  • Interpolation / same-line — strings with {variables} need a hand-written t() call with the interpolation values.

Best practice: author new data as keys

The cleanest long-term answer is to never put raw strings in data in the first place. For content you write going forward, store the message key directly and resolve it at render time:

// data
export const features = [
  { key: "features.fast.title" },
  { key: "features.safe.title" },
];

// catalog (messages/en.json)
{ "features": { "fast": { "title": "Fast" }, "safe": { "title": "Safe" } } }

// component
const t = useTranslations();
features.map((f) => <h3 key={f.key}>{t(f.key)}</h3>);

This sidesteps the analysis entirely — there's nothing to trace, and every new string is translatable by construction.

Review shared repairs as a complete plan

Use polyglot wrap --plan --file <path>, inspect wrap --show <plan-id>, then apply the same plan ID. A discovered object may still have unproven consumers; never treat all title/label properties as display-only. Count affected candidates separately from shared repair groups.

The Needs Review workflow keeps the blocker, proof condition and reasoned local decisions close to the source. After a repair, save and recheck with a fresh plan, then exercise the changed screen.

Data-Driven Content - Docs | Polyglot