# I18n L10n Patterns

> Use when building or reviewing internationalized applications — covers message externalization, ICU MessageFormat, CLDR plural rules, Intl.DateTimeFormat/NumberFormat, RTL layout with CSS logical properties, Intl.Collator for locale-correct sorting, translation workflow, pseudo-localization testing, and i18n anti-patterns across TypeScript and React

- **Type:** Skill
- **Install:** `agentstack add skill-mickeyyaya-refactoring-skills-i18n-l10n-patterns`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [mickeyyaya](https://agentstack.voostack.com/s/mickeyyaya)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [mickeyyaya](https://github.com/mickeyyaya)
- **Source:** https://github.com/mickeyyaya/refactoring-skills/tree/main/skills/i18n-l10n-patterns

## Install

```sh
agentstack add skill-mickeyyaya-refactoring-skills-i18n-l10n-patterns
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Internationalization and Localization Patterns

## Overview

Hardcoded strings, binary plural logic, and locale-less date formatting create invisible bugs that only surface when users switch languages or regions. Use this guide when building new features that display user-visible text, numbers, dates, or currency — and during code review to catch i18n defects before they ship.

**When to use:** Any feature that renders text, numbers, dates, times, or currency to users; reviewing components for hardcoded strings; evaluating plural and date formatting logic; adding RTL language support; designing translation workflows.

## Quick Reference

| Topic | Core Idea | Primary Red Flag |
|-------|-----------|-----------------|
| Message Externalization | All user-visible strings in resource bundles or JSON | String literals in JSX/templates |
| ICU MessageFormat | Rich interpolation, plurals, selects in one syntax | String concatenation to build sentences |
| CLDR Plural Rules | Six categories: zero/one/two/few/many/other | `count === 1 ? "item" : "items"` binary check |
| Intl.DateTimeFormat | Locale-aware date/time via native API | `toLocaleDateString()` without explicit locale |
| Intl.NumberFormat | Locale-aware numbers and currency | Manual decimal/comma formatting |
| RTL Layout | CSS logical properties + `dir` attribute | `margin-left`, `padding-right` hard-coding direction |
| Intl.Collator | Locale-correct string sorting | `Array.sort()` with default JS comparator |
| Translation Workflow | Key-based lookups, context annotations, interpolation | Source strings directly in JSX |
| i18n Testing | Pseudo-localization, string expansion, missing key detection | No i18n-specific test coverage |

---

## Patterns in Detail

### 1. Message Externalization

Store every user-visible string outside source code in resource bundles (`.properties`, `.po`) or structured JSON translation files. Business logic must never contain literal display strings.

**Red Flags:**
- String literals inside JSX: `Welcome back!`
- Template literals building UI text: `` `Hello ${name}, you have ${n} messages` ``
- Error messages hardcoded in API responses
- Strings mixed with logic: `label = isAdmin ? "Admin Panel" : "My Dashboard"`

**TypeScript/React — JSON translation file (`en.json`):**
```json
{
  "nav.dashboard": "My Dashboard",
  "nav.admin": "Admin Panel",
  "welcome.greeting": "Welcome back, {name}!",
  "cart.items": "{count, plural, one {# item} other {# items}} in your cart"
}
```

**React component using `react-intl` — replace hardcoded JSX strings with `` or `intl.formatMessage({ id: ... }, values)`.**

**Resource bundle pattern (Node.js / `i18next`):**
```typescript
import i18next from 'i18next';

await i18next.init({
  lng: 'en',
  fallbackLng: 'en',
  resources: {
    en: { translation: require('./locales/en.json') },
    de: { translation: require('./locales/de.json') },
    ar: { translation: require('./locales/ar.json') },
  },
});

// Use in service layer — never hardcode the string here
const message = i18next.t('welcome.greeting', { name: user.displayName });
```

Cross-reference: `code-documentation-patterns` — annotating translation keys with context comments for translators.

---

### 2. ICU MessageFormat and Interpolation

ICU MessageFormat is the standard syntax for rich message patterns: variable interpolation, plural selection, gender select, and date/number formatting all within a single message string. This eliminates string concatenation that breaks translation.

**Red Flags:**
- Concatenated sentence fragments: `"You have " + count + " new messages"`
- Separate translation keys for singular/plural: `msg.singular` / `msg.plural`
- Gender/select logic in component code instead of message string
- Positional placeholders (`{0}`, `%s`) that cannot be reordered by translators

**ICU MessageFormat syntax reference:**
```
Simple variable:   "Hello, {name}!"
Plural:            "{count, plural, =0 {No items} one {# item} other {# items}}"
Select:            "{gender, select, male {He} female {She} other {They}} replied."
Date:              "Joined {date, date, medium}"
Number:            "Balance: {amount, number, ::currency/USD}"
Nested:            "{count, plural, one {{name} sent a message} other {{name} sent # messages}}"
```

**TypeScript with `@formatjs/intl`:**
```typescript
import { createIntl, createIntlCache } from '@formatjs/intl';

const cache = createIntlCache();
const intl = createIntl({ locale: 'en-US', messages: enMessages }, cache);

// Handles plurals, interpolation, and locale in one call
const text = intl.formatMessage(
  { id: 'cart.items' },
  { count: cartItems.length }
);
// en-US: "3 items in your cart"
// de-DE: "3 Artikel in Ihrem Warenkorb"
```

// In React: `` — translation handles select logic, no JSX branching needed.

---

### 3. CLDR Plural Rules

The Unicode CLDR defines six plural categories — `zero`, `one`, `two`, `few`, `many`, `other` — that vary by language. English only uses `one` and `other`, but Arabic uses all six. Binary `count === 1` checks are always wrong for non-English locales.

**Red Flags:**
- `count === 1 ? 'item' : 'items'` — binary plural, fails for Arabic, Slavic, and other languages
- Only two translation keys when six CLDR plural categories exist
- Missing `zero` form for languages where zero has distinct grammar (Arabic, Welsh)
- Client-side plural logic in JavaScript instead of ICU MessageFormat

**Binary plural anti-pattern:**
```typescript
// WRONG — only works for English; breaks Arabic, Polish, Russian
function getItemLabel(count: number): string {
  return count === 1 ? 'item' : 'items';
}
```

**Correct approach — ICU plural in translation string:**
```json
{
  "en": {
    "search.results": "{count, plural, =0 {No results} one {# result} other {# results}}"
  },
  "ar": {
    "search.results": "{count, plural, zero {لا نتائج} one {نتيجة واحدة} two {نتيجتان} few {# نتائج} many {# نتيجة} other {# نتيجة}}"
  },
  "pl": {
    "search.results": "{count, plural, one {# wynik} few {# wyniki} many {# wyników} other {# wyników}}"
  }
}
```

// In React: `` — the intl library resolves the correct CLDR plural category for the active locale automatically.

Cross-reference: `data-validation-schema-patterns` — validating locale codes against CLDR language tag schemas.

---

### 4. Date and Time Formatting

Always use `Intl.DateTimeFormat` with an explicit locale and timezone. Never rely on `Date.toString()`, `toLocaleDateString()` without locale, or manual format strings — these produce inconsistent output across environments.

**Red Flags:**
- `date.toLocaleDateString()` without locale — uses system locale, not user preference
- `date.toISOString().split('T')[0]` for display — always renders as `YYYY-MM-DD` regardless of locale
- Hardcoded format string: `${month}/${day}/${year}` — month/day order varies by locale
- Missing timezone: `new Intl.DateTimeFormat('en-US')` — uses local timezone, not user's timezone

**TypeScript date formatting utilities:**
```typescript
// BEFORE — locale and timezone ignored
// return date.toLocaleDateString();

// AFTER — explicit locale and timezone
function formatDate(date: Date, locale: string, timeZone: string): string {
  return new Intl.DateTimeFormat(locale, {
    year: 'numeric', month: 'long', day: 'numeric', timeZone,
  }).format(date);
}

function formatDateTime(date: Date, locale: string, timeZone: string): string {
  return new Intl.DateTimeFormat(locale, {
    dateStyle: 'medium', timeStyle: 'short', timeZone,
  }).format(date);
}

formatDate(new Date('2024-03-15'), 'en-US', 'America/New_York');  // "March 15, 2024"
formatDate(new Date('2024-03-15'), 'de-DE', 'Europe/Berlin');     // "15. März 2024"
formatDate(new Date('2024-03-15'), 'ja-JP', 'Asia/Tokyo');        // "2024年3月15日"
```

// In React: wrap `Intl.DateTimeFormat(ctx.locale, { ..., timeZone: ctx.timeZone }).format(date)` in a `` element for machine-readable output.

**Relative time with `Intl.RelativeTimeFormat`:**
```typescript
function formatRelativeTime(diffMs: number, locale: string): string {
  const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
  const diffSeconds = Math.round(diffMs / 1000);
  if (Math.abs(diffSeconds) ` or component roots
- Icons or chevrons not mirrored for RTL (back arrow becomes forward arrow)
- `text-align: left` hardcoded instead of `text-align: start`
- Absolutely positioned elements using `left`/`right` instead of `inset-inline-start`/`inset-inline-end`

**CSS logical properties:**
```css
/* BEFORE — physical properties, break in RTL */
.card {
  padding-left: 16px;
  padding-right: 16px;
  border-left: 4px solid var(--accent);
  margin-left: auto;
  text-align: left;
}

/* AFTER — logical properties, correct in both LTR and RTL */
.card {
  padding-inline-start: 16px;
  padding-inline-end: 16px;
  border-inline-start: 4px solid var(--accent);
  margin-inline-start: auto;
  text-align: start;
}
```

**React — setting `dir` attribute from locale:**
```tsx
const RTL_LOCALES = new Set(['ar', 'he', 'fa', 'ur']);

function getTextDirection(locale: string): 'ltr' | 'rtl' {
  const lang = locale.split('-')[0];
  return RTL_LOCALES.has(lang) ? 'rtl' : 'ltr';
}

function App({ locale }: { locale: string }) {
  const dir = getTextDirection(locale);
  return (
    
      {/* content */}
    
  );
}
```

**Mirroring icons conditionally:**
```tsx
function BackButton() {
  const { dir } = useTextDirection();
  return (
    
      
      
    
  );
}
```

**Tailwind CSS logical property utilities (v3.3+):**
```html

...

...
```

---

### 7. String Collation and Locale-Correct Sorting

JavaScript's default `Array.sort()` uses Unicode code point order, which is incorrect for most locales. Use `Intl.Collator` for locale-aware sorting — especially for names, product lists, and search results.

**Red Flags:**
- `items.sort()` or `items.sort((a, b) => a.localeCompare(b))` without explicit locale
- `localeCompare()` without collator options — ignores sensitivity and numeric sorting
- Sorting names server-side with `ORDER BY name` without locale-aware collation
- Treating sort order as stable across locales (Swedish `å` sorts after `z`, not after `a`)

**TypeScript collation utilities:**
```typescript
// BEFORE — code point order, wrong for most languages
// const sorted = names.sort();

// AFTER — locale-correct collation
function sortByName(items: string[], locale: string): string[] {
  const collator = new Intl.Collator(locale, {
    sensitivity: 'base',  // ignore case and diacritics for equality
    ignorePunctuation: true,
  });
  return [...items].sort((a, b) => collator.compare(a, b)); // immutable sort
}

sortByName(['Ångström', 'Äpfel', 'Apple', 'azure'], 'sv-SE');
// Swedish: ['Äpfel', 'Apple', 'azure', 'Ångström'] — Ä comes after Z in Swedish

sortByName(['Ångström', 'Äpfel', 'Apple', 'azure'], 'de-DE');
// German: ['Äpfel', 'Ångström', 'Apple', 'azure'] — Ä treated as Ae

// Numeric sort: "item2" before "item10"
const numericCollator = new Intl.Collator('en-US', { numeric: true });
['item10', 'item2', 'item1'].sort((a, b) => numericCollator.compare(a, b));
// ['item1', 'item2', 'item10']
```

// In React: memoize the collator with `useMemo(() => new Intl.Collator(locale, { sensitivity: 'base' }), [locale])` and sort with `useMemo` on the data dependency — avoids recreating the collator on every render.

---

### 8. Translation Workflow

Keys should be semantic identifiers (not source strings). Provide context comments for translators. Keep interpolation variables descriptive. Never expose raw ICU syntax to translators without tooling support.

**Red Flags:**
- Keys that are the source string: `t('Hello, world!')` — breaks when English text changes
- No context for ambiguous keys: `t('open')` — is this a verb or an adjective?
- Positional placeholders `{0}`, `{1}` — translator cannot tell what they represent
- Massive flat translation files without namespace organization
- Concatenation in keys: `t('status.' + status)` — static analysis cannot find usages

**Key naming conventions:**
```json
{
  "_comment": "Context for translator: Button label shown in checkout flow, submits payment",
  "checkout.payment.submit_button": "Pay {formattedAmount}",

  "_comment": "Context: Status badge on order list, adjective describing order state",
  "order.status.open": "Open",
  "order.status.closed": "Closed",
  "order.status.pending": "Pending",

  "_comment": "Context: Error shown when payment card is declined",
  "checkout.error.card_declined": "Your card was declined. Please try a different payment method."
}
```

**TypeScript — type-safe translation keys:**
```typescript
// Generate types from translation file to catch missing keys at compile time
import en from './locales/en.json';

type TranslationKey = keyof typeof en;

function t(key: TranslationKey, values?: Record): string {
  return intl.formatMessage({ id: key }, values);
}

t('checkout.payment.submit_button', { formattedAmount: '$42.00' }); // OK
t('checkout.nonexistent_key'); // TypeScript compile error
```

**Namespace organization for large apps:**
```typescript
// i18next namespace pattern
const namespaces = {
  common: 'common',     // shared UI: buttons, labels, errors
  checkout: 'checkout', // checkout flow
  profile: 'profile',   // user profile
  admin: 'admin',       // admin panel (only loaded for admins)
};

// Lazy-load namespaces on demand
await i18next.loadNamespaces('checkout');
i18next.t('checkout:payment.submit_button');
```

Cross-reference: `code-documentation-patterns` — JSDoc `@i18n` annotations on functions that return user-visible strings.

---

### 9. i18n Testing

i18n bugs are invisible in development when only English is tested. Pseudo-localization, string expansion simulation, and missing key detection catch issues before reaching production.

**Red Flags:**
- No pseudo-locale in development or CI
- Only testing with English locale
- No assertion that all keys in source locale exist in target locales
- Layout tests not run with longest translations (German is ~35% longer than English)
- No detection for untranslated strings (keys rendered raw)

**Pseudo-localization utility:**
```typescript
// Transforms English text to visually distinct but readable pseudo-locale
// Catches hardcoded strings that bypass the translation system
function pseudoLocalize(text: string): string {
  const charMap: Record = {
    a: 'â', e: 'ê', i: 'î', o: 'ô', u: 'û',
    A: 'Â', E: 'Ê', I: 'Î', O: 'Ô', U: 'Û',
  };
  const transformed = text.split('').map((c) => charMap[c] ?? c).join('');
  return `[!! ${transformed} !!]`; // brackets reveal untranslated strings
}
// "Hello, world!" → "[!! Hêllô, wôrld! !!]"
```

**String expansion simulation (German ~35% longer):**
```typescript
function simulateStringExpansion(text: string, factor = 1.4): string {
  const padding = 'x'.repeat(Math.ceil(text.length * (factor - 1)));
  return `${text}${padding}`;
}
// Use in layout tests to verify UI doesn't overflow or truncate
```

**Missing key detection in tests:**
```typescript
import en from '../locales/en.json';
import de from '../locales/de.json';
import ar from '../locales/ar.json';

describe('translation completeness', () => {
  const enKeys = new Set(Object.keys(en));

  test.each([
    ['de', de],
    ['ar', ar],
  ])('%s has all required keys', (locale, translations) => {
    const localeKeys = new Set(Object.keys(translations));
    const missingKeys = [...enKeys].filter((k) => !localeKeys.has(k));
    expect(missingKeys).toEqual([]);
  });
});
```

**Component i18n test — render with target locale and assert formatted output:**
```tsx
test('renders product price in German locale', () => {
  render(
    
      
    
  );
  expect(screen.getByText('29,99 €')).toBeInTheDocument();
});
```

Cross-reference: `testing-patterns` — snapshot testing caveats when locale output is included in snapshots.

---

## i18n Anti-Patterns

| Anti-Pattern | Description

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [mickeyyaya](https://github.com/mickeyyaya)
- **Source:** [mickeyyaya/refactoring-skills](https://github.com/mickeyyaya/refactoring-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-mickeyyaya-refactoring-skills-i18n-l10n-patterns
- Seller: https://agentstack.voostack.com/s/mickeyyaya
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
