Skip to content

前端国际化方案 (i18n)

多语言应用开发的完整方案,从技术选型到工程化实践

目录


核心概念

i18n vs l10n

概念全称说明示例
i18nInternationalization国际化架构设计文本分离、占位符
l10nLocalization本地化内容翻译中文翻译、日期格式

国际化涉及的维度

┌─────────────────────────────────────────────────────────┐
│                     国际化内容                          │
├───────────────┬───────────────┬─────────────────────────┤
│    文本翻译    │    格式本地化   │       文化适配           │
├───────────────┼───────────────┼─────────────────────────┤
│ • 静态文案     │ • 日期时间     │ • 阅读方向 (RTL)        │
│ • 动态消息     │ • 数字货币     │ • 颜色含义              │
│ • 错误提示     │ • 排序规则     │ • 图标图片              │
│ • 复数形式     │ • 时区        │ • 法律合规              │
└───────────────┴───────────────┴─────────────────────────┘

技术方案对比

框架特点Bundle Size
react-i18nextReact功能全面、生态丰富~40KB
react-intl (FormatJS)ReactICU 标准、TypeScript 友好~50KB
vue-i18nVueVue 官方推荐~30KB
next-intlNext.jsApp Router 原生支持~15KB
Intl API原生零依赖、格式化专用0

React i18n 实践

react-i18next 配置

typescript
// i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';

import en from './locales/en.json';
import zh from './locales/zh.json';

i18n
  .use(LanguageDetector)   // 自动检测语言
  .use(initReactI18next)   // React 绑定
  .init({
    resources: {
      en: { translation: en },
      zh: { translation: zh }
    },
    fallbackLng: 'en',     // 回退语言
    interpolation: {
      escapeValue: false   // React 已做 XSS 防护
    }
  });

export default i18n;

翻译文件结构

json
// locales/en.json
{
  "common": {
    "confirm": "Confirm",
    "cancel": "Cancel"
  },
  "greeting": "Hello, {{name}}!",
  "items": "{{count}} item",
  "items_plural": "{{count}} items"
}

// locales/zh.json
{
  "common": {
    "confirm": "确认",
    "cancel": "取消"
  },
  "greeting": "你好,{{name}}!",
  "items": "{{count}} 个项目"
}

组件使用

tsx
import { useTranslation, Trans } from 'react-i18next';

function MyComponent() {
  const { t, i18n } = useTranslation();

  return (
    <div>
      {/* 基础翻译 */}
      <button>{t('common.confirm')}</button>
      
      {/* 插值 */}
      <p>{t('greeting', { name: 'John' })}</p>
      
      {/* 复数 */}
      <p>{t('items', { count: 5 })}</p>
      
      {/* 包含 JSX 的翻译 */}
      <Trans i18nKey="welcome">
        Welcome to <strong>our app</strong>
      </Trans>
      
      {/* 切换语言 */}
      <button onClick={() => i18n.changeLanguage('zh')}>
        中文
      </button>
    </div>
  );
}

动态加载翻译文件

typescript
// 按需加载,减少首屏体积
import i18n from 'i18next';
import Backend from 'i18next-http-backend';

i18n
  .use(Backend)
  .init({
    backend: {
      loadPath: '/locales/{{lng}}/{{ns}}.json'
    },
    ns: ['common', 'home', 'settings'],
    defaultNS: 'common'
  });

Intl API

日期格式化

javascript
const date = new Date();

// 基础格式化
new Intl.DateTimeFormat('zh-CN').format(date);
// "2024/3/15"

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

// 相对时间
const rtf = new Intl.RelativeTimeFormat('zh', { numeric: 'auto' });
rtf.format(-1, 'day');   // "昨天"
rtf.format(2, 'hour');   // "2小时后"

数字与货币

javascript
// 数字格式化
new Intl.NumberFormat('de-DE').format(1234567.89);
// "1.234.567,89"

