# Bad Example: i18n String Concatenation
> A checkout component that violates Atelier's i18n principles. Each
> violation is cited, then fixed.
## The Code
```typescript
// 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 (
{welcome}
{items} · {price} · {dateStr}
);
}
```
```typescript
// 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.
```json
// en-US.json (ICU MessageFormat)
{
"checkout.welcome": "Welcome, {name}!",
"checkout.cart.summary": "{count, plural, one {# item} other {# items}} · {price} · {date}"
}
```
```json
// 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)
and `domains/i18n/first-principles.md` P3.
### 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-branch `if` is 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.PluralRules` for the active locale; the resource
carries the variant for that category. The code passes the count,
nothing more.
```typescript
// 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) and `domains/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). In
`de-DE` the euro formats as `"1.234,56 €"` (symbol after, dot
grouping). In `ar-EG` the pound formats as `"١٬٢٣٤٫٥٦ ج.م."`
(Arabic-Indic digits, different grouping).
- `(d.getMonth() + 1) + "/" + d.getDate() + "/" + d.getFullYear()`
produces `11/7/2024` — US `mm/dd/yyyy`. Most of the world reads
`dd/mm/yyyy`; ISO is `yyyy-mm-dd`. A hand-rolled date formatter
encodes one locale's convention and silently produces wrong output
for every other.
- **Fix:** `Intl.NumberFormat` and `Intl.DateTimeFormat` with a BCP
47 locale tag. CLDR is the source of truth; `Intl` is the runtime.
```typescript
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) and `domains/i18n/first-principles.md` P5.
### 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 not `null`.
- **Fix:** extract source strings into `en-US.json` from day one,
even before a second locale exists. The resource layer is the
boundary from the first commit.
- See `domains/i18n/first-principles.md` P1 and
`domains/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` — the `Intl`/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.