多语言国际化:zh_CN/en_US 限定词与 string.json 资源体系
一、引言
国际化(i18n)是"多设备短视频"应用走向更多用户的基本功。一个覆盖直板机、平板与 PC 的应用,若把文案硬编码在代码里,每一次文案调整、每新增一种语言,都要翻遍全部源码——这既是维护负担,也是质量隐患。HarmonyOS 提供了一套与深色模式同构的资源限定词机制:文案声明在string.json中,通过zh_CN、en_US等语言限定词目录提供多语言版本,代码中一律用$r('app.string.xxx')引用,系统按设备语言环境自动匹配,切换语言零代码。
multi-short-video 工程在五个产品模块、四个特性模块与公共模块中全面落地了这一机制。本文以首页(products/default)、评论(multishortvideocomment)、个人作品页(multishortvideoindividual)三处真实文案为例,讲解 string.json 的资源定义、$r引用、格式化占位符与兜底策略。
二、zh_CN/en_US 限定词目录与 string.json 资源定义
国际化的载体是"语言限定词目录"。以评论模块为例,其资源目录结构为:
features/multishortvideocomment/src/main/resources/ ├── base/element/string.json # 默认文案(兜底,中文) ├── zh_CN/element/string.json # 简体中文 ├── en_US/element/string.json # 美式英语 ├── dark/element/color.json # 深色颜色(与语言正交) └── base/media/ # 图标等媒体资源base目录承担兜底职责:当设备语言不匹配任何限定词目录时,系统取base中的取值。因此工程把base的文案定义为中文,zh_CN与base内容一致,en_US提供英文——即便未来新增小语种,未覆盖的文案也能正确回退到中文。限定词之间用中划线连接可组合,例如dark/zh_CN同时限定主题与语言,本文与深色模式文章(51)可以互相印证。
string.json 的格式为"name + value"数组。name 是编译期键,必须与语言无关(推荐英文下划线风格);value 是展示文案。评论模块的中英文定义如下:
// features/multishortvideocomment/src/main/resources/zh_CN/element/string.json(节选) { "string": [ { "name": "leave_comment", "value": "写评论" }, { "name": "author", "value": "作者" }, { "name": "reply", "value": "回复" }, { "name": "view_all_reply", "value": "查看全部回复" }, { "name": "nothing_more", "value": "暂时没有更多了!" } ] }// features/multishortvideocomment/src/main/resources/en_US/element/string.json(节选) { "string": [ { "name": "leave_comment", "value": "leave a comment" }, { "name": "author", "value": "author" }, { "name": "view_all_reply", "value": "view all replies" }, { "name": "nothing_more", "value": "There's nothing more for now!" } ] }注意en_US中可以省略reply——未定义时自动回退base的中文"回复"。工程保留它是因为英文文案与中文不同,故显式声明。限定词目录只写差异、base 兜底,是控制多语言维护成本的关键。
base与zh_CN内容一致的约定还有一个工程理由:IDE 与构建系统在资源冲突时会有提示,但不会阻止两份文件内容漂移(例如某次需求只改了 base 忘记同步 zh_CN)。工程建议通过脚本或 lint 规则强制二者 key 集合一致,把"兜底目录"真正变成"默认语言的备份",而不是放任它成为第二事实源。同理,dark颜色目录与base颜色目录的 key 集合也应保持一致,任何新增颜色都要双份声明——这与第 51 篇文章中深色资源的治理纪律完全同构。
三、$r('app.string.xxx') 引用方式
UI 层引用文案统一使用$r('app.string.资源名')。$r返回一个Resource对象,可传递给 Text 组件的文本参数、TextInput 的 placeholder、Tabs 的标题等。评论页(features/multishortvideocomment/src/main/ets/view/Comment.ets)中的真实引用:
Text($r('app.string.author')) // "作者"徽标 Text($r('app.string.reply')) // "回复"按钮 Text($r('app.string.view_all_reply')) // "查看全部回复" Text($r('app.string.nothing_more')) // 列表底部"暂时没有更多了!" TextInput({ text: this.commentInput!!, placeholder: $r('app.string.leave_comment'), // 输入框占位文案 controller: this.controller })个人作品页(features/multishortvideoindividual/.../components/Individual.ets)同样统一引用:$r('app.string.author_id')(国家地理/National Geographic)、$r('app.string.likes')(获赞/likes)、$r('app.string.search_placeholder')(搜索/Search)、$r('app.string.introduction')(点击添加介绍…)等;页签标题通过 ViewModel 注入(IndividualTabsViewModel.ets):
this.individualTabsData.push(new MSVDataModel(works, $r('app.string.ind_work'))); // 作品/work this.individualTabsData.push(new MSVDataModel(privacy, $r('app.string.ind_private'))); // 私密/private this.individualTabsData.push(new MSVDataModel(collection, $r('app.string.ind_collection'))); // 收藏/collection this.individualTabsData.push(new MSVDataModel(like, $r('app.string.ind_like'))); // 喜欢/like首页 TabBar 文案定义在products/default/.../viewmodel/MainTabsViewModel.ets中,中英文对照如下:
| 资源名 | zh_CN | en_US |
home_title | 首页 | Home |
friend_title | 朋友 | Friend |
message_title | 消息 | Message |
recommend_title | 推荐 | Recommend |
follow_title | 关注 | Follow |
这里有个重要观察:$r引用是"跨模块的"——评论模块、个人页模块、公共模块的文案都通过各自模块的 string.json 定义,引用路径统一为app.string.*,系统按"当前模块资源 → AppScope 全局资源"的顺序解析。这也要求各模块资源名全局不冲突,是第 55 篇文章(代码规范)强调资源命名纪律的原因之一。
四、格式化字符串与占位符
评论数、点赞数这类"数字 + 文案"的场景,不能简单拼接字符串——不同语言对数字与名词的顺序、复数形式要求各异。正确做法是在 string.json 中声明带占位符的模板,用$r('app.string.xxx', 参数)在引用时注入。
视频模块的评论标题即为此范例(features/multishortvideoadaptivevideo/src/main/resources/zh_CN/element/string.json):
{ "name": "comment_title", "value": "评论(%d)" }// features/multishortvideoadaptivevideo/src/main/resources/en_US/element/string.json { "name": "comment_title", "value": "comment(%d)" }播放器控件层引用时注入数值(features/multishortvideoadaptivevideo/.../view/AdaptiveVideo.ets第 322 行):
// bindSheet 半模态标题:显示"评论(5)" title: { title: $r('app.string.comment_title', 5) }%d是十进制整数占位符,系统按当前语言环境格式化后替换。这样中英文环境下数字与文案的相对位置、标点都由各语言自己的模板决定,翻译时可以自由调整结构而不影响代码。除了%d,鸿蒙资源还支持%s(字符串)、%f(浮点数)等占位符,多参数按序对应。
选择"整句翻译"还是"模板占位符",取决于数字与文案的排列关系是否跨语言稳定。以视频模块的发布时间为例,中英文的时间表达差异明显("2小时前" vs "two hours ago"),工程选择整句翻译——release_time资源直接声明整句,避免"数字+单位"跨语言排列错位;只有当数字与文案顺序在所有语言中都一致时才使用占位符。判断标准很简单:把翻译交给译者时,模板中保留的占位符越少,出错概率越低。
五、运行时语言切换与兜底策略
运行时语言切换完全由系统承担:用户在系统设置中修改语言后,应用进程内资源引用自动指向新语言目录,无需重启应用或手动刷新——前提是所有文案都通过$r引用,且没有把字符串缓存在普通变量中。工程中Comment.ets、Individual.ets等组件每次 build 时都实时求值$r('app.string.xxx'),天然支持热切换。
兜底策略则要回答"语言不齐时怎么办"。限定词匹配的完整规则是:先按限定词匹配程度排序(匹配项越多越优先),全部不匹配时回退base。因此:
base必须完整且兜底可用(工程中 base 即中文,能覆盖全部文案);en_US等语言目录允许缺项,缺项回退 base;- 但缺项会造成"半中半英"的混排体验。工程中
products/default/.../resources/en_US/element/string.json的mine_title值仍为"我的"(中文未翻译),就是一个需要排查的真实案例——en_US存在同名键但值未翻译,系统不会回退到其他目录,只能靠审查发现。这正是国际化质量治理的切入点:要么该语言目录完整覆盖,要么干脆不建目录靠 base 兜底,最怕"建了目录却不翻全"。
从实现层面看,语言资源还影响module.json5中的应用名与 Ability 标签:products/default的DefaultAbility_label、module_desc也走了$r('app.string.xxx')国际化,系统桌面、任务卡片上展示的应用名会随语言变化。也就是说,国际化不是"页面文案"的局部工程,而是从桌面图标、任务卡片到页面内容的全链路统一。做国际化自查时,除了页面遍历,还要把桌面名称、通知栏文案、权限弹窗文案一并纳入检查范围。
六、多模块文案的协同管理
工程有五个产品模块、四个特性模块加一个公共模块,每个模块维护自己的base/zh_CN/en_US三份 string.json,跨模块文案的协同由此成为治理重点。工程的实际分工是:公共文案放 common,业务文案放所属特性模块,产品定制文案放产品模块。
公共模块common/multishortvideobase只声明了一条empty_text("暂无内容"/"No content"),供各特性模块的空态组件复用;评论、个人页、视频页的文案各自收口在本模块内(leave_comment、ind_work、comment_title等);首页 TabBar 的文案则因各产品形态不同而放在产品模块——default 的home_title/recommend_title即是如此,tv 产品若有差异化文案只需在自己的资源目录覆盖。这种"就近声明"避免了公共资源命名空间的膨胀,也符合products → features → common三层架构的职责边界。
协同管理还需处理两个跨模块细节。其一是资源命名空间的唯一性:$r('app.string.xxx')在运行时按"当前模块 → AppScope"顺序解析,两个模块声明同名 key 时,当前模块内的引用优先命中自己那份——这既是能力也是陷阱,若想跨模块复用公共文案,应引用公共模块明确暴露的资源,而不是碰运气。其二是资源表与代码的同步:新增文案后要同步检查zh_CN/en_US两份文件,CI 可加脚本比对 key 集合,发现缺项即失败(对应前文"目录完整性"纪律);代码审查时也可用$r('app.string.')的全局搜索快速核对每个引用点都有对应资源。
对于需要"按设备能力裁剪文案"的场景,工程还可以组合限定词与断点:同一 key 在不同形态产品模块中给出不同 value,配合WidthBreakpointType的代码分支,做到文案与布局、交互同步适配。总之,多模块国际化的本质是把"文案即代码"的观念升级为"文案即资源":定义在资源层、引用在代码层、差异由系统层解决。
七、总结与最佳实践
多语言国际化在本工程中沉淀为四条最佳实践:
- 文案零硬编码:所有展示文案进
string.json,代码一律$r('app.string.xxx')引用;占位符场景用$r('app.string.comment_title', 5)注入参数,不手工拼接。 - base 兜底 + 语言目录只写差异:base 放默认语言(本工程为中文),
zh_CN/en_US按需声明;新语言目录未覆盖的文案自动回退,控制维护成本。 - name 与语言无关:资源名统一英文下划线风格(
leave_comment),value 才允许写中文,避免把"语言"写进编译期键。 - 目录完整性审查:语言目录要么建全、要么不建;对"同名键值未翻译"(如 en_US 中
mine_title残留中文)这类隐患做专项排查,可借助资源比对脚本在 CI 中拦截。
遵循这四条,新增页面时只需"建资源 → 写引用"两步,多语言与多设备(第 05 篇文章的资源规范)共同构成工程化文案管理的地基。