Skip to content

08 - 国际化

概述

项目使用 i18next + react-i18next 实现国际化,支持四种语言:

语言码语言文件大小说明
zhcn简体中文~170KB默认语言
zhtw繁体中文~170KB-
enEnglish~176KB-
arالعربية~226KBRTL 从右到左

目录结构

src/i18n/
├── index.js          # 对外 API 入口
├── instance.js       # i18next 单例配置与资源加载
├── config-t.js       # 编译期翻译表(供 app.config.js 等 Node 环境用)

src/subpages/i18n/    # i18n 语言包分包
├── index.js          # 分包占位页
├── resources.js      # 资源汇总入口
└── locales/
    ├── zhcn.json     # 简体中文
    ├── zhtw.json     # 繁体中文
    ├── en.json       # 英文
    └── ar.json       # 阿拉伯语

语言码映射

存储/Redux ↔ i18next

js
// 存储/Redux 语言码 → i18next lng
export const STORAGE_TO_I18N = {
  zhcn: 'zh-CN',
  zhtw: 'zh-TW',
  en: 'en',
  ar: 'ar'
}

// i18next lng → 存储语言码
export const I18N_TO_STORAGE = {
  'zh-CN': 'zhcn',
  'zh-TW': 'zhtw',
  en: 'en',
  ar: 'ar'
}

export const SUPPORTED_STORAGE_LANGS = ['zhcn', 'zhtw', 'en', 'ar']

语言别名归一化

支持多种输入格式的自动识别:

js
const STORAGE_LANG_ALIASES = {
  'zh-cn': 'zhcn', 'zh_cn': 'zhcn', zh: 'zhcn', zhcn: 'zhcn',
  zhtw: 'zhtw', 'zh-tw': 'zhtw', 'zh_tw': 'zhtw', tw: 'zhtw',
  en: 'en', 'en-us': 'en', 'en_cn': 'en',
  ar: 'ar', 'ar-sa': 'ar'
}

i18next 初始化

js
if (!i18n.isInitialized) {
  i18n.use(initReactI18next).init({
    resources: {},                          // 初始为空,异步加载
    lng: resolveInitialLng(),              // 从 Storage 或 env 读取
    fallbackLng: ['zh-CN'],
    interpolation: { escapeValue: false },
    react: { useSuspense: false },         // 不使用 Suspense
    returnNull: false
  })
}

初始语言解析

优先级:

  1. Taro Storage 的 lang 字段
  2. 环境变量 APP_DEFAULT_LANGUAGE
  3. 默认 en

双模式资源加载

微信小程序:分包异步加载

语言包放在 subpages/i18n/ 分包中,通过 __non_webpack_require__ 异步加载,避免主包膨胀:

js
function loadLocalePackage() {
  if (process.env.TARO_ENV === 'weapp') {
    return new Promise((resolve, reject) => {
      __non_webpack_require__('subpages/i18n/resources', () => {
        const resources = __non_webpack_require__('subpages/i18n/resources')
        Taro.__i18nResources = { ...(Taro.__i18nResources || {}), ...resources }
        resolve()
      })
    })
  }
  // ...
}

预下载策略:在 app.config.js 中配置 preloadRule,首页加载时预下载 i18n 分包:

js
config.preloadRule = {
  'pages/index': { network: 'all', packages: ['subpages/i18n'] },
  'pages/purchase/index': { network: 'all', packages: ['subpages/i18n'] }
}

H5:同步加载

语言包由 webpack 打进主包,同步 require 加载:

js
if (process.env.TARO_ENV === 'h5') {
  const resources = require('../subpages/i18n/resources')
  Taro.__i18nResources = { ...(Taro.__i18nResources || {}), ...resources }
  return Promise.resolve()
}

构建配置:资源转换

微信小程序环境下,构建时将 JSON 语言文件转换为 CommonJS 模块(config/index.js copy patterns):

js
// resources.js 中的 .json 引用替换为 .js
{
  from: 'src/subpages/i18n/resources.js',
  to: `subpages/i18n/resources.js`,
  transform: i18nResourceTransform
  // 替换: ./locales/zhcn.json → ./locales/zhcn.js
}

// 每个语言 JSON 转换为 module.exports 格式
{
  from: `src/subpages/i18n/locales/${lang}.json`,
  to: `subpages/i18n/locales/${lang}.js`,
  transform: i18nLocaleTransform
  // 转换: { "key": "value" } → module.exports = { "key": "value" }
}

语言同步

js
export async function syncI18nLanguage(storageLang) {
  const normalizedLang = normalizeStorageLang(storageLang)
  const lng = await loadI18nResource(normalizedLang)

  if (i18n.language === lng) {
    // 资源刚加载但语言未变,手动触发 languageChanged 事件
    if (!hadResourceBundle) i18n.emit('languageChanged', lng)
    return
  }
  await i18n.changeLanguage(lng)
}

