I18n
Skills I use with Claude Code across my projects. Architecture, code review, testing, security, deployment, and more.
npx -y skills add pvnarp/agent-skills --skill i18nAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Guides internationalization (i18n) and localization (l10n) implementation. Covers string extraction, locale handling, date/number/currency formatting, RTL support, and translation workflows. Use when adding multi-language support or reviewing i18n implementation.
SKILL.md
5.4 KB, as published. Nobody here has run it
Internationalization (i18n)
i18n is architecture. l10n is content. Get the architecture right first - adding languages later becomes trivial.
Core Principles
- No hardcoded user-facing strings. Every string the user sees comes from a translation file.
- Locale-aware formatting. Dates, numbers, currency, plurals - never format these yourself.
- Design for expansion. German text is ~30% longer than English. UI must accommodate.
- Separate translation from code. Translators edit translation files, not source code.
String Externalization
Before (hardcoded)
"3 items in your cart"
"Welcome back, John"
"January 15, 2024"
After (externalized)
t('cart.item_count', { count: 3 }) → "3 items in your cart" / "3 Artikel in Ihrem Warenkorb"
t('greeting.welcome_back', { name }) → "Welcome back, John" / "Willkommen zurück, John"
formatDate(date, { locale }) → "January 15, 2024" / "15. Januar 2024"
Translation File Structure
{
"cart": {
"item_count": "{count, plural, one {# item in your cart} other {# items in your cart}}",
"empty": "Your cart is empty",
"checkout": "Proceed to checkout"
},
"greeting": {
"welcome_back": "Welcome back, {name}"
}
}
Rules:
- Keys are hierarchical and descriptive (
cart.item_count, notstr_47) - Keep translations close to where they're used (per-page or per-component bundles for large apps)
- English is the source of truth - other locales are translations of it
- Include context comments for translators when meaning is ambiguous
Pluralization
Different languages have different plural rules. English has 2 forms (1, other). Arabic has 6. Russian has 3. Never do this:
// WRONG
count === 1 ? "1 item" : `${count} items`
Use ICU MessageFormat or your framework's plural support:
"{count, plural, one {# item} other {# items}}"
| Language | Plural Forms | Example |
|---|---|---|
| English | one, other | 1 item, 2 items |
| French | one, other | (but 0 is "one") |
| Russian | one, few, many, other | 1 файл, 2 файла, 5 файлов |
| Arabic | zero, one, two, few, many, other | 6 forms |
| Japanese | other | (no plural) |
Date, Number, and Currency Formatting
Never format manually. Use Intl (JS), NumberFormat / DateFormat (JVM), or equivalent.
// Dates
new Intl.DateTimeFormat('de-DE', { dateStyle: 'long' }).format(date)
// → "15. Januar 2024"
// Numbers
new Intl.NumberFormat('de-DE').format(1234567.89)
// → "1.234.567,89"
// Currency
new Intl.NumberFormat('ja-JP', { style: 'currency', currency: 'JPY' }).format(1000)
// → "¥1,000"
Gotchas:
- Decimal separator:
.(US) vs,(EU) vs٫(Arabic) - Date order: MM/DD/YYYY (US) vs DD/MM/YYYY (EU) vs YYYY/MM/DD (ISO/Asia)
- Currency symbol position: $100 (US) vs 100€ (EU) vs 100 ₽ (Russia)
- Week starts on: Sunday (US) vs Monday (EU/ISO)
RTL (Right-to-Left) Support
Required for Arabic, Hebrew, Persian, Urdu.
CSS
/* Use logical properties instead of physical */
margin-inline-start: 1rem; /* NOT margin-left */
padding-inline-end: 0.5rem; /* NOT padding-right */
border-inline-start: 1px solid; /* NOT border-left */
text-align: start; /* NOT text-align: left */
/* Set direction on the root */
html[dir="rtl"] { direction: rtl; }
Layout
- Flexbox and Grid with
start/endhandle RTL automatically - Icons with directional meaning (arrows, progress) must be mirrored
- Phone numbers, code blocks, and URLs stay LTR even in RTL context
Implementation Checklist
Phase 1: Architecture
- i18n library/framework integrated
- Locale detection (browser, user preference, URL)
- Translation file structure defined
- All user-facing strings externalized (no hardcoded strings)
Phase 2: Formatting
- Dates use locale-aware formatting
- Numbers use locale-aware formatting
- Currency uses locale-aware formatting with correct currency code
- Pluralization uses ICU/CLDR rules (not
if count === 1)
Phase 3: Layout
- UI handles 30%+ text expansion without breaking
- CSS uses logical properties (
start/endnotleft/right) - RTL layout tested (if supporting RTL languages)
- Fonts support target language character sets
Phase 4: Workflow
- Translation file format works with translator tools
- New strings flagged for translation in code review
- Missing translations fall back to default language gracefully
- Translation coverage tracked per locale
Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
| Concatenating translated strings | "Hello " + name + "!" breaks in other languages (word order differs) | Use interpolation: t('hello', { name }) |
| Hardcoded date format | "01/02/2024" - is that Jan 2 or Feb 1? | Use Intl.DateTimeFormat |
| Assuming text length | German/Finnish can be 30-50% longer | Design flexible layouts |
| Translating dev-facing text | Error codes, log messages | Only translate user-facing text |
| Image with embedded text | Can't translate | Use text overlays or separate images per locale |