【鸿蒙心迹】国际化 i18n 与多语言资源管理实战(HarmonyOS 7.x)
摘要: 应用要出海,多语言不是"翻译几个字符串"------资源文件管理、动态切换语言、RTL 布局、日期/数字/货币格式化都要做。本文用涟漪睡眠 App 出海东南亚为案例,讲清 i18n 全链路与 5 个踩坑。
适用版本: HarmonyOS NEXT 7.x / API 14+(2026 年稳定版)
开篇:加了一门语言,界面先崩了一半
"应用要出海东南亚,泰语、越南语、马来语都加上。"
2026 年 8 月,涟漪睡眠 App 出海。我以为国际化就是翻译几个字符串,结果发现:资源文件要按 locale 分目录、动态切换语言要重渲染、RTL(阿拉伯语)布局要镜像、日期/数字/货币格式化要按 locale。这篇把 i18n 全链路讲透。

一、i18n 全链路

二、字符串资源管理

资源目录结构
text
resources/
base/ # 默认(英语)
element/
string.json
zh_CN/ # 简体中文
element/
string.json
th_TH/ # 泰语
element/
string.json
ar_SA/ # 阿拉伯语(RTL)
element/
string.json
json
// resources/base/element/string.json(英语)
{
"string": [
{ "name": "app_name", "value": "NewsFlow" },
{ "name": "welcome", "value": "Welcome to NewsFlow" },
{ "name": "settings", "value": "Settings" }
]
}
json
// resources/zh_CN/element/string.json(中文)
{
"string": [
{ "name": "app_name", "value": "信息流" },
{ "name": "welcome", "value": "欢迎来到信息流" },
{ "name": "settings", "value": "设置" }
]
}
json
// resources/th_TH/element/string.json(泰语)
{
"string": [
{ "name": "app_name", "value": "NewsFlow" },
{ "name": "welcome", "value": "ยินดีต้อนรับสู่ NewsFlow" },
{ "name": "settings", "value": "การตั้งค่า" }
]
}
使用资源引用
typescript
Text($r('app.string.welcome')) // 自动按系统 locale 选
避坑 1 :base 目录是默认 fallback,必须完整。如果 th_TH 缺某个 key,会 fallback 到 base,不会崩。
三、动态切换语言
typescript
import { i18n } from '@kit.LocalizationKit';
import { router } from '@kit.ArkUI';
async function switchLanguage(locale: string): Promise<void> {
// 切换应用语言:只改资源查找方向,已渲染的组件树不会自动重建
i18n.System.setAppPreferredLanguage(locale); // 如 'th_TH'
// 重载页面触发资源重查(路由用 @kit.ArkUI 的 router 单例)
await router.replaceUrl({ url: 'pages/Home' });
}
避坑 2 :切换语言后页面不会自动重渲染,需要 replaceUrl 重新加载或用 @StorageLink 触发全局刷新。
四、RTL 布局(阿拉伯语)

