一、总体实施流程

  1. 评估现状

    • 识别硬编码文本

    • 确定目标 locale 列表

    • 分析 SSR/SPA 架构

    • 检查微前端架构

  2. 设计资源组织与 key 规范

    • 按模块/页面拆分

    • 制定 key 命名规范

    • ICU 语法标准化

  3. 搭建提取与校验流程

    • 提取 → 上传 TMS → 导入 → CI 校验

  4. 代码引入与初始化

    • React/Vue/Next.js 初始化

    • 实现按需加载与缓存

  5. 测试与监控

    • 单元测试、E2E、视觉回归

    • 缺失 key、ICU 错误监控

  6. 逐步迁移

    • 按页面/路由推进

    • 灰度发布策略

二、提取硬编码字符串(自动化)

基于 Babel AST 的提取脚本:

javascript

// tools/extract-i18n.js
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const glob = require('glob');

function extractFromFile(file) {
  const code = fs.readFileSync(file, 'utf8');
  const ast = parser.parse(code, {
    sourceType: 'module',
    plugins: ['jsx', 'typescript', 'classProperties', 'optionalChaining']
  });
  
  const results = [];
  traverse(ast, {
    JSXText(path) {
      const text = path.node.value.trim();
      if (text) results.push(text);
    },
    StringLiteral(path) {
      const val = path.node.value;
      if (val && val.length > 1 && !/^(https?:\/\/|\/|\.\/)/.test(val)) {
        const p = path.parent;
        if (p && p.type === 'CallExpression' && p.callee && p.callee.name === 't') {
          // skip keyed t('xxx')
        } else {
          results.push(val);
        }
      }
    },
    TemplateLiteral(path) {
      if (path.node.quasis && path.node.quasis.length) {
        path.node.quasis.forEach(q => {
          const v = q.value && q.value.cooked && q.value.cooked.trim();
          if (v) results.push(v);
        });
      }
    }
  });
  return results;
}

const root = process.argv[2] || './src';
const files = glob.sync(`${root}/**/*.{js,jsx,ts,tsx}`);
const out = {};

files.forEach(f => {
  try {
    const arr = extractFromFile(f);
    if (arr && arr.length) out[f] = Array.from(new Set(arr));
  } catch (e) {
    console.warn('skip', f, e.message);
  }
});

console.log(JSON.stringify(out, null, 2));

运行方式:node tools/extract-i18n.js ./src > messages.json

三、消息格式与 key 设计(ICU)

Key 规范module.page.element(如 checkout.payment.title

消息格式示例

json

{
  "checkout.payment.title": {
    "message": "You will be charged {amount, number, USD} on {date, date, long}",
    "description": "Title shown on payment confirmation. amount is in USD, date is the scheduled date."
  },
  "notifications.count": {
    "message": "{count, plural, =0 {You have no notifications} one {You have one notification} other {You have # notifications}}",
    "description": "Notification count summary"
  }
}

四、前端 i18n 初始化

1. React (i18next + react-i18next)

javascript

// src/i18n.js
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

const defaultNS = 'common';

async function loadLocale(locale) {
  if (locale === 'en') {
    return import('../locales/en/common.json');
  }
  return import(/* webpackChunkName: "locale-[request]" */ `../locales/${locale}/common.json`);
}

i18n.use(initReactI18next).init({
  lng: detectLocaleFromBrowserOrCookie(),
  fallbackLng: 'en',
  ns: [defaultNS],
  defaultNS,
  resources: { en: { common: require('../locales/en/common.json') } },
  interpolation: { escapeValue: false },
  react: { useSuspense: true }
});

export async function changeLocale(locale) {
  if (!i18n.hasResourceBundle(locale, defaultNS)) {
    const messages = (await loadLocale(locale)).default;
    i18n.addResourceBundle(locale, defaultNS, messages, true, true);
  }
  await i18n.changeLanguage(locale);
}

export default i18n;

2. Vue 3 (vue-i18n)

javascript

import { createI18n } from 'vue-i18n';
import en from '../locales/en/common.json';

export const i18n = createI18n({
  legacy: false,
  locale: 'en',
  fallbackLocale: 'en',
  messages: { en }
});

export async function changeLocale(locale) {
  if (!i18n.global.availableLocales.includes(locale)) {
    const msgs = (await import(`../locales/${locale}/common.json`)).default;
    i18n.global.setLocaleMessage(locale, msgs);
  }
  i18n.global.locale.value = locale;
}

3. Next.js (SSR/SSG)

javascript

// pages/index.js
export async function getStaticProps({ locale }) {
  const messages = await import(`../locales/${locale}/common.json`);
  return { props: { initialI18n: messages.default } };
}

五、构建与打包优化

  • 配置 webpack/rollup/vite 的 code-splitting

  • 动态 import 语言包,chunkName 带 locale

  • 生产环境开启 gzip/brotli 压缩

  • SSR/SSG 为每个 locale 生成独立页面

  • 语言包添加版本哈希便于缓存管理

六、Service Worker 缓存策略

javascript

// sw.js
const CACHE_NAME = 'i18n-v1';

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll([
        '/locales/en/common.json',
        '/locales/zh/common.json'
      ]);
    })
  );
});

