Skill
·
v1.0.0
·
MIT
i18n-review
Audit code and UI for internationalization correctness - string extraction, pluralization, dates/numbers/currencies, RTL support, and locale-aware sorting. Use when the user asks for an i18n review, localization audit, or wants to make an app translation-ready.
elyra ›
/skills install i18n-review
The goal: a translator can ship a new locale without touching code, and the result is correct for native speakers, not "English with words swapped."
When to use
- "Review X for i18n / localization"
- "Audit translation readiness"
- "Make this app translatable"
- "Add support for
"
Procedure
- Identify the framework —
i18next,react-intl,vue-i18n,Laravel __(),gettext,Rails I18n, custom? Match existing conventions. - Walk the checklist below
- Group findings by severity
Checklist
String extraction
- No hardcoded user-facing strings in templates, components, or controllers
- Strings live in translation files (
en.json,messages.po,lang/en/…) - Translation keys are stable identifiers, not the English text itself (
auth.login.titlenot"Sign in") - Keys aren't constructed dynamically (
t('errors.' + code)defeats static extraction — use a switch with explicit keys instead) - No concatenated sentences:
t('hello') + ' ' + name→ use interpolation:t('hello_name', {name}) - No HTML embedded in translatable strings — use placeholders or rich-text helpers
- Plurals use the framework's plural helper, not
if (n === 1)
Plurals
Plural rules vary wildly. English has 2 forms; Polish has 4; Arabic has 6. Don't fake it:
// ❌ Don't
const msg = count === 1 ? '1 item' : `${count} items`;
// ✅ Do
t('item_count', { count })
// en.json: { "item_count_one": "{{count}} item", "item_count_other": "{{count}} items" }
// pl.json: { "item_count_one": "...", "item_count_few": "...", "item_count_many": "...", "item_count_other": "..." }
Dates, numbers, currencies
- No hardcoded date formats (
YYYY-MM-DD) for display — useIntl.DateTimeFormator framework helper - Numbers formatted with locale separators (
1,234.56vs1.234,56vs1 234,56) - Currencies formatted with
Intl.NumberFormat(locale, {style:'currency', currency}), never by string concat ('$' + amount) - Currency code stored, locale-formatted on display
- Time zones explicit — store UTC, display in user's TZ
- Week start day not hardcoded (Sunday vs Monday vs Saturday varies)
Text direction (RTL)
- Layout uses logical properties:
margin-inline-startnotmargin-left,padding-inline-endnotpadding-right -
dir="rtl"flips the layout correctly (test with Arabic, Hebrew, or?dir=rtl) - Icons that imply direction (arrows, back/forward) flip in RTL
- Numbers and code blocks stay LTR even in RTL contexts
- No
text-align: leftwhen you mean "start" — usetext-align: start
Sorting & collation
- Lists sorted with
Intl.Collator(locale), not byte-wise /Array.sort() - Case-insensitive comparisons use
localeComparewith proper options - Search/filter is accent-insensitive where appropriate (
sensitivity: 'base')
Input
- Name fields accept unicode (no
[a-zA-Z]+validation) - Email validation allows internationalized domains
- Phone number format not hardcoded to one country (use
libphonenumberor similar) - Address forms don't assume US-style state/zip
- Long words / long translations don't break layout (German is famously verbose)
Tooling
- Linter / extractor rule prevents new hardcoded strings (
eslint-plugin-i18next,i18n-ally,phpcsrules, …) - CI fails if translations are missing keys (or warns with a baseline)
- Locale fallback chain is explicit (
fr-CA → fr → en) - Language switching doesn't require full reload (where the framework supports it)
Quick greps
# Hardcoded English strings in templates (heuristic)
grep -RInE '>[A-Z][a-z]+ [a-z]+' --include='*.{vue,jsx,tsx,blade.php,erb}' . | head
# Concatenated translations
grep -RInE "t\([^)]+\)\s*\+|i18n\.[a-z]+\([^)]+\)\s*\+" --include='*.{js,ts,jsx,tsx,vue}' .
# Hardcoded currency symbols
grep -RInE "['\"\`]\\\$\{|\\\$[0-9]|'\\\$'" --include='*.{js,ts,jsx,tsx,vue,blade.php}' .
# Physical CSS properties that should be logical
grep -RInE 'margin-(left|right)|padding-(left|right)|text-align:\s*(left|right)' \
--include='*.{css,scss,vue,jsx,tsx}' .
# Date format strings that look hardcoded
grep -RInE "format\(['\"](YYYY|DD|MM)" --include='*.{js,ts,jsx,tsx,vue}' .
Output format
## i18n review: <scope>
**Framework:** <i18next / vue-i18n / Laravel / …>
**Locales in use:** <en, no, ar, …>
### 🔴 Blockers
- `path/file.tsx:42` — <issue>. **Impact:** <breaks for which locales/users>. **Fix:** <what>.
### 🟠 Major
- …
### 🟡 Minor
- …
### ✅ What's working
- <category> — <one sentence>
Severity rubric
| Tag | Meaning |
|---|---|
| 🔴 Blocker | Cannot be translated correctly (hardcoded strings, broken plurals, broken RTL layout) |
| 🟠 Major | Renders incorrectly for many locales (date/number format, sorting, address forms) |
| 🟡 Minor | Polish (long-text overflow, missing locale fallback) |
Anti-patterns
- ❌
t('You have ' + count + ' messages')— broken in every language with grammatical agreement - ❌ Translating button text but baking in
width: 80px(German "Einstellungen" doesn't fit) - ❌ Storing formatted currency in DB (
"$10.00") - ❌ Sorting names with
Array.sort()instead ofIntl.Collator - ❌ Right-aligned designs that "just work" in LTR
- ❌ Assuming names have a first and a last name with a space between
- ❌ Validating names with
[a-zA-Z\s]+ - ❌ Mirror-flipping all icons in RTL (don't flip clocks, logos, media controls)
Useful references
- Unicode CLDR — locale data the world relies on
- W3C Internationalization Activity — specs and guides
- MDN: Intl — browser-native locale APIs
- Falsehoods Programmers Believe About Names
- Falsehoods Programmers Believe About Time