i18n-localization

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Internationalization & Localization

国际化与本地化

Comprehensive guide for building globally-ready applications.
面向全球就绪应用的综合指南。

Key Concepts

核心概念

Terminology

术语

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
i18n (Internationalization):
- 为多语言/区域进行工程化设计
- 搭建基础架构
- 开发阶段一次性完成

L10n (Localization):
- 适配特定区域设置
- 翻译、格式调整、内容适配
- 针对每个区域持续进行

Locale:
- 语言+区域的组合
- 格式:language-REGION(例如:en-US, pt-BR)
- 决定格式规则

What Needs Localization?

需要本地化的内容

CategoryExamples
TextUI labels, messages, errors
Numbers1,234.56 vs 1.234,56
DatesMM/DD/YYYY vs DD/MM/YYYY
Times12-hour vs 24-hour
Currency$1,234.56 vs €1.234,56
Plurals1 item vs 2 items vs 5 items
DirectionLTR vs RTL
ImagesCultural appropriateness
ColorsCultural significance
NamesOrder, formality

分类示例
文本UI标签、提示信息、错误提示
数字1,234.56 vs 1.234,56
日期MM/DD/YYYY vs DD/MM/YYYY
时间12小时制 vs 24小时制
货币$1,234.56 vs €1.234,56
复数1项 vs 2项 vs 5项
排版方向LTR vs RTL
图片文化适配性
颜色文化象征意义
名称顺序、正式程度

Text & Messages

文本与提示信息

Externalize All Strings

外部化所有字符串

typescript
// 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}}"
}
typescript
// 错误示例:硬编码字符串
const message = "Welcome back, " + username;

// 正确示例:外部化、可翻译的字符串
const message = t('welcome_back', { name: username });

// 翻译文件 (en.json)
{
  "welcome_back": "Welcome back, {{name}}"
}

// 翻译文件 (es.json)
{
  "welcome_back": "Bienvenido de nuevo, {{name}}"
}

Message Format

消息格式

typescript
// 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}
}"
typescript
// ICU消息格式(推荐)
// 支持复数、选择器、日期、数字处理

// 基础用法
"greeting": "Hello, {name}!"

// 复数处理
"items_count": "{count, plural, =0 {No items} one {# item} other {# items}}"

// 选择器(性别等)
"notification": "{gender, select, male {He} female {She} other {They}} liked your post"

// 嵌套用法
"cart": "{itemCount, plural,
  =0 {Your cart is empty}
  one {You have # item in your cart}
  other {You have # items in your cart}
}"

Pluralization

复数规则

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

不同语言的复数规则不同:

英语:1(单数),2+(复数)
法语:0-1(单数),2+(复数)
俄语:1、2-4、5-20、21等复杂规则
阿拉伯语:6种复数形式!
中文:无复数形式

始终使用库处理复数,不要手动实现:
- react-intl / formatjs
- i18next
- Intl.PluralRules API

Date & Time

日期与时间

JavaScript Intl API

JavaScript Intl API

typescript
// 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"
typescript
// 日期格式化
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"

// 自定义选项
new Intl.DateTimeFormat("en-US", {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric",
}).format(date);
// "Friday, March 15, 2024"

Time Zones

时区处理

typescript
// 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;
typescript
// 始终以UTC格式存储
const utcDate = new Date().toISOString();
// "2024-03-15T14:30:00.000Z"

// 以用户时区展示
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"

// 获取用户时区
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;

Relative Time

相对时间

typescript
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"

typescript
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"

// 自动选择时间单位,可使用date-fns等库
import { formatDistanceToNow } from "date-fns";
formatDistanceToNow(date, { addSuffix: true });
// "3 days ago"

Numbers & Currency

数字与货币

Number Formatting

数字格式化

typescript
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%"
typescript
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"

// 百分比格式化
new Intl.NumberFormat("en-US", {
  style: "percent",
  minimumFractionDigits: 1,
}).format(0.256);
// "25.6%"

Currency Formatting

货币格式化

typescript
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)
typescript
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" (日元无小数位)

Currency Best Practices

货币最佳实践

typescript
// 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

typescript
// 存储金额+货币代码
interface Money {
  amount: number; // 最小单位(如分)
  currency: string; // ISO 4217代码(USD, EUR等)
}