// 货币格式化
new Intl.NumberFormat('ja-JP', {
  style: 'currency',
  currency: 'JPY'
}).format(1000);
// "¥1,000"

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

// 紧凑表示
new Intl.NumberFormat('en', {
  notation: 'compact',
  compactDisplay: 'short'
}).format(1500000);
// "1.5M"

复数规则

javascript
const pr = new Intl.PluralRules('en');
pr.select(0);  // "other"
pr.select(1);  // "one"
pr.select(2);  // "other"

const prRu = new Intl.PluralRules('ru');
prRu.select(1);   // "one"
prRu.select(2);   // "few"
prRu.select(5);   // "many"

列表格式化

javascript
const lf = new Intl.ListFormat('en', { 
  style: 'long', 
  type: 'conjunction' 
});

lf.format(['Apple', 'Banana', 'Orange']);
// "Apple, Banana, and Orange"

const lfZh = new Intl.ListFormat('zh', { type: 'disjunction' });
lfZh.format(['苹果', '香蕉', '橙子']);
// "苹果、香蕉或橙子"

工程化

翻译文件组织

src/
├── locales/
│   ├── en/
│   │   ├── common.json
│   │   ├── home.json
│   │   └── settings.json
│   └── zh/
│       ├── common.json
│       ├── home.json
│       └── settings.json

提取翻译 Key

bash
# 使用 i18next-parser 自动提取
npx i18next-parser 'src/**/*.{ts,tsx}'
javascript
// i18next-parser.config.js
module.exports = {
  locales: ['en', 'zh'],
  output: 'src/locales/$LOCALE/$NAMESPACE.json',
  input: ['src/**/*.{ts,tsx}'],
  defaultNamespace: 'common',
  keySeparator: '.',
  namespaceSeparator: ':'
};

TypeScript 类型安全

typescript
// i18n.d.ts
import 'i18next';
import en from './locales/en/common.json';

declare module 'i18next' {
  interface CustomTypeOptions {
    defaultNS: 'common';
    resources: {
      common: typeof en;
    };
  }
}

CI 检查

yaml
# .github/workflows/i18n-check.yml
name: i18n Check
on: [push, pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: npm ci
      - run: npm run i18n:extract
      - run: |
          if [ -n "$(git diff --name-only)" ]; then
            echo "Missing translations detected!"
            git diff
            exit 1
          fi

高频面试题

Q1: 如何处理复数形式?

ICU MessageFormat 语法

json
{
  "items": "{count, plural, =0 {No items} one {# item} other {# items}}"
}

不同语言复数规则不同(英语 2 种、俄语 4 种、阿拉伯语 6 种),使用 Intl.PluralRules 或库内置支持。

Q2: 如何实现 RTL (从右到左) 布局?

css
/* 使用逻辑属性 */
.container {
  /* 替代 padding-left */
  padding-inline-start: 16px;
  /* 替代 margin-right */
  margin-inline-end: 8px;
}

/* 根据 dir 属性切换 */
[dir="rtl"] .icon {
  transform: scaleX(-1);
}
html
<html dir="rtl" lang="ar">

Q3: 翻译文件过大如何优化?

  1. 按路由拆分:每个页面独立 namespace
  2. 动态加载i18next-http-backend 按需请求
  3. Tree-shaking:编译时移除未使用的 key
  4. CDN 缓存:翻译文件单独部署

Q4: 如何保证翻译完整性?

  1. 自动提取:CI/CD 中运行 i18next-parser
  2. 缺失检测:比较各语言文件的 key 一致性
  3. 类型检查:TypeScript 类型推断
  4. 翻译管理平台:Crowdin、Lokalise、Phrase

最佳实践

  • [ ] Key 使用语义化命名(form.validation.required
  • [ ] 避免拼接字符串,使用插值
  • [ ] 复数和性别使用 ICU MessageFormat
  • [ ] 日期/货币使用 Intl API
  • [ ] CI 中检查翻译完整性
  • [ ] 考虑 RTL 布局支持

前端面试知识库