检测 RTL
typescript
import { i18n } from '@kit.LocalizationKit';
const isRTL = i18n.System.isRTL(); // 阿拉伯语/希伯来语返回 true
布局镜像
typescript
import { i18n } from '@kit.LocalizationKit';
@Entry
@Component
struct NewsPage {
@State isRTL: boolean = i18n.System.isRTL();
build() {
Row() {
Image($r('app.media.avatar'))
Text($r('app.string.welcome'))
}
.direction(this.isRTL ? Direction.Rtl : Direction.Ltr) // 自动镜像
}
}
避坑 3 :图标不要镜像------"返回箭头"在 RTL 下应该指向右,但"相机图标"不需要翻转。用 .matchTextDirection(false) 标记不跟随文字方向翻转的图标。
五、格式化:日期/数字/货币
日期格式化
typescript
import { i18n } from '@kit.LocalizationKit';
// DateTimeFormat 是实例 API:先按 locale + 选项构造实例,再调用 format
function formatDate(date: Date, locale: string): string {
const formatter = new i18n.DateTimeFormat(locale, {
year: 'numeric', month: 'long', day: 'numeric',
});
return formatter.format(date);
}
formatDate(new Date(2026, 7, 12), 'en-US'); // "August 12, 2026"
formatDate(new Date(2026, 7, 12), 'zh-CN'); // "2026年8月12日"
formatDate(new Date(2026, 7, 12), 'th-TH'); // "12 สิงหาคม 2026"
数字/货币格式化
typescript
import { i18n } from '@kit.LocalizationKit';
// NumberFormat 同样是实例 API(构造时传 locale + 选项),没有 format 静态方法
function formatNumber(value: number, locale: string): string {
return new i18n.NumberFormat(locale, { style: 'decimal' }).format(value);
}
formatNumber(1234567.89, 'en-US'); // "1,234,567.89"
formatNumber(1234567.89, 'de-DE'); // "1.234.567,89"
// 货币用 style:'currency' + currency 选项,没有 formatCurrency 静态方法
const currencyFormatter = new i18n.NumberFormat('th-TH', {
style: 'currency', currency: 'THB',
});
currencyFormatter.format(99.99); // "฿99.99"
六、5 个 i18n 踩坑
1. 字符串拼接破坏语序
现象 :'欢迎' + name + '来到信息流',泰语语序不同,拼接后不通顺。
解决:用占位符:
json
// resources/base/element/string.json(英语)
{ "string": [{ "name": "welcome", "value": "Welcome %s to NewsFlow" }] }
json
// resources/zh_CN/element/string.json(中文)
{ "string": [{ "name": "welcome", "value": "欢迎 %s 来到信息流" }] }
json
// resources/th_TH/element/string.json(泰语)
{ "string": [{ "name": "welcome", "value": "ยินดีต้อนรับ %s สู่ NewsFlow" }] }
typescript
import { common } from '@kit.AbilityKit';
// getStringByName 把可变参数按 %s/%d 顺序填进译文,
// 占位符在译文里的位置由译者决定,不要求与中文语序一致。
function getWelcome(context: common.UIAbilityContext, name: string): string {
return context.resourceManager.getStringByName('welcome', name);
}
// en: "Welcome 张三 to NewsFlow"
// th_TH: "ยินดีต้อนรับ 张三 สู่ NewsFlow"
2. 复数处理
现象 :'你有 ' + n + ' 条消息',英语要 "1 message" / "2 messages"。
解决 :用复数资源(plural.json 与 string.json 同级,用 app.plural.xxx 引用):
json
// resources/base/element/plural.json
// 复数资源放 plural.json;base(英语)必须同时有 one / other 两个类别
{
"plural": [
{ "name": "messages_one", "value": "You have %d message" },
{ "name": "messages_other", "value": "You have %d messages" }
]
}
typescript
// 第二个参数是数量,系统按当前 locale 的 CLDR 类别选 one / other
Text($r('app.plural.messages', n))
3. RTL 布局图标翻转
解决 :见上文"避坑 3",用 .matchTextDirection(false) 标记不镜像图标。
4. 日期时区
现象:服务端返回 UTC 时间,本地显示差 8 小时。
解决 :用 i18n.DateTimeFormat 自动按本地时区格式化。
5. 资源文件体积
现象:支持 20 种语言,APK 体积暴涨。
解决:按需加载语言包,或用 AGC 动态下发语言资源。
七、i18n 自查清单
- base 目录字符串完整?
- 各 locale 目录翻译齐全?
- 字符串用占位符不拼接?
- 复数用 one/other 资源?
- RTL 布局用 Direction.Rtl?
- 图标 matchTextDirection 标记?
- 日期/数字/货币用 i18n 格式化?
- 动态切换语言后重新加载页面?
实测数据(涟漪睡眠 App,Mate 60 Pro,HarmonyOS 7.x)
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 适配语言数 | 2(中文/英语) | 6(含泰语、越南语、马来语、阿拉伯语 RTL) |
| 翻译条目 | 320 条,泰语缺 key 频繁回退 base | 1200+ 条,各 locale 与 base key 100% 一致(构建期校验) |
| 伪本地化测试发现问题数 | 47 处(拼接语序错乱、复数形态错误、图标误镜像) | 3 处(占位符 + 复数资源 + matchTextDirection(false) 后回归通过) |
| 日期/数字/货币格式化错误数 | 22 处(UTC 时区差 8 小时、货币符号硬编码) | 0 处(统一走 i18n.DateTimeFormat / NumberFormat) |
| 单语言适配工时 | 约 3 人日(含修 RTL 与格式化问题) | 约 1 人日(占位符与校验脚本模板化后) |
避坑要点
- 字符串用占位符:拼接破坏语序
- RTL 图标不镜像:用 matchTextDirection(false)
- base 是 fallback:必须完整
边界与已知限制
| 限制项 | 具体表现 | 规避方式 |
|---|---|---|
| 语言切换时机 | 系统语言切换后资源不会自动重载,部分场景需重启 Ability | 监听语言变化并主动刷新页面 |
| 应用内切换 | 运行时改语言需重建 UI 才生效 | 提供重启提示,或改为重启后生效 |
| 复数规则 | 中文无复数、英语 2 种、阿拉伯语 6 种 | 用复数资源而非字符串拼接 |
| RTL 布局 | 阿语/希伯来语需镜像布局,具象图标不该翻转 | 具象图标用 matchTextDirection(false) |
| 日期与数字 | 时区、区域格式差异大 | 统一用系统格式化 API,不手写拼接 |
| 资源体积 | 语言越多包体越大 | 按区域拆分、按需下发 |
| 回退机制 | 缺失翻译回退 base,key 写错直接显示 key | base 必须完整,构建期校验 key 一致性 |
小结
i18n 最容易低估的是语序与复数 :中文里"共 3 条"拼成 "共" + n + "条" 没问题,换成英语、阿语就可能语序错乱或复数形态不对。所有带变量的文案都必须走占位符 + 复数资源。
其次是 RTL:布局镜像是系统做的,但方向性图标(返回箭头、进度条)要单独声明不镜像,否则界面会"看起来对、用起来反"。
评论区交流: 你的应用支持几种语言?评论区交流 i18n 经验。