// 格式化展示
function formatMoney(money: Money, locale: string): string {
  return new Intl.NumberFormat(locale, {
    style: "currency",
    currency: money.currency,
  }).format(money.amount / 100); // 将分转换为元
}

// 后端处理汇率转换
// 绝不要在客户端转换货币

RTL (Right-to-Left) Support

RTL(从右到左)布局支持

RTL Languages

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语言:
- 阿拉伯语 (ar)
- 希伯来语 (he)
- 波斯语 (fa)
- 乌尔都语 (ur)

布局注意事项:
┌─────────────────────────────┐  ┌─────────────────────────────┐
│ Logo      Menu    Settings  │  │ Settings    Menu      Logo  │
│ ← Back                      │  │                      Back → │
│ [Icon] Label                │  │                Label [Icon] │
│                      Next → │  │ ← Next                      │
└─────────────────────────────┘  └─────────────────────────────┘
        LTR布局                        RTL布局

CSS for RTL

RTL相关CSS

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 */
}
css
/* 使用逻辑属性 */
/* 错误示例:物理属性 */
.element {
  margin-left: 10px;
  padding-right: 20px;
  text-align: left;
  float: left;
}

/* 正确示例:逻辑属性 */
.element {
  margin-inline-start: 10px;
  padding-inline-end: 20px;
  text-align: start;
  float: inline-start;
}

/* 根据区域设置排版方向 */
html[dir="rtl"] {
  direction: rtl;
}

/* 或使用:dir()伪类 */
.icon:dir(rtl) {
  transform: scaleX(-1); /* 镜像图标 */
}

/* Flexbox在RTL下自动反转 */
.container {
  display: flex;
  /* 无需为RTL修改 */
}

HTML Direction

HTML方向设置

html
<!-- 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>

html
<!-- 文档级设置 -->
<html lang="ar" dir="rtl">
  <!-- 动态设置 -->
  <div dir="auto">
    <!-- 自动检测文本方向 -->
    مرحبا بالعالم
  </div>

  <!-- 隔离双向文本 -->
  <p>The word <bdi>مرحبا</bdi> means "hello".</p>
</html>

Implementation (React)

React实现方案

react-intl Setup

react-intl配置

typescript
// 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>
  );
}
typescript
// 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>
  );
}

Using Translations

翻译使用示例

tsx
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>
  );
}
tsx
import { FormattedMessage, useIntl } from "react-intl";

function ProductCard({ product }) {
  const intl = useIntl();

  return (
    <div>
      {/* 组件式用法 */}
      <h2>
        <FormattedMessage id="product.title" values={{ name: product.name }} />
      </h2>

      {/* Hook式用法(适用于属性等场景) */}
      <img
        src={product.image}
        alt={intl.formatMessage({ id: "product.image_alt" })}
      />

      {/* 数字格式化 */}
      <p>
        <FormattedNumber
          value={product.price}
          style="currency"
          currency="USD"
        />
      </p>

      {/* 日期格式化 */}
      <p>
        <FormattedDate value={product.createdAt} dateStyle="medium" />
      </p>

      {/* 复数处理 */}
      <p>
        <FormattedMessage
          id="product.reviews"
          values={{ count: product.reviewCount }}
        />
      </p>
    </div>
  );
}

Translation Files

翻译文件示例

json
// 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"
}

json
// 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"
}

Implementation (Node.js)

Node.js实现方案

i18next Setup

i18next配置

typescript
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");
typescript
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,
  },
});

// 使用示例
const t = i18next.t;

t("welcome", { name: "John" });
// "Welcome, John!"

// 切换语言
await i18next.changeLanguage("es");

Express Middleware

Express中间件

typescript
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 }),
  });
});

typescript
import { Request, Response, NextFunction } from "express";

function detectLocale(req: Request, res: Response, next: NextFunction) {
  // 优先级:查询参数 > Cookie > Accept-Language请求头 > 默认值
  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 }),
  });
});

Translation Workflow

翻译工作流

File Structure

文件结构

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
locales/
├── en/
│   ├── common.json       # 通用字符串
│   ├── auth.json         # 认证相关
│   ├── products.json     # 产品相关
│   └── errors.json       # 错误提示
├── es/
│   └── ...
├── fr/
│   └── ...
└── _source/
    └── en.json           # 基准源文件

Key Naming Convention

键命名规范

