Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

I18n Rules

ASecurity

Internationalisation discipline — every user-facing string in a catalog (no inline strings); ICU MessageFormat for plurals + interpolation; Intl APIs for date / number / currency / list / collation; BCP 47 locale identifiers; RTL mirroring for Arabic / Hebrew / Persian / Urdu; locale-aware sort + search; per-country address + name formats; currency placement varies by locale; transactional emails / SMS / push routed through the same i18n pipeline. Auto-fires on i18n / locales / translations /...

12 stars
0 votes
0 copies
0 views
Added 9/28/2026
developmenttypescriptpythonrustgojavarubyreactvuenextjsnode

Works with

api

Security Analysis

A100/100

Scanned 9/28/2026

Install to Claude Code

$npx -y skills add Nmor/the-claude-council --skill i18n-rules --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of I18n Rules?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for I18n Rules
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/nmor-i18n-rules/badge)](https://www.skillsdirectory.com/skills/nmor-i18n-rules)

More formats (shields.io, HTML) on the badges page.

Files
SKILL.md
---
name: i18n-rules
description: Internationalisation discipline — every user-facing string in a catalog (no inline strings); ICU MessageFormat for plurals + interpolation; Intl APIs for date / number / currency / list / collation; BCP 47 locale identifiers; RTL mirroring for Arabic / Hebrew / Persian / Urdu; locale-aware sort + search; per-country address + name formats; currency placement varies by locale; transactional emails / SMS / push routed through the same i18n pipeline. Auto-fires on i18n / locales / translations / message catalogs.
paths:
  - "**/i18n/**"
  - "**/locales/**"
  - "**/translations/**"
  - "**/messages/**"
  - "**/lang/**"
  - "**/*.po"
  - "**/*.pot"
  - "**/*.xliff"
  - "**/*.arb"
  - "**/messages*.json"
  - "**/translations*.json"
  - "**/intl/**"
---

# i18n-rules

> Migrated 2026-06-02 from `~/.claude/rules/common/` as part of the lazy-rules-loading plan. Phase H
> will delete the source files to close the eager-load loop.
>
> **Size budget: 21 KB** — `token-budget.mjs --check`.

## Source files migrated

- `rules-library/common/i18n.md`

---

<!-- ============================================================
     Section: i18n.md (from rules/common/)
     ============================================================ -->

## Internationalisation (i18n) Rule (Always-On, Global)

> Auto-fires on every file. Sister to `a11y.md` (RTL + text
> expansion overlap), `error-codes.md` (codes are i18n keys),
> `gdpr-ccpa.md` (per-region compliance), `task-intake-due-
> diligence.md` Q13. Standards: **Unicode CLDR** (Common Locale
> Data Repository), **ICU Message Format**, **BCP 47** (language
> tags), **ECMA-402** (Intl API), **RFC 5646** (language tags),
> **W3C Internationalization Working Group**.

### Core Principle

**Every user-facing string, number, date, currency, address, and
plural form is locale-aware. The system is designed to support
any locale from day one — even if launch is single-locale. Adding
a language later should not require touching application code.**

i18n is not just "translate the buttons." It's plural rules,
date/number formats, currency, RTL layout, name ordering, address
formats, sort orders, search collation, calendar systems, and
locale-aware error messages.

### Hard rules

#### 1. No hardcoded user-facing strings

Every user-visible string lives in a translation catalog
(`.json`, `.po`, `.xliff`, `.properties`, `.yml`). Code
references the KEY, not the string:

```typescript
// WRONG
toast.success("Order placed");

// RIGHT
toast.success(t('orders.placed.success'));
```

The catalog is the source of truth; translators work on the
catalog; code never embeds untranslated strings.

#### 2. Use ICU Message Format for plurals + interpolation

ICU MessageFormat (CLDR-based) handles plural rules across all
languages — including languages with multiple plural forms
(Polish: 4; Arabic: 6):

```text
"orders.count": "{count, plural,
  =0 {No orders}
  one {1 order}
  other {# orders}
}"
```

Library support: `@formatjs/intl` (React/JS), `fluent` (Mozilla,
JS/Rust), `messageformat`, `i18next` (with ICU plugin).

NEVER concatenate translated fragments: `"You have " + count + "
orders"` is grammatically wrong in many languages. Always pass
the full sentence through the formatter.

#### 3. Use the platform Intl API

ECMA-402 (Intl) is universally available. Never hand-roll
formatting:

```typescript
// Numbers — locale-aware grouping + decimals
new Intl.NumberFormat('en-US').format(1234567.89);     // "1,234,567.89"
new Intl.NumberFormat('de-DE').format(1234567.89);     // "1.234.567,89"
new Intl.NumberFormat('fr-FR').format(1234567.89);     // "1 234 567,89"
new Intl.NumberFormat('ar-EG').format(1234567.89);     // "١٬٢٣٤٬٥٦٧٫٨٩"

// Currency
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' })
  .format(99.5);                                        // "$99.50"
new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' })
  .format(99.5);                                        // "¥100" (no decimals)

// Dates
new Intl.DateTimeFormat('en-US', { dateStyle: 'long' })
  .format(new Date());                                  // "May 26, 2026"
new Intl.DateTimeFormat('ja-JP-u-ca-japanese', { dateStyle: 'long' })
  .format(new Date());                                  // "令和8年5月26日"

// Relative time
new Intl.RelativeTimeFormat('en').format(-1, 'day');    // "1 day ago"
new Intl.RelativeTimeFormat('fr').format(-1, 'day');    // "il y a 1 jour"

// List
new Intl.ListFormat('en', { style: 'long', type: 'conjunction' })
  .format(['Apple', 'Banana', 'Cherry']);               // "Apple, Banana, and Cherry"
new Intl.ListFormat('de').format(['Apfel', 'Banane', 'Kirsche']);
                                                        // "Apfel, Banane und Kirsche"
```

Equivalent APIs exist in every modern language (Java
`java.text.MessageFormat`, Python `babel`, Go `golang.org/x/text`,
Ruby `R18n`, etc.).

#### 4. Locale identification: BCP 47

The canonical format is BCP 47:

- Language: `en`, `fr`, `ar`, `zh`
- Language + region: `en-US`, `en-GB`, `pt-BR`, `pt-PT`
- Language + script: `zh-Hant` (Traditional), `zh-Hans` (Simplified)
- Full: `zh-Hant-HK`, `sr-Latn-RS` (Serbian in Latin script)
- Extensions: `en-US-u-ca-gregory-fw-mon` (Gregorian calendar,
  week starts Monday)

The user's locale is stored on their profile + sent in
`Accept-Language` header + matched via Intl negotiation:

```typescript
const userLocale = Intl.getCanonicalLocales(req.user.locale ?? req.acceptsLanguages())[0];
```

#### 5. RTL languages mirror layout

Arabic, Hebrew, Persian, Urdu, and other RTL languages reverse
the reading direction. The layout MUST mirror:

- `<html dir="rtl">` (and `lang="ar"`) for RTL pages
- CSS logical properties (`margin-inline-start` not
  `margin-left`, `padding-inline-end` not `padding-right`)
- `text-align: start` / `text-align: end` (not `left` / `right`)
- Icons reflect direction (arrow-back becomes arrow-forward in
  RTL)
- BUT: numbers + Latin script don't flip; phone numbers stay
  LTR even in RTL contexts
- BUT: progress bars + media seek bars typically don't flip

Test with at least one RTL locale (Arabic or Hebrew) — the
mirroring catches a class of "assumed left-to-right" bugs.

#### 6. Currency + payment locale

Different from display locale. A user in Germany browsing in
English might still expect prices in EUR:

| Concept | Source |
| --- | --- |
| **Display language** | User profile / Accept-Language |
| **Currency** | Account settings / billing region |
| **Tax calculation** | Billing address / VAT rules per country |
| **Date/time format** | Display language |
| **Number format** | Display language |
| **Phone format** | E.164 storage; libphonenumber-formatted by region |
| **Address format** | Per country (use a library — Google Address Components or `i18n-postal-address`) |
| **Name format** | Per culture (first/last vs family/given vs single name) |

#### 7. Text expansion + truncation

English → German: ~30% longer on average. English → French:
~25% longer. English → Russian: ~40% longer. Buttons + labels
designed for English break in other languages:

- Avoid fixed-width buttons / containers for text
- Reserve space for the longest reasonable translation
- Use ellipsis + tooltip for truncated long names — never silently
  truncate
- Pseudo-localisation in dev: wrap every string in `[[ščẞ ... ščẞ]]`
  (visible markers + accented characters + 30% padding) to surface
  unlocalised strings + truncation issues

#### 8. Locale-aware errors

Per `error-codes.md` — codes are stable; messages translate.
Every error code has an i18n key:

```typescript
{
  "errors.wallet_insufficient_funds": {
    "en": "You don't have enough balance for this purchase.",
    "fr": "Vous n'avez pas assez de solde pour cet achat.",
    "ar": "لا يوجد لديك رصيد كافٍ لإتمام عملية الشراء هذه.",
    "ja": "この購入を完了するための残高が不足しています。"
  }
}
```

Error details that contain dynamic values (required vs available
balance) use ICU placeholders + Intl.NumberFormat for the
currency display.

#### 9. Translation memory + glossary

The translation infrastructure is more than catalog files:

- **Translation Management System (TMS)**: Crowdin, Lokalise,
  POEditor, Phrase, Smartling
- **Translation memory** (TM): leverages prior translations for
  consistency + cost reduction
- **Glossary**: brand terms, product names that should NOT be
  translated; technical terms with prescribed translations
- **Context for translators**: screenshots, descriptions,
  character limits, ICU rules
- **Locale QA pipeline**: native-speaker review for launch
  locales; machine translation + post-edit for long-tail

Translators are not engineers — providing CONTEXT (where this
string appears, max length, gender/number variants) is the
engineering team's job.

#### 10. Locale fallback chain

User's preferred locale is `ko-KR`. The catalog has:

- `ko-KR.json` (most specific) — used first
- `ko.json` (language fallback) — used for missing keys
- `en.json` (default — typically the source language) — used
  for missing keys above

The fallback chain is documented + the catalog includes coverage
metrics: "es-AR is 87% translated; missing keys fall back to es,
then en."

Untranslated strings should NEVER show as keys to the user
(`orders.placed.success` visible in the UI is a bug). They show
in the fallback language.

### Specific concerns

#### Plurals across languages

| Language | Plural forms | Categories |
| --- | --- | --- |
| English, Spanish | 2 | one, other |
| French | 2 | one, other (one includes 0 + 1) |
| Russian, Polish | 4 | one, few, many, other |
| Arabic | 6 | zero, one, two, few, many, other |
| Chinese, Japanese | 1 | other (no plural distinction) |
| Welsh | 6 | zero, one, two, few, many, other |

Hard-coding "1 item" / "{N} items" assumes English plurals; it
breaks in 6+ languages. Use ICU `plural` selector.

#### Date / time

| Format | Considerations |
| --- | --- |
| **Storage** | UTC ISO 8601 (`2026-05-26T14:32:18Z`); never local time |
| **Display** | User's timezone + locale-formatted (Intl.DateTimeFormat) |
| **Calendar systems** | Gregorian default; some users prefer Buddhist, Japanese, Islamic, Hebrew calendars |
| **Week start** | Monday (most of EU) vs Sunday (US) — Intl handles this |
| **First week** | ISO 8601 week 1 = first week with ≥ 4 days in the new year; US sometimes differs |
| **12 vs 24-hour clock** | Locale-determined |

#### Address formats

| Country | Format quirks |
| --- | --- |
| **US** | Street, City, State, ZIP |
| **UK** | House+Street, Town, County (optional), Postcode |
| **Japan** | Postcode FIRST, then prefecture → city → block → street → name |
| **Germany** | Street + house number, postcode + city |
| **Brazil** | Includes complement, CEP, neighborhood |
| **Egypt / Arabic** | Free-form often; building/street/city/governorate |

Use a library (Google Maps Address Components, `i18n-postal-
address`, Stripe Address Element) rather than hand-rolling.

#### Name formats

| Culture | Order |
| --- | --- |
| **Most Western** | Given Family |
| **East Asian** | Family Given (Chinese, Japanese, Korean, Vietnamese) |
| **Hungarian** | Family Given |
| **Single-name** | Single field (Indonesia, Iceland — patronymic) |
| **Arabic** | Multiple components (given, father's name, family name) |

Form: `first_name` + `last_name` is culturally biased. Use
`given_name` + `family_name` (CLDR terminology) OR a single
`full_name` field with a hint about ordering.

#### Search + sort collation

- Locale-aware sort: German `ä` sorts with `a` in DIN-1; with
  `ae` in DIN-2 (phonebook); Swedish sorts `ä` after `z`
- Search match: case + accent insensitive in most user-facing
  search ("café" matches "cafe")
- Use `Intl.Collator` for sort:

  ```typescript
  arr.sort((a, b) => new Intl.Collator('de').compare(a, b));
  ```

### Per-stack tooling

#### Web (JS / TS)

- **react-intl** / **formatjs** — ICU MessageFormat in React
- **next-intl** — Next.js i18n
- **vue-i18n** — Vue 3
- **i18next** — framework-agnostic
- **lingui** — type-safe i18n
- **lit-localize** — Web Components

#### Mobile

- **iOS**: NSLocalizedString + `.strings` files + `.stringsdict`
  for plurals
- **Android**: strings.xml + `<plurals>` element
- **React Native**: `i18next` + `react-native-localize`
- **Flutter**: `flutter_localizations` + ARB files

#### Backend

- **Node.js**: `@formatjs/intl` + `Intl`
- **Java**: `java.text.MessageFormat`, `ResourceBundle`, ICU4J
- **Python**: `babel` + `gettext`
- **Go**: `golang.org/x/text/language`, `go-i18n`
- **Ruby**: Rails i18n + `i18n-tasks`
- **.NET**: `IStringLocalizer<>` + `.resx` files

### Anti-patterns

#### Anti-pattern 1: "We'll add languages later"

Adding i18n after launch is 5-10x more expensive than building
i18n-aware from day one. Every concatenated string, every
hardcoded date format, every fixed-width button becomes a
multi-week refactor.

#### Anti-pattern 2: Auto-translate at runtime

Google Translate API on every request is slow, expensive,
inconsistent, and produces unprofessional output for product
strings. Use it ONLY for user-generated content (comments,
reviews) — and even then, with a "translated by machine"
disclaimer.

#### Anti-pattern 3: Localising only the UI

A localised UI that emails English receipts, sends English SMS
codes, and shows English error messages on backend failures is
not localised. ALL user-facing surfaces — including
transactional emails, SMS, push notifications, PDFs, support
chat — go through the same i18n pipeline.

#### Anti-pattern 4: One developer translates

The developer's "best-effort French" is worse than no translation
(it loses trust). Use professional translators or native
speakers; reserve machine translation for high-velocity / low-
quality-need surfaces with disclaimers.

#### Anti-pattern 5: Number / currency string concatenation

```typescript
// WRONG
return `$${amount}`;

// RIGHT
return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount);
```

Currency placement (€100 vs 100€), thousands separator, decimal
separator, decimal precision (JPY has none) — all vary by locale.

### Cross-references

- `a11y.md` — RTL layouts, text expansion, screen-reader pronunciation
- `error-codes.md` — codes have i18n keys
- `gdpr-ccpa.md` — privacy notices in every supported locale
- `task-intake-due-diligence.md` Q13 (i18n)
- `documentation-requirements.md` — docs are i18n too for public
  surfaces
- `feature-flags.md` — feature availability can vary by locale
- `audit-logging.md` — audit timestamps stored UTC

### Standards cited

- **Unicode CLDR (Common Locale Data Repository)** — the canonical
  i18n data
- **ICU MessageFormat 4.6+**
- **ECMA-402: ECMAScript Internationalization API**
- **BCP 47 / RFC 5646** — Language tags
- **Unicode Standard 16.0** — Character handling
- **ISO 4217** — Currency codes
- **ISO 3166-1 alpha-2** — Country codes
- **W3C i18n Working Group recommendations**

### Why this rule exists

Most products start in English + one market. The "let's localise
later" plan looks reasonable until customer success starts
asking: "Can we sell to Germany?" The answer requires a
multi-week refactor that touches every UI string, every date
format, every currency display, every plural pattern, every form
field.

Costs of localising from day one:

- Catalog files instead of inline strings (zero ongoing cost)
- Intl.NumberFormat / DateTimeFormat instead of string templates
  (already in the platform)
- ICU plural rules instead of `count === 1 ? "item" : "items"`
- 1 reusable translator workflow

Costs of localising after launch:

- Engineering refactor across the entire frontend
- New code paths for every formatter
- Test coverage for every locale
- Cultural review of UX patterns (e.g., name ordering, address
  forms)
- Multiple deploy cycles to roll out per-locale

Build i18n-aware on day one even when launching in one language;
the catalog + Intl approach has zero overhead and saves quarter-
long retrofits later.

### Learning hooks

Per `~/.claude/rules/common/continuous-learning-mandate.md`:

**Signals to watch**:

- Hardcoded user-facing string shipped (rule 1 weakening — every string lives in catalog)
- Plural form expressed via `count === 1 ? "item" : "items"` (rule 2 violation — ICU MessageFormat
  required)
- Hand-rolled number / date / currency formatter shipped (rule 3 weakening — Intl API mandatory)
- Locale identifier non-BCP-47 (rule 4 weakening)
- RTL layout missing on new UI for a locale that requires it (rule 5 weakening)
- Currency placement / decimal separator / thousands separator hardcoded (anti-pattern 5)
- Transactional email / SMS / push not routed through the i18n pipeline (anti-pattern 3 — partial
  localisation)
- Auto-translate API used as the sole translation source for product strings (anti-pattern 2)
- Locale fallback chain produces visible key strings to users (rule 10 weakening)

**Refinement candidates**:

- New locale row when a launch locale gains support, including its plural form count + RTL flag
- New tooling row when a TMS / translation memory vendor becomes the team's choice
- Tightening of the "no concatenation" rule when a new context-sensitive surface (e.g., voice /
  chat) emerges
- New cross-reference when a sister rule (a11y, error-codes) defines the i18n key shape the catalog
  must align to

---

Attribution

NmorNmor
View sourceMore from Nmor →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Browser Extension Developer

Use this skill when developing or maintaining browser extension code in the `browser/` directory, including Chrome/Firefox/Edge compatibility, content scripts, background scripts, or i18n updates.

284972 votes

Seo Optimizer

SEO optimization with keyword analysis, readability assessment, technical validation, content quality. Use for search rankings, blog posts, content audits, or encountering keyword density, readability scores, meta tags, schema markup errors.

2222 votes

Google Official Seo Guide

Official Google SEO guide covering search optimization, best practices, Search Console, crawling, indexing, and improving website search visibility based on official Google documentation

1862 votes

Tanstack Start

Build a full-stack TanStack Start app on Cloudflare Workers from scratch — SSR, file-based routing, server functions, D1+Drizzle, better-auth, Tailwind v4+shadcn/ui. Use whenever the user mentions TanStack Start, asks to scaffold a full-stack Cloudflare app with SSR, wants an SSR dashboard, or asks for a React 19 + Cloudflare Workers app with file-based routing and server functions — even if they don't name TanStack Start specifically. No template repo — Claude generates every file fresh per ...

10311 votes

Pentest

PTES-aligned adversarial security audit for backend, frontend, and mobile applications. Produces a CVSS-scored Hacker Report with verified PoCs and phased remediation.

5491 votes
View all in development →