self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('/locales/')) {
    event.respondWith(
      caches.match(event.request).then((response) => {
        return response || fetch(event.request);
      })
    );
  }
});

七、CI 校验与自动化

package.json 脚本

json

{
  "scripts": {
    "i18n:extract": "node tools/extract-i18n.js ./src > i18n_messages.json",
    "i18n:lint": "node tools/i18n-lint.js ./locales",
    "i18n:check": "npm run i18n:extract && npm run i18n:lint"
  }
}

GitHub Actions 示例

yaml

name: i18n Check
on: [pull_request]
jobs:
  i18n:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Use Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'
      - run: npm ci
      - run: npm run i18n:check

八、测试策略

单元测试 (Jest)

javascript

import messages_en from '../locales/en/common.json';
import messages_zh from '../locales/zh/common.json';

test('all keys present in zh', () => {
  expect(Object.keys(messages_zh)).toEqual(
    expect.arrayContaining(Object.keys(messages_en))
  );
});

E2E 测试 (Playwright)

javascript

// test/i18n.spec.ts
test('rtl layout for Arabic', async ({ page }) => {
  await page.goto('/?lang=ar');
  await expect(page.locator('html')).toHaveAttribute('dir', 'rtl');
  await expect(page.locator('nav')).toHaveScreenshot('nav-rtl.png');
});

九、监控与遥测

javascript

function t(key, vars) {
  try {
    return i18n.t(key, vars);
  } catch (e) {
    Sentry.captureException(e, {
      extra: { 
        key, 
        locale: i18n.language, 
        route: window.location.pathname 
      }
    });
    return `[MISSING:${key}]`;
  }
}

十、迁移策略

  1. 制定优先级页面列表

  2. 小范围引入 i18n 框架,灰度测试

  3. CI 阶段阻止新增硬编码文本

  4. 替换后立即运行 UI 自动化测试

  5. 设置语言包版本化回滚策略

十一、微前端协作

主应用通信

javascript

// 主应用
window.dispatchEvent(new CustomEvent('languageChange', { 
  detail: { locale: 'zh' } 
}));

// 子应用
window.addEventListener('languageChange', (e) => {
  changeLocale(e.detail.locale);
});

十二、RTL 特殊处理

HTML 设置

html

<html lang="ar" dir="rtl">

CSS 适配

css

.element {
  margin-inline-start: 10px; /* 替代 margin-left */
  margin-inline-end: 10px;   /* 替代 margin-right */
}

.rtl-icon {
  transform: scaleX(-1);
}

十三、翻译工作流自动化

  1. 提取 → 生成 XLIFF/CSV

  2. 上传翻译平台 (Crowdin/Lokalise)

  3. 翻译/审校 → 导回 JSON

  4. CI 校验 → 合并发布

十四、回滚与版本管理

  • 语言包版本号:i18n-20251030-1

  • CDN 目录版本控制

  • 配置中心控制语言包版本 URL

十五、AI 辅助翻译

  • 机器翻译生成初稿

  • LLM 检查上下文一致性

  • AI 自动生成翻译上下文

十六、验收检查清单

  • 默认语言无 FOUC

  • 语言切换立即生效

  • ICU 表达式通过校验

  • RTL 页面方向正确

  • CI 无新增未国际化文本

  • 监控面板记录正常

十七、工具清单

类别 推荐工具
AST 提取 @babel/parser, @babel/traverse
ICU 校验 @formatjs/icu-messageformat-parser
翻译平台 Lokalise, Crowdin, Transifex
i18n 库 react-i18next, vue-i18n, next-intl
缓存 Service Worker, IndexedDB
测试 Jest, Playwright, Percy
监控 Sentry, Datadog, Prometheus

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