08 - 国际化
概述
项目使用 i18next + react-i18next 实现国际化,支持四种语言:
| 语言码 | 语言 | 文件大小 | 说明 |
|---|---|---|---|
zhcn | 简体中文 | ~170KB | 默认语言 |
zhtw | 繁体中文 | ~170KB | - |
en | English | ~176KB | - |
ar | العربية | ~226KB | RTL 从右到左 |
目录结构
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
})
}初始语言解析
优先级:
- Taro Storage 的
lang字段 - 环境变量
APP_DEFAULT_LANGUAGE - 默认
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.js 供 app.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" }
}