对外 API

src/i18n/index.js

js
import i18n, { syncI18nLanguage, normalizeStorageLang, ... } from './instance'

export { i18n, syncI18nLanguage, isI18nResourceReady, normalizeStorageLang,
  STORAGE_TO_I18N, I18N_TO_STORAGE, SUPPORTED_STORAGE_LANGS }
export { useTranslation, Trans } from 'react-i18next'

// 获取当前语言码(zhcn | zhtw | en | ar)
export function getLocale() { ... }

// 取文案
export function $t(key) {
  if (!i18n.exists(key)) return ''
  return i18n.t(key)
}

// 带占位符的翻译:${0}, ${1}... 与 args 数组对应
export function ti(key, args) {
  const raw = $t(key)
  return raw.replace(/\$\{(\d+)\}/g, (match, index) => {
    return args[index] !== undefined ? String(args[index]) : match
  })
}

使用方式

js
// 在组件中使用
import { $t, ti, useTranslation, getLocale } from '@/i18n'

// 函数组件
const { t } = useTranslation()
<View>{t('95285d68.93f311')}</View>

// 直接取文案
const text = $t('95285d68.93f311')

// 带参数
const msg = ti('cart.total_count', [3])
// 假设原文: "共${0}件" → "共3件"

翻译键格式

项目使用哈希风格的键名(非语义化键名):

'95285d68.93f311'  →  "您的位置信息将用于定位附近门店"
'95285d68.0ed510'  →  "小程序"
'3b69f96e.361c28'  →  "购物车"

格式为 <8位hash>.<6位hash>,由工具自动生成,便于管理和避免冲突。

编译期翻译(config-t.js)

src/i18n/config-t.jsapp.config.js 等 Node 编译期执行的模块使用,不依赖 i18next/Taro:

js
const TABLES = {
  zhcn: { '95285d68.93f311': '您的位置信息将用于定位附近门店', ... },
  zhtw: { '95285d68.93f311': '您的位置資訊將用於定位附近門店', ... },
  en:   { '95285d68.93f311': 'Your location is used to find nearby stores', ... },
  ar:   { '95285d68.93f311': 'يُستخدم موقعك للعثور على المتاجر القريبة', ... }
}

export function $t(key) {
  const table = resolveTable()
  return table[key] || String(key)
}

用于 app.config.js 中需要翻译的静态文案(如权限描述、导航栏标题)。

RTL 支持

CSS 层面

src/style/rtl.scss 提供完整的 RTL 布局支持:

scss
.rtl-layout, [dir='rtl'] {
  direction: rtl;
  text-align: right;

  .text-left { text-align: right; }
  .text-right { text-align: left; }
  .float-left, .pull-left { float: right; }

  // 箭头图标翻转 180 度
  .at-icon-chevron-right { transform: rotate(180deg); }
}

Mixins 中的 RTL 自动生成

scss
// generate-margin 中自动生成 RTL 反转规则
.rtl-layout {
  .ml-10 { margin-left: 0; margin-right: 10px; }
  .mr-10 { margin-right: 0; margin-left: 10px; }
}

AtTabs RTL 修复

scss
// direction: rtl 会影响 transform: translateX() 方向
// 解决方案:容器保持 LTR,内容区域保持 RTL
.rtl-layout .at-tabs {
  direction: ltr !important;
  .at-tabs__body { direction: ltr !important; }
  .at-tabs-pane { direction: rtl !important; }
}

使用 RTL

当语言切换到阿拉伯语时,在页面根节点添加 dir="rtl"className="rtl-layout"

jsx
import { getLocale } from '@/i18n'

function App({ children }) {
  const isRTL = getLocale() === 'ar'
  return <View dir={isRTL ? 'rtl' : 'ltr'}>{children}</View>
}

语言切换

js
import { syncI18nLanguage } from '@/i18n'
import { updateLang } from '@/store/slices/user'

// 切换语言
await syncI18nLanguage('en')  // 加载资源 + 切换 i18next 语言
dispatch(updateLang('en'))    // 更新 Redux 状态 + 持久化

新增翻译文案

1. 生成翻译键

使用项目工具或手动生成 <8位hash>.<6位hash> 格式的键名。

2. 添加到语言文件

json
// src/subpages/i18n/locales/zhcn.json
{ "a1b2c3d4.e5f6a7": "加入购物车" }

// src/subpages/i18n/locales/en.json
{ "a1b2c3d4.e5f6a7": "Add to Cart" }

3. 在代码中使用

js
import { $t } from '@/i18n'
const text = $t('a1b2c3d4.e5f6a7')

4. 如需在 app.config.js 中使用

还需要添加到 config-t.js

js
// src/i18n/config-t.js
const TABLES = {
  zhcn: { ..., "a1b2c3d4.e5f6a7": "加入购物车" },
  en:   { ..., "a1b2c3d4.e5f6a7": "Add to Cart" }
}