Internationalization and localization for global applications. Use when adding multi-language support, handling regional formats, or preparing apps for global markets.
Comprehensive guide for building globally-ready applications.
i18n (Internationalization):
- Engineering for multiple languages/regions
- Building the infrastructure
- Happens once, during development
L10n (Localization):
- Adapting for specific locale
- Translation, formatting, content
- Happens per locale, ongoing
Locale:
- Language + Region combination
- Format: language-REGION (e.g., en-US, pt-BR)
- Determines formatting rules
| Category | Examples | | ------------- | ---------------------------- | | Text | UI labels, messages, errors | | Numbers | 1,234.56 vs 1.234,56 | | Dates | MM/DD/YYYY vs DD/MM/YYYY | | Times | 12-hour vs 24-hour | | Currency | $1,234.56 vs €1.234,56 | | Plurals | 1 item vs 2 items vs 5 items | | Direction | LTR vs RTL | | Images | Cultural appropriateness | | Colors | Cultural significance | | Names | Order, formality |
// DON'T: Hardcoded strings
const message = "Welcome back, " + username;
// DO: Externalized, translatable
const message = t('welcome_back', { name: username });
// Translation file (en.json)
{
"welcome_back": "Welcome back, {{name}}"
}
// Translation file (es.json)
{
"welcome_back": "Bienvenido de nuevo, {{name}}"
}
// ICU Message Format (recommended)
// Handles plurals, select, dates, numbers
// Simple
"greeting": "Hello, {name}!"
// Plural
"items_count": "{count, plural, =0 {No items} one {# item} other {# items}}"
// Select (gender, etc.)
"notification": "{gender, select, male {He} female {She} other {They}} liked your post"
// Nested
"cart": "{itemCount, plural,
=0 {Your cart is empty}
one {You have # item in your cart}
other {You have # items in your cart}
}"
Languages have different plural rules:
English: 1 (one), 2+ (other)
French: 0-1 (one), 2+ (other)
Russian: Complex rules for 1, 2-4, 5-20, 21, etc.
Arabic: 6 plural forms!
Chinese: No plural forms
Always use library pluralization, never DIY:
- react-intl / formatjs
- i18next
- Intl.PluralRules API
// Date formatting
const date = new Date("2024-03-15");
new Intl.DateTimeFormat("en-US").format(date);
// "3/15/2024"
new Intl.DateTimeFormat("de-DE").format(date);
// "15.3.2024"
new Intl.DateTimeFormat("ja-JP").format(date);
// "2024/3/15"
// With options
new Intl.DateTimeFormat("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
}).format(date);
// "Friday, March 15, 2024"
// Always store in UTC
const utcDate = new Date().toISOString();
// "2024-03-15T14:30:00.000Z"
// Display in user's timezone
new Intl.DateTimeFormat("en-US", {
timeZone: "America/New_York",
dateStyle: "full",
timeStyle: "long",
}).format(new Date(utcDate));
// "Friday, March 15, 2024 at 10:30:00 AM EDT"
// Get user's timezone
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
rtf.format(-1, "day"); // "yesterday"
rtf.format(1, "day"); // "tomorrow"
rtf.format(-3, "hour"); // "3 hours ago"
rtf.format(2, "week"); // "in 2 weeks"
// For automatic unit selection, use a library like date-fns
import { formatDistanceToNow } from "date-fns";
formatDistanceToNow(date, { addSuffix: true });
// "3 days ago"
const number = 1234567.89;
new Intl.NumberFormat("en-US").format(number);
// "1,234,567.89"
new Intl.NumberFormat("de-DE").format(number);
// "1.234.567,89"
new Intl.NumberFormat("fr-FR").format(number);
// "1 234 567,89"
// Percentages
new Intl.NumberFormat("en-US", {
style: "percent",
minimumFractionDigits: 1,
}).format(0.256);
// "25.6%"
const amount = 1234.56;
new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount);
// "$1,234.56"
new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
}).format(amount);
// "1.234,56 €"
new Intl.NumberFormat("ja-JP", {
style: "currency",
currency: "JPY",
}).format(amount);
// "¥1,235" (no decimal for JPY)
// Store amount + currency code
interface Money {
amount: number; // In smallest unit (cents)
currency: string; // ISO 4217 code (USD, EUR, etc.)
}
// Format for display
function formatMoney(money: Money, locale: string): string {
return new Intl.NumberFormat(locale, {
style: "currency",
currency: money.currency,
}).format(money.amount / 100); // Convert cents to units
}
// Handle exchange rates on backend
// Never convert currencies client-side
RTL Languages:
- Arabic (ar)
- Hebrew (he)
- Persian/Farsi (fa)
- Urdu (ur)
Layout considerations:
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ Logo Menu Settings │ │ Settings Menu Logo │
│ ← Back │ │ Back → │
│ [Icon] Label │ │ Label [Icon] │
│ Next → │ │ ← Next │
└─────────────────────────────┘ └─────────────────────────────┘
LTR Layout RTL Layout
/* Use logical properties */
/* DON'T: Physical properties */
.element {
margin-left: 10px;
padding-right: 20px;
text-align: left;
float: left;
}
/* DO: Logical properties */
.element {
margin-inline-start: 10px;
padding-inline-end: 20px;
text-align: start;
float: inline-start;
}
/* Set direction based on locale */
html[dir="rtl"] {
direction: rtl;
}
/* Or use :dir() pseudo-class */
.icon:dir(rtl) {
transform: scaleX(-1); /* Mirror icons */
}
/* Flexbox auto-reverses in RTL */
.container {
display: flex;
/* No need to change for RTL */
}
<!-- Set at document level -->
<html lang="ar" dir="rtl">
<!-- Or dynamically -->
<div dir="auto">
<!-- Auto-detects text direction -->
مرحبا بالعالم
</div>
<!-- Isolate bidirectional text -->
<p>The word <bdi>مرحبا</bdi> means "hello".</p>
</html>
// src/i18n/index.ts
import { createIntl, createIntlCache } from '@formatjs/intl';
const cache = createIntlCache();
const messages = {
en: () => import('./locales/en.json'),
es: () => import('./locales/es.json'),
fr: () => import('./locales/fr.json'),
};
export async function loadMessages(locale: string) {
const loader = messages[locale] || messages.en;
return (await loader()).default;
}
// App.tsx
import { IntlProvider } from 'react-intl';
function App() {
const [locale, setLocale] = useState('en');
const [messages, setMessages] = useState({});
useEffect(() => {
loadMessages(locale).then(setMessages);
}, [locale]);
return (
<IntlProvider locale={locale} messages={messages}>
<MainApp />
</IntlProvider>
);
}
import { FormattedMessage, useIntl } from "react-intl";
function ProductCard({ product }) {
const intl = useIntl();
return (
<div>
{/* Component-based */}
<h2>
<FormattedMessage id="product.title" values={{ name: product.name }} />
</h2>
{/* Hook-based (for attributes, etc.) */}
<img
src={product.image}
alt={intl.formatMessage({ id: "product.image_alt" })}
/>
{/* Numbers */}
<p>
<FormattedNumber
value={product.price}
style="currency"
currency="USD"
/>
</p>
{/* Dates */}
<p>
<FormattedDate value={product.createdAt} dateStyle="medium" />
</p>
{/* Plurals */}
<p>
<FormattedMessage
id="product.reviews"
values={{ count: product.reviewCount }}
/>
</p>
</div>
);
}
// locales/en.json
{
"product.title": "{name}",
"product.image_alt": "Photo of {name}",
"product.reviews": "{count, plural, =0 {No reviews} one {# review} other {# reviews}}",
"cart.empty": "Your cart is empty",
"cart.checkout": "Proceed to checkout"
}
// locales/es.json
{
"product.title": "{name}",
"product.image_alt": "Foto de {name}",
"product.reviews": "{count, plural, =0 {Sin reseñas} one {# reseña} other {# reseñas}}",
"cart.empty": "Tu carrito está vacío",
"cart.checkout": "Proceder al pago"
}
import i18next from "i18next";
import Backend from "i18next-fs-backend";
await i18next.use(Backend).init({
lng: "en",
fallbackLng: "en",
supportedLngs: ["en", "es", "fr", "de"],
backend: {
loadPath: "./locales/{{lng}}/{{ns}}.json",
},
interpolation: {
escapeValue: false,
},
});
// Usage
const t = i18next.t;
t("welcome", { name: "John" });
// "Welcome, John!"
// Change language
await i18next.changeLanguage("es");
import { Request, Response, NextFunction } from "express";
function detectLocale(req: Request, res: Response, next: NextFunction) {
// Priority: Query param > Cookie > Accept-Language header > Default
const locale =
req.query.locale ||
req.cookies.locale ||
req.acceptsLanguages(["en", "es", "fr"]) ||
"en";
req.locale = locale;
req.t = i18next.getFixedT(locale);
next();
}
app.use(detectLocale);
app.get("/api/greeting", (req, res) => {
res.json({
message: req.t("greeting", { name: req.user.name }),
});
});
locales/
├── en/
│ ├── common.json # Shared strings
│ ├── auth.json # Auth-related
│ ├── products.json # Product-related
│ └── errors.json # Error messages
├── es/
│ └── ...
├── fr/
│ └── ...
└── _source/
└── en.json # Source of truth
{
// Feature.element.description
"auth.login.button": "Sign In",
"auth.login.error.invalid_credentials": "Invalid email or password",
// Or flat with prefixes
"btn_login": "Sign In",
"err_invalid_credentials": "Invalid email or password"
// Consistent, descriptive, unique
}
TOOLS:
- Lokalise, Crowdin, Phrase (SaaS platforms)
- Weblate (open source)
- POEditor, Transifex
WORKFLOW:
1. Extract strings from code
2. Upload to translation platform
3. Translators work in context
4. Review translations
5. Download and integrate
6. Test in app
7. Repeat for changes
AUTOMATION:
- CI/CD integration
- Automatic string extraction
- Translation memory
- Machine translation + human review
// Transforms: "Hello" → "[Ħëľľö!!!]"
// Helps identify:
// - Hardcoded strings
// - Text truncation issues
// - Character encoding problems
// - Layout issues with longer text
function pseudoLocalize(text: string): string {
const charMap: Record<string, string> = {
a: "ä",
b: "β",
c: "ç",
d: "δ",
e: "ë",
// ... etc
};
const transformed = text
.split("")
.map((c) => charMap[c.toLowerCase()] || c)
.join("");
// Add padding (most translations are 30% longer)
const padding = "!".repeat(Math.ceil(text.length * 0.3));
return `[${transformed}${padding}]`;
}
FOR EACH LOCALE:
□ All strings translated
□ No hardcoded text visible
□ Dates format correctly
□ Numbers format correctly
□ Currency displays properly
□ Plurals work for all cases
□ UI accommodates text length
□ RTL layout correct (if applicable)
□ Images are appropriate
□ No encoding issues
□ Forms validate appropriately
npx skills add travisjneuman/i18n-localization下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer