Frontend i18n skill for Codex

Plurals, RTL and locale URLs done properly. Codex localizes a frontend without the classic mistakes: whole-sentence ICU messages instead of concatenation, plural categories that work for Polish and Arabic, Intl formatters with explicit time zones so SSR matches the client, logical CSS and bdi for RTL, and locale-prefixed URLs with hreflang. A pseudo-locale pass then proves no hard-coded strings were missed.

  • Frontend
  • $frontend-i18n
  • 158 lines
  • 2 files
  • ~1,950 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- frontend-i18n

Installs to ~/.agents/skills/frontend-i18n/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $frontend-i18n, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Internationalizes web frontends with ICU MessageFormat (plural, select, selectordinal), Intl APIs for numbers, dates, lists and relative time, RTL layouts with dir and CSS logical properties, locale-prefixed routing with hreflang, message extraction and pseudo-localization, using react-intl/FormatJS, next-intl, i18next, vue-i18n or Lingui. Use when a user asks to translate or localize an app, add a language or Arabic/Hebrew RTL support, fix plurals or concatenated strings, format currency and dates per locale, or set up /en and /fr routes.

Use it when

  • An app is being translated or a new language, especially RTL, is added
  • Strings are concatenated or plurals are handled with ternaries
  • Dates, numbers or currency render inconsistently across locales or between server and client
  • Localized pages need their own URLs and hreflang

Use something else when

How to check the result

Run a pseudo-locale build to catch hard-coded strings, switch to an RTL locale to check mirroring, test plurals with 0, 1, 2, 5 and 21 in Polish and Arabic, and inspect hreflang in page source.

The mistake it prevents

Building sentences from translated fragments or deciding plurals with count === 1, both of which break in most non-English languages.

~/.agents/skills/frontend-i18n/SKILL.md

--- name: frontend-i18n description: Internationalizes web frontends with ICU MessageFormat (plural, select, selectordinal), Intl APIs for numbers, dates, lists and relative time, RTL… license: MIT ---

Done well means: no user-visible string is hard-coded or concatenated, every count uses a plural-aware message, numbers and dates go through Intl, the layout mirrors correctly in RTL, each locale has its own crawlable URL, and a pseudo-locale run shows nothing was missed.

Before changing anything

  1. Read AGENTS.md, package.json and any existing locales/, messages/ or i18n config. If a library is already in use, stay with it.
  2. Find the framework and rendering model (SPA, SSR, RSC). It decides where messages load and where the locale is resolved.
  3. Ask (or find in docs) the target locales, the default, and whether URLs should carry the locale. Adding a library needs network; ask for approval in a sandbox.

Library choice (new projects)

StackDefaultNotes
Next.js App Routernext-intlRSC-aware, ICU syntax, locale routing helpers
React SPA / Vitereact-intl (FormatJS)ICU native, extraction CLI, precompiled messages
Existing i18next codebasei18next + react-i18nextAdd i18next-icu for ICU syntax, or use its _one/_other key plurals consistently
Vue / Nuxtvue-i18n / @nuxtjs/i18nOwn message syntax; pipe-separated plurals
Minimal bundle, compile-timeLingui or ParaglideMessages compiled to functions, tree-shaken per locale

Message rules

{
  "cart.summary": "{count, plural, =0 {Your cart is empty} one {# item in your cart} other {# items in your cart}}",
  "invite.sent": "{gender, select, female {She invited you} male {He invited you} other {They invited you}} to {project}",
  "ranking": "You finished {place, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}",
  "terms": "By continuing you accept the <link>terms of service</link>."
}
<FormattedMessage id="terms" values={{ link: (chunks) => <a href="/terms">{chunks}</a> }} />
  • One message per complete sentence. Never build sentences from fragments (t("you_have") + n + t("items")); word order changes between languages.
  • Always other in every plural and select. Languages need different categories: English uses one/other, Polish one/few/many/other, Arabic all six (zero/one/two/few/many/other). Translators add categories; source code must not assume two.
  • Use # inside plural branches so the number is locale-formatted.
  • Rich text via tags in the message (<link>…</link>), never HTML strings with dangerouslySetInnerHTML.
  • Give each message a description for translators (description in FormatJS, comments in JSON/PO) with context: where it appears, max length, what placeholders mean.
  • Keys: stable, semantic (checkout.payment.error.declined), not the English text.

Intl formatting

