关于 i18n-localization
面向应用开发者的国际化与本地化指南,涵盖文本、日期、数字、货币、复数和 RTL 支持。提供 React 与 Node.js 实现示例,以及翻译工作流和测试清单。
主要功能
- 01国际化与本地化概念
- 02文本外置与 ICU 消息格式
- 03日期、数字和货币格式化
- 04RTL 布局支持
- 05React 与 Node.js 实现示例
- 06翻译流程与本地化测试
适用场景
- 01为应用规划多语言基础
- 02按地区格式显示日期与货币
- 03为界面添加 RTL 语言支持
- 04检查翻译覆盖与文本溢出
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ travisjneuman/ .claude --skill i18n-localization安装后在 Claude Code 中输入/i18n-localization确认已加载。
详细介绍
国际化与本地化
构建适用于全球用户的应用程序的综合指南。
核心概念
术语
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
哪些内容需要本地化?
| 类别 | 示例 |
|---|---|
| 文本 | UI 标签、消息、错误 |
| 数字 | 1,234.56 与 1.234,56 |
| 日期 | MM/DD/YYYY 与 DD/MM/YYYY |
| 时间 | 12 小时制与 24 小时制 |
| 货币 | $1,234.56 与 €1.234,56 |
| 复数形式 | 1 个项目、2 个项目与 5 个项目 |
| 文字方向 | 从左到右与从右到左 |
| 图像 | 文化适宜性 |
| 颜色 | 文化含义 |
| 姓名 | 顺序、正式程度 |
文本与消息
将所有字符串外置
// 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
日期与时间
JavaScript Intl 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(从右到左)支持
RTL 语言
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
RTL 的 CSS
/* 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 */
}
HTML 文字方向
<html lang="ar" dir="rtl">
<div dir="auto">
مرحبا بالعالم
</div>
<p>The word <bdi>مرحبا</bdi> means "hello".</p>
</html>
实现(React)
配置 react-intl
// 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"
}
实现(Node.js)
配置 i18next
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");
Express 中间件
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
最佳实践
应该:
- 将所有面向用户的字符串外置
- 对复杂字符串使用 ICU 消息格式
- 支持切换区域设置而无需重新加载
- 使用伪本地化进行测试
- 以 UTC 存储日期,并以本地时间显示
- 尽可能使用原生 Intl API
- 为文本扩展预留空间(长度可能增加 30-50%)
- 从一开始就支持 RTL
不应该:
- 将任何面向用户的文本硬编码
- 拼接字符串来构造消息
- 假定日期和数字的格式
- 手动处理复数形式
- 忽略文化背景
- 在代码中翻译(使用翻译文件)
- 忽略双向文本问题
- 跳过使用真实语言进行测试
清单
初始设置
- 选择 i18n 库
- 设置翻译文件结构
- 配置区域设置检测
- 添加区域设置切换器 UI
- 设置 RTL 支持
每项功能
- 将所有字符串提取到翻译文件中
- 使用消息格式处理变量
- 正确处理复数形式
- 格式化日期、数字和货币
- 使用伪本地化进行测试
- 在上下文中检查文本
发布
- 完成专业翻译
- 测试所有区域设置
- 验证 RTL 布局
- 为多语言设置 SEO(hreflang)
- 审核特定区域设置的内容
评论
支持 Markdown