Install
$ agentstack add skill-mickeyyaya-refactoring-skills-i18n-l10n-patterns ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →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):
{
"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):
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:
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
zeroform for languages where zero has distinct grammar (Arabic, Welsh) - Client-side plural logic in JavaScript instead of ICU MessageFormat
Binary plural anti-pattern:
// 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:
{
"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 preferencedate.toISOString().split('T')[0]for display — always renders asYYYY-MM-DDregardless 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:
// 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:
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:
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:
function BackButton() {
const { dir } = useTextDirection();
return (
);
}
Tailwind CSS logical property utilities (v3.3+):
...
...
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()oritems.sort((a, b) => a.localeCompare(b))without explicit localelocaleCompare()without collator options — ignores sensitivity and numeric sorting- Sorting names server-side with
ORDER BY namewithout locale-aware collation - Treating sort order as stable across locales (Swedish
åsorts afterz, not aftera)
TypeScript collation utilities:
// 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:
{
"_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:
// 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:
// 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:
// 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):
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:
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:
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
- Source: mickeyyaya/refactoring-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.