前言
应用要出海,第一道坎就是多语言。HarmonyOS 内置 i18n 国际化框架——把所有文案抽到string.json,按语言放不同目录,代码用$r('app.string.xxx')引用,系统自动按设备语言加载。
本篇以「猫猫大作战」中英文切换为锚点,把string.json结构、zh_CN/en_US目录分流、$r引用与运行时切换三大要点讲透。
提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1–28 篇。本篇是布局进阶的第九篇。
一、场景拆解:中英文切换
回顾「猫猫大作战」主菜单文案(第 1 篇):
// 来源:entry/src/main/ets/pages/Index.ets MainMenuView() Text('猫猫大作战').fontSize(36).fontWeight(FontWeight.Bold).fontColor('#2C3E50') Text('合并进化 · 策略消除').fontSize(16).fontColor('#95A5A6') Button('开始游戏').width('70%').height(56)痛点:所有文案硬编码中文,海外用户看不懂。要支持英文,得把每个Text('中文')改成Text($r('app.string.xxx')),并准备两份string.json。
i18n 方案:
resources/ ├─ base/element/string.json # 默认(中文) │ { "string": [{ "name": "app_title", "value": "猫猫大作战" }] } ├─ zh_CN/element/string.json # 中文覆盖(与 base 相同) │ { "string": [{ "name": "app_title", "value": "猫猫大作战" }] } └─ en_US/element/string.json # 英文覆盖 { "string": [{ "name": "app_title", "value": "Cat Battle" }] }// 代码用 $r,系统按设备语言自动加载 Text($r('app.string.app_title')) // 设备中文 → 猫猫大作战 // 设备英文 → Cat Battle关键经验:i18n = 文案抽到 string.json + 目录按语言分流 + 代码用 $r 引用。
二、string.json 结构
2.1 基本结构
resources/base/element/string.json(默认中文):
{ "string": [ { "name": "app_title", "value": "猫猫大作战" }, { "name": "app_subtitle", "value": "合并进化 · 策略消除" }, { "name": "start_game", "value": "开始游戏" }, { "name": "pause", "value": "暂停" }, { "name": "restart", "value": "重新开始" }, { "name": "game_rules", "value": "游戏规则" }, { "name": "rule_1", "value": "点击列投放猫咪" }, { "name": "rule_2", "value": "相邻同级猫咪自动合并升级" }, { "name": "high_score", "value": "最高分" }, { "name": "game_over", "value": "游戏结束" }, { "name": "final_score", "value": "最终得分" }, { "name": "new_record", "value": "新纪录" }, { "name": "play_again", "value": "再来一局" }, { "name": "back_to_menu", "value": "返回主菜单" } ] }字段说明:
| 字段 | 含义 |
|---|---|
name | 资源名(蛇形,与代码引用一致) |
value | 默认文案 |
2.2 en_US 覆盖
resources/en_US/element/string.json:
{ "string": [ { "name": "app_title", "value": "Cat Battle" }, { "name": "app_subtitle", "value": "Merge & Evolve · Strategic Elimination" }, { "name": "start_game", "value": "Start Game" }, { "name": "pause", "value": "Pause" }, { "name": "restart", "value": "Restart" }, { "name": "game_rules", "value": "Game Rules" }, { "name": "rule_1", "value": "Tap a column to drop a cat" }, { "name": "rule_2", "value": "Adjacent same-level cats auto-merge" }, { "name": "high_score", "value": "High Score" }, { "name": "game_over", "value": "Game Over" }, { "name": "final_score", "value": "Final Score" }, { "name": "new_record", "value": "New Record" }, { "name": "play_again", "value": "Play Again" }, { "name": "back_to_menu", "value": "Back to Menu" } ] }关键经验:en_US 的 string.json 只放需要翻译的 key——名字必须与 base 一致,否则编译报错。
三、语言目录分流
3.1 目录命名规则
HarmonyOS 按语言-地区命名目录:
| 目录 | 语言 | 设备语言匹配 |
|---|---|---|
base/ | 默认 | 兜底,所有设备 |
zh_CN/ | 简体中文 | 设备语言=中文+地区=中国 |
zh_TW/ | 繁体中文 | 设备语言=中文+地区=台湾 |
en_US/ | 美式英文 | 设备语言=英文+地区=美国 |
en_GB/ | 英式英文 | 设备语言=英文+地区=英国 |
ja_JP/ | 日文 | 设备语言=日文 |
ko_KR/ | 韩文 | 设备语言=韩文 |
3.2 加载优先级
1. 精确匹配:zh_CN/ (设备=中文+中国) 2. 语言匹配:zh_HK/ → zh_CN/ → zh/(fallback) 3. 默认兜底:base/实战经验:只准备base/(默认中文)+en_US/(英文)两套,覆盖 95% 场景。其他语言按需追加。
3.3 目录结构示例
resources/ ├─ base/ │ ├─ element/ │ │ ├─ string.json # 默认中文文案 │ │ └─ color.json │ ├─ media/ │ └─ profile/ ├─ en_US/ │ └─ element/ │ └─ string.json # 英文文案 ├─ zh_CN/ # 可选,与 base 相同则省略 │ └─ element/ │ └─ string.json └─ dark/ # 暗色模式(第 28 篇) └─ element/ └─ color.json四、改造主菜单支持 i18n
4.1 主菜单文案全部用 $r
@Builder MainMenuView() { Column() { Spacer().height('15%') // 标题区(i18n 改造) Text('🐱').fontSize(72).margin({ bottom: 8 }) Text($r('app.string.app_title')) .fontSize(36) .fontWeight(FontWeight.Bold) .fontColor($r('app.color.title_color')) .margin({ bottom: 8 }) Text($r('app.string.app_subtitle')) .fontSize(16) .fontColor($r('app.color.subtitle_color')) .margin({ bottom: 48 }) // 最高分 if (this.highScore > 0) { Row() { Text('🏆 ').fontSize(16).fontColor('#F1C40F') Text($r('app.string.high_score') + ': ') .fontSize(16).fontColor('#F1C40F') Text(this.highScore.toString()) .fontSize(20).fontWeight(FontWeight.Bold).fontColor('#F1C40F') }.margin({ bottom: 32 }) } // 开始游戏按钮 Button($r('app.string.start_game')) .width('70%').height(56) .fontSize(20).fontWeight(FontWeight.Bold) .fontColor('#FFFFFF') .backgroundColor($r('app.color.primary_button')) .borderRadius(28) .shadow({ radius: 8, color: 'rgba(46, 204, 113, 0.4)', offsetY: 4 }) .onClick(() => { this.startGame(); }) Spacer().height(24) // 游戏规则面板(i18n 改造) Scroll() { Column() { Text($r('app.string.game_rules')) .fontSize(14).fontWeight(FontWeight.Bold) .fontColor($r('app.color.text_primary')) Text('• ' + $r('app.string.rule_1').toString()) .fontSize(13).fontColor($r('app.color.text_secondary')) Text('• ' + $r('app.string.rule_2').toString()) .fontSize(13).fontColor($r('app.color.text_secondary')) Text('• ' + $r('app.string.rule_3').toString()) .fontSize(13).fontColor($r('app.color.text_secondary')) Text('• ' + $r('app.string.rule_4').toString()) .fontSize(13).fontColor($r('app.color.text_secondary')) } .alignItems(HorizontalAlign.Start) } .scrollable(ScrollDirection.Vertical) .scrollBar(BarState.Auto) .width('80%').height(180).padding(16) .backgroundColor($r('app.color.card_bg')) .borderRadius(12) Spacer() } .width('100%').height('100%') .linearGradient({ direction: GradientDirection.Bottom, colors: [ [$r('app.color.bg_gradient_start'), 0.0], [$r('app.color.bg_gradient_mid'), 0.5], [$r('app.color.bg_gradient_end'), 1.0] ] }) .alignItems(HorizontalAlign.Center) }4.2 改造要点
| 原写法 | 改造后 | 效果 |
|---|---|---|
Text('猫猫大作战') | Text($r('app.string.app_title')) | 中英自动切 |
Button('开始游戏') | Button($r('app.string.start_game')) | 中英自动切 |
'最高分: ' | $r('app.string.high_score') + ': ' | 拼接字符串 |
提示:
Button也接受$r资源——Button($r('app.string.start_game'))直接渲染多语言文案。
五、$r 的格式化参数
5.1 带占位符的字符串
string.json:
{ "name": "score_format", "value": "得分:%d" }代码引用:
Text($r('app.string.score_format', this.score)) // 中文 → 得分:99 // 英文(en_US string.json: { "name": "score_format", "value": "Score: %d" })→ Score: 995.2 多参数
{ "name": "battle_result", "value": "玩家%s击败了%s,获得%d分" }Text($r('app.string.battle_result', playerName, enemyName, score)) // 玩家Alice击败了Boss,获得1500分关键经验:$r第二个参数起是格式化实参——%s/%d/%f按 C 语言 printf 规则替换。
5.3 复数(plural)
某些语言有复数形式(如英文 1 cat / 2 cats):
string.json:
{ "plural": [ { "name": "cat_count", "value": "one cat", "value_plural": "%d cats" } ] }代码引用:
Text($r('app.plural.cat_count', this.catCount, this.catCount)) // catCount=1 → one cat // catCount=5 → 5 cats六、运行时语言切换
6.1 跟随系统(默认)
不做任何设置,应用自动按设备语言加载对应string.json。
6.2 应用内强制切换语言
HarmonyOS 不直接提供「应用内切语言」API,需通过AppStorage+ 条件渲染模拟:
@StorageProp('appLanguage') appLanguage: string = 'zh'; // 根据语言选择 string.json getLocalizedString(name: string): Resource { if (this.appLanguage === 'en') { return $r('app.string.' + name + '_en'); } return $r('app.string.' + name + '_zh'); }更优方案:使用i18n模块的setAppLocale(API 12+):
import { i18n } from '@kit.LocalizationKit'; // 强制应用语言为英文 i18n.System.setAppLocale('en-US'); // 恢复跟随系统 i18n.System.setAppLocale('');实战经验:游戏类应用建议提供「跟随系统 / 简体中文 / English」三选项——海外玩家可能想强制英文。
6.3 监听系统语言变化
export default class EntryAbility extends UIAbility { onCreate(): void { AppStorage.setOrCreate('systemLanguage', this.context.config.locale.language); } onConfigurationUpdate(newConfig: Configuration): void { AppStorage.setOrCreate('systemLanguage', newConfig.locale.language); } }七、踩坑提示
7.1 string.json key 拼错
// base string.json { "name": "app_title", "value": "猫猫大作战" } // en_US string.json(拼错) { "name": "app_Title", "value": "Cat Battle" }后果:编译报错或 en_US 覆盖失败。en_US 的 key 必须与 base 完全一致。
7.2 硬编码文案未替换
// ❌ 错误:部分文案还是硬编码 Text('猫猫大作战') // 硬编码 Button($r('app.string.start_game')) // 已适配 // ✅ 正确:所有文案统一用 $r Text($r('app.string.app_title')) Button($r('app.string.start_game'))7.3 格式化参数类型不匹配
// string.json: { "name": "score", "value": "得分:%d" } // ❌ 错误:传字符串给 %d Text($r('app.string.score', 'abc')) // ✅ 正确:传数字 Text($r('app.string.score', 99))7.4 复数使用错误
// ❌ 错误:plural 用 string 引用 Text($r('app.string.cat_count', count)) // ✅ 正确:plural 用 plural 引用 Text($r('app.plural.cat_count', count, count))八、调试技巧
- DevEco 预览器切语言:预览器右上角可切换设备语言,实时看多语言效果。
- 真机系统设置:设置 → 系统和更新 → 语言和输入法 → 添加语言,验证应用适配。
console.info打 locale:onConfigurationUpdate里 lognewConfig.locale.language,追系统语言切换。- 未翻译排查:全局搜索
Text('和Button(',看是否有硬编码文案。
九、性能与最佳实践
- i18n = 文案抽 string.json + 目录分流 + $r 引用——业务代码零改动。
- 准备 base(默认)+ en_US(英文)两套,覆盖 95% 场景。
- 所有文案统一用 $r——硬编码文案不跟随语言。
- 格式化用 %s/%d 占位符——避免字符串拼接。
- 复数用 plural 资源——避免 if/else 判断单复数。
- 提供三选项——跟随系统 / 简体中文 / English。
总结
本篇我们从多语言 i18n 切入,掌握了string.json 结构与 zh_CN/en_US 目录分流、$r 引用与格式化参数、运行时语言切换与监听、复数 plural 资源四大要点,并给出了主菜单 i18n 改造完整代码。核心要点:i18n 零代码改动,全靠 string.json + $r;准备 base + en_US 两套;格式化用占位符;复数用 plural。
下一篇我们将拆解 AttributeModifier——按钮样式复用的利器。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- 「猫猫大作战」项目源码:本仓库
entry/src/main/ets/pages/Index.ets - HarmonyOS i18n 国际化官方指南
- string.json 资源结构官方文档
- LocalizationKit i18n API 参考
- i18n 国际化最佳实践
- 开源鸿蒙跨平台社区
- HarmonyOS 开发者官方文档首页
- 系列索引:本仓库
articles/INDEX.md