---ci--- project: atelier phase: 6 milestone: v0.3 status: complete requirements: covered: [ATELIER-60..91] partial: [] ---/ci---
7.8 KiB
Bad Example: i18n String Concatenation
A checkout component that violates Atelier's i18n principles. Each violation is cited, then fixed.
The Code
// Checkout.tsx — the cardinal i18n violation
function CartSummary({ itemCount, name, total, currency, date }) {
// P3 VIOLATION: inline string concatenation. The source-language
// word order ("Welcome, {name}! You have {n} items") is baked into
// code. Every locale with different word order is broken.
const welcome = "Welcome, " + name + "!";
// P4 VIOLATION: hand-rolled plural branching. `if (n === 1)` encodes
// exactly English's one/other rule. Arabic (six categories), Russian
// (three), Polish (three) are all wrong.
const items =
itemCount === 1 ? "1 item" : itemCount + " items";
// P5 VIOLATION: hand-rolled currency + date formatting. "$" + total
// is wrong for de-DE (symbol, grouping, placement). The date
// `getMonth() + 1 + "/" + getDay()` is US-only (mm/dd/yyyy).
const price = "$" + total.toFixed(2);
const d = new Date(date);
const dateStr = (d.getMonth() + 1) + "/" + d.getDate() + "/" + d.getFullYear();
return (
<div>
<h1>{welcome}</h1>
<p>{items} · {price} · {dateStr}</p>
</div>
);
}
// The "resource" file — a custom CSV the team hand-rolled.
// locale,en_us
// welcome_prefix,Welcome,
// item_singular,item
// item_plural,items
//
// This is a P3 violation on its own: a custom format no standard
// tool (xgettext, i18next, FormatJS) can extract from or compose with.
The team runs their first i18n test against real Arabic translations —
after the string freeze, after the translator was paid. The Arabic
build renders "Welcome, محمد!" with the name on the wrong side of
the comma, "1 items" for a single item (Arabic has six plural
categories, not two), and the price as "$1,234.56" (Arabic-Egypt
formats as "١٬٢٣٤٫٥٦ ج.م."). Every screen is a rewrite, not a patch.
What Makes It Bad
Inline String Concatenation (i18n P3 Resources are External, Not Inline)
-
"Welcome, " + name + "!"bakes English word order into code. In Japanese the name comes first (ようこそ、محمدさん!); in Arabic the structure differs again. The concatenation is invisible to the extraction pipeline (xgettext,i18next-parser) — the translator never sees it as a unit, and the string cannot be versioned or rolled back as a whole. -
The custom
.csv"resource" store is a second P3 violation: no standard tool reads it, it carries no plural grammar, and it cannot compose with the ICU formatting layer. -
Fix: strings live in a standard locale resource file, addressed by key. Code calls
t("welcome", { name }); the resource carries the parameterized message.// en-US.json (ICU MessageFormat) { "checkout.welcome": "Welcome, {name}!", "checkout.cart.summary": "{count, plural, one {# item} other {# items}} · {price} · {date}" }// ar-EG.json — six plural categories per CLDR; the code is identical { "checkout.welcome": "أهلاً بك، {name}!", "checkout.cart.summary": "{count, plural, zero {لا عناصر} one {عنصر واحد} two {عنصران} few {# عناصر} many {# عنصرًا} other {# عنصر}} · {price} · {date}" } -
See
domains/i18n/locale-resources.md(Resources are the Boundary) anddomains/i18n/first-principles.mdP3.
Hand-Rolled Plural Branching (i18n P4 Plural and Gender are Parameterized)
-
itemCount === 1 ? "1 item" : itemCount + " items"encodes English's one/other rule and nothing else. Arabic has six categories (zero, one, two, few, many, other); Russian has three (one, few, many); Polish has three with different boundaries. A two-branchifis a C1 (Correctness) violation masquerading as a shortcut — it returns a wrong answer for every non-English locale. -
Fix: the count goes to ICU MessageFormat; the formatter consults
Intl.PluralRulesfor the active locale; the resource carries the variant for that category. The code passes the count, nothing more.// The code passes the count; the resource + formatter pick the form. t("checkout.cart.summary", { count: itemCount, price, date }); // Intl.PluralRules("ar-EG").select(1) === "one" -> "عنصر واحد" // Intl.PluralRules("ar-EG").select(2) === "two" -> "عنصران" // Intl.PluralRules("ar-EG").select(5) === "few" -> "٥ عناصر" -
See
domains/i18n/locale-resources.md(Plural and Gender in Resources) anddomains/i18n/formatting.md(Plural Rules).
Hand-Rolled Currency and Date Formatting (i18n P5 Formatting is Locale-Aware)
-
"$" + total.toFixed(2)hardcodes the US dollar symbol, US grouping (,), and US placement (symbol before the number). Inde-DEthe euro formats as"1.234,56 €"(symbol after, dot grouping). Inar-EGthe pound formats as"١٬٢٣٤٫٥٦ ج.م."(Arabic-Indic digits, different grouping). -
(d.getMonth() + 1) + "/" + d.getDate() + "/" + d.getFullYear()produces11/7/2024— USmm/dd/yyyy. Most of the world readsdd/mm/yyyy; ISO isyyyy-mm-dd. A hand-rolled date formatter encodes one locale's convention and silently produces wrong output for every other. -
Fix:
Intl.NumberFormatandIntl.DateTimeFormatwith a BCP 47 locale tag. CLDR is the source of truth;Intlis the runtime.new Intl.NumberFormat("ar-EG", { style: "currency", currency: "EGP" }) .format(1234.56); // "١٬٢٣٤٫٥٦ ج.م." new Intl.DateTimeFormat("ar-EG", { dateStyle: "medium" }) .format(new Date(date)); // "٧ نوفمبر ٢٠٢٤" -
See
domains/i18n/formatting.md(the Intl surface, dates, numbers, currencies) anddomains/i18n/first-principles.mdP5.
Source Language Treated as the Default (i18n P1 Source Language is a Locale)
- The component has no resource layer at all for the source locale —
English is "just the strings in the code." When the first second
locale arrives, the fix is a rewrite (extract every string,
restructure every concatenation), not a patch. The source language
is
en-US, a locale among many — it is notnull. - Fix: extract source strings into
en-US.jsonfrom day one, even before a second locale exists. The resource layer is the boundary from the first commit. - See
domains/i18n/first-principles.mdP1 anddomains/uiux/copywriting.md.
The Cascade
The violations compound. Inline concatenation makes strings invisible
to the extraction pipeline, so the translator never receives them as
units — they reconstruct them by reading the code. Hand-rolled
plurals return wrong answers for every non-English locale, so the
Arabic build ships "1 items" for a single item. Hand-rolled
formatting produces US-shaped output everywhere, so the price and
date are wrong for de-DE, ar-EG, zh-Hans-CN, and every other
locale. And because the first i18n test ran against real translations
(a P8 violation — pseudo-locales should have surfaced all of this
while the fix was still cheap), the defects are found after the
string freeze, after the translator was paid, and after the release
date was promised. The fix is now a re-translation and a re-release,
not a commit.
Cross-Domain Links
domains/i18n/locale-resources.md— the resource layer this code lacks; the standard formats (.po, JSON, Fluent, ICU Resource Bundle) it should have used.domains/i18n/formatting.md— theIntl/ICU/CLDR formatting this code should call instead of hand-rolling.domains/i18n/first-principles.md— P3, P4, P5, and P8 (pseudo- locales test early).domains/uiux/copywriting.md— copy lives in resources, not in code.domains/api/error-responses.md— the same parameterized-message discipline applies to localized API errors.