json
{
  // 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
}
json
{
  // 功能.元素.描述
  "auth.login.button": "Sign In",
  "auth.login.error.invalid_credentials": "Invalid email or password",

  // 或带前缀的扁平化命名
  "btn_login": "Sign In",
  "err_invalid_credentials": "Invalid email or password"

  // 保持一致、描述性、唯一性
}

Translation Management

翻译管理工具

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

工具推荐:
- Lokalise, Crowdin, Phrase(SaaS平台)
- Weblate(开源)
- POEditor, Transifex

工作流:
1. 从代码中提取字符串
2. 上传至翻译平台
3. 译者在上下文环境中翻译
4. 审核翻译内容
5. 下载并集成到项目
6. 在应用中测试
7. 内容变更时重复流程

自动化:
- CI/CD集成
- 自动提取字符串
- 翻译记忆库
- 机器翻译+人工审核

Testing

测试

Pseudo-Localization

伪本地化测试

typescript
// 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}]`;
}
typescript
// 转换示例: "Hello" → "[Ħëľľö!!!]"
// 用于检测:
// - 硬编码字符串
// 文本截断问题
// - 字符编码问题
// - 长文本布局问题

function pseudoLocalize(text: string): string {
  const charMap: Record<string, string> = {
    a: "ä",
    b: "β",
    c: "ç",
    d: "δ",
    e: "ë",
    // ... 其他字符映射
  };

  const transformed = text
    .split("")
    .map((c) => charMap[c.toLowerCase()] || c)
    .join("");

  // 添加填充(大部分语言翻译后文本会变长30%)
  const padding = "!".repeat(Math.ceil(text.length * 0.3));

  return `[${transformed}${padding}]`;
}

Locale Testing Checklist

区域设置测试清单

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

针对每个区域设置:
□ 所有字符串已翻译
□ 无硬编码文本显示
□ 日期格式正确
□ 数字格式正确
□ 货币展示正常
□ 复数处理正确
□ UI适配文本长度
□ RTL布局正确(若适用)
□ 图片符合文化场景
□ 无编码问题
□ 表单验证逻辑适配

Best Practices

最佳实践

DO:

建议做法:

  • Externalize ALL user-facing strings
  • Use ICU message format for complex strings
  • Support locale switching without reload
  • Test with pseudo-localization
  • Store dates in UTC, display in local time
  • Use native Intl APIs when possible
  • Plan for text expansion (30-50% longer)
  • Support RTL from the start
  • 外部化所有用户可见字符串
  • 复杂字符串使用ICU消息格式
  • 支持无刷新切换语言
  • 使用伪本地化测试
  • 以UTC存储日期,以用户时区展示
  • 尽可能使用原生Intl API
  • 提前规划文本扩展(30-50%变长)
  • 从项目初期就支持RTL布局

DON'T:

避免做法:

  • Hardcode any user-facing text
  • Concatenate strings for messages
  • Assume date/number formats
  • Handle plurals manually
  • Forget about cultural context
  • Translate in code (use translation files)
  • Ignore bidirectional text issues
  • Skip testing with real languages

  • 硬编码任何用户可见文本
  • 拼接字符串生成提示信息
  • 假设日期/数字格式统一
  • 手动处理复数规则
  • 忽略文化背景差异
  • 在代码中直接翻译(使用翻译文件)
  • 忽略双向文本问题
  • 跳过真实语言测试

Checklist

检查清单

Initial Setup

初始配置

  • Choose i18n library
  • Set up translation file structure
  • Configure locale detection
  • Add locale switcher UI
  • Set up RTL support
  • 选择i18n库
  • 搭建翻译文件结构
  • 配置区域设置检测
  • 添加语言切换UI
  • 配置RTL支持

Per Feature

功能开发阶段

  • Extract all strings to translation files
  • Use message format for variables
  • Handle plurals correctly
  • Format dates/numbers/currency
  • Test with pseudo-localization
  • Review text in context
  • 提取所有字符串到翻译文件
  • 变量使用消息格式
  • 正确处理复数
  • 格式化日期/数字/货币
  • 伪本地化测试
  • 上下文环境中审核文本

Launch

发布阶段

  • Professional translations complete
  • All locales tested
  • RTL layouts verified
  • SEO for multi-language (hreflang)
  • Locale-specific content reviewed
  • 专业翻译完成
  • 所有区域设置测试通过
  • RTL布局验证完成
  • 多语言SEO配置(hreflang)
  • 区域特定内容审核通过