const fmt = (locale: string) => ({
  money: (v: number, currency: string) =>
    new Intl.NumberFormat(locale, { style: "currency", currency }).format(v),
  compact: (v: number) => new Intl.NumberFormat(locale, { notation: "compact" }).format(v),
  date: (d: Date, timeZone: string) =>
    new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeZone }).format(d),
  relative: (days: number) =>
    new Intl.RelativeTimeFormat(locale, { numeric: "auto" }).format(days, "day"),
  list: (items: string[]) =>
    new Intl.ListFormat(locale, { type: "conjunction" }).format(items),
});
  • Currency is data, not locale: a French user can see a USD price. Store the currency code with the amount.
  • Pass an explicit timeZone when rendering on the server, or server and client output differ and cause hydration mismatches. Use the user’s zone from a cookie or profile.
  • Cache formatter instances; constructing Intl.* objects in a hot render loop is slow.
  • Sort user-visible lists with Intl.Collator(locale).compare, not localeCompare without a locale or plain <.
  • Truncate by grapheme (Intl.Segmenter) not slice, or emoji and combined characters break.

RTL

<html lang="ar" dir="rtl">
  • Set lang and dir on <html> from the active locale (server-rendered, not after hydration).
  • Use logical CSS: margin-inline-start, padding-inline, inset-inline-end, text-align: start, border-start-start-radius. Tailwind: ms-*, me-*, ps-*, start-*, rtl: variant.
  • Mirror directional icons (arrows, back, progress, send) with [dir="rtl"] .icon-directional { transform: scaleX(-1) }; do not mirror clocks, checkmarks, media play buttons or logos.
  • User-generated text: dir="auto" on inputs and display containers; wrap embedded opposite- direction values (names, numbers, usernames) in <bdi>.
  • Carousels, sliders and swipe gestures reverse direction in RTL.

Locale routing

StrategyUse when
Path prefix /fr/pricing (default as-needed or always)Default for public, SEO-relevant sites
Subdomain or ccTLD fr.example.comSeparate markets, separate teams or hosting
Cookie/user setting only, same URLLogged-in apps where SEO does not matter
  • Detect from Accept-Language only on first visit to redirect, then respect the user’s explicit choice (cookie). Never auto-redirect search engine crawlers away from a URL.
  • Every locale page lists alternates: <link rel="alternate" hreflang="fr" href="…/fr/pricing"> for each locale plus hreflang="x-default", and a self-referencing canonical.
  • Next.js: locale detection lives in middleware.ts (renamed proxy.ts in Next.js 16) with a [locale] route segment; follow the installed library’s routing docs.
  • Translate slugs only if you can maintain redirects for them.

Loading messages

  • Load only the active locale, split by route or namespace for large apps. In RSC setups, keep messages on the server and pass only what client components need.
  • Fallback chain: pt-BR → pt → default locale. Log missing keys in development; never show raw keys in production.
  • Precompile ICU messages (FormatJS compile, Lingui compile) to drop the parser from the bundle.

Extraction and QA

npx formatjs extract "src/**/*.{ts,tsx}" --out-file lang/en.json --id-interpolation-pattern "[sha512:contenthash:base64:6]"
npx formatjs compile lang/fr.json --ast --out-file compiled-lang/fr.json

(Use the equivalent for i18next or Lingui.) Then:

  • Pseudo-localize (accented, expanded ~35%, bracketed: [Ŝàvé çĥàñĝéš·····]). Any plain English text left is a hard-coded string; clipped brackets show layout that cannot expand.
  • Diff key sets between locales in CI so missing translations fail or warn.
  • German and Finnish run ~30% longer than English; check buttons, tabs and table headers.

Gotchas

  • toLocaleString() with no locale argument uses the server’s locale in SSR.
  • Plural logic in code (count === 1 ? … : …) is wrong for most languages; delete it.
  • Gendered or case-inflected languages need whole-message variants, not swapped nouns.
  • Images with embedded text, and dates in alt text, need localization too.
  • Changing the locale client-side must also update <html lang dir> and document.title.
  • Machine translation output must be reviewed by a fluent speaker before release; mark machine-translated files as such.

Verification

  • Pseudo-locale build shows no untranslated strings on key pages.
  • Switch to an RTL locale: layout, icons and focus order mirror; nothing overlaps.
  • Plural messages tested with 0, 1, 2, 5, 21 and 1.5 in English, Polish and Arabic.
  • View source of a locale page: correct lang, dir, canonical and hreflang set.
  • Server and client render the same date strings (no hydration warning).

Output

Report: library and config, locales added, message files and key count, routing strategy, RTL changes, extraction/CI commands, and strings needing translator or legal review.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Frontend i18n"
  short_description: "Plurals, RTL and locale URLs done properly"
  default_prompt: "$frontend-i18n Add French and Arabic to this Next.js app with /fr and /ar routes"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem