HarmonyOS社交通讯应用开发49 : 国际化动态切换
49 国际化动态切换
引言
例如:标题显示"社交通讯全场景协同";如果把设备语言切成英文再打开,同样的位置就变成了"Full-Scenario Collaboration in Social Communication"。不止首页——编辑页的"添加位置"、"内容编辑",浏览页的底部 Tab 文案,甚至详情页的整篇模拟数据,都会跟着系统语言自动切换。

这个"自动切换"背后是三套机制在协同工作:资源目录限定符(zh_CN/en_US 两份 string.json)、i18n 能力(读取与监听系统语言偏好)、AppStorage + @Watch(把语言变化广播到所有页面)。本篇文章把这三套机制逐一拆解,看看项目如何做到"用户改一次系统语言,全应用即时响应"。
一、国际化基础:资源、引用与语言读取
1. 资源限定符目录:一份资源,多份翻译
ArkUI 的资源目录用"目录名 + 限定符"来区分不同语言环境。本项目在 resources 下准备了两个语言目录:
resources/
├── base/element/string.json // 默认语言资源
├── en_US/element/string.json // 美式英语
└── zh_CN/element/string.json // 简体中文
同一份 string.json,语言不同则 value 不同。比如"添加位置"这一条:
// zh_CN/element/string.json
{
"name": "add_local",
"value": "添加位置"
}
// en_US/element/string.json
{
"name": "add_local",
"value": "Add Location"
}
系统在加载资源时会根据当前语言偏好,自动从对应目录取 value。开发者不需要关心"现在该读哪份"——框架替你选好了。
2. $r() 资源引用:在代码里"按名取词"
在 ArkTS 代码里,用 $r('app.string.xxx') 引用字符串资源。xxx 就是 string.json 里 name 字段。以首页 Index.ets 为例:
Text($r('app.string.title'))
.fontSize(30)
...
Button($r('app.string.button1'))
.width('100%')
...
编译时 $r('app.string.title') 会解析为一个 Resource 对象,运行时按当前语言解析出实际文本。注意:$r 引用要求 name 必须在所有语言目录中保持一致(只允许 value 不同)。检查本项目两份 string.json,name 集合完全一致,只是 value 不同,这正是国际化的基本纪律。
3. i18n.System.getAppPreferredLanguage:读取系统语言偏好
i18n.System.getAppPreferredLanguage() 返回当前应用偏好的语言标签,例如中文环境返回类似 "zh-Hans-CN" 的字符串,英文环境返回 "en-US" 之类的值。本项目用它的返回值初始化全局语言状态,也用它判断当前是否中文环境。它是整条国际化链路的"语言源头"。
二、语言初始化与动态监听:从系统到 AppStorage
1. 启动时播种语言
UIAbility 的 onCreate 是语言的"播种点"。EntryAbility.ets 中:
// init language
let locale: string = i18n.System.getAppPreferredLanguage();
AppStorage.setOrCreate(CommonConstants.LANGUAGE, locale);
AppStorage 的键由 CommonConstants 统一管理,避免魔法字符串:
public static readonly LANGUAGE: string = 'language';
public static readonly CHINESE_LANGUAGE: string = 'zh';
注意播种的值是完整语言标签(如 "zh-Hans-CN"),而判断中文用 includes('zh') 这样的子串匹配,就是为了兼容各种地区变体。
2. 运行时监听语言变化
只播种还不够。用户在设置里切换语言时,应用需要感知到。本项目注册了 EnvironmentCallback 环境回调:
let environmentCallback: EnvironmentCallback = {
onConfigurationUpdated(config) {
AppStorage.setOrCreate(CommonConstants.LANGUAGE, config.language);
},
onMemoryLevel(level) {
hilog.info(DOMAIN, TAG, FORMAT, `onMemoryLevel level: ${level}`);
}
};
let applicationContext = this.context.getApplicationContext();
try {
this.callbackId = applicationContext.on('environment', environmentCallback);
} catch (paramError) {
hilog.error(DOMAIN, TAG, FORMAT,
`error: ${(paramError as BusinessError).code}, ${(paramError as BusinessError).message}`);
}
onConfigurationUpdated 回调携带新的 config,其中 language 字段就是最新的语言标签。项目把它写入 AppStorage 的 language 键——接下来就交给上一篇文章讲过的 @Watch 机制,自动广播到所有订阅页面。从"系统事件"到"全局状态"再到"UI 刷新",只用了三行核心代码,这就是状态驱动架构的红利。
三、页面响应语言变化:三个真实案例
1. 浏览页:Tab 文案整组切换
ContentBrowsePage.ets 用 @Watch 监听 language 键,语言一变就重建底部 Tab 数据:
@StorageLink(CommonConstants.LANGUAGE) @Watch('changeTab') language: string = CommonConstants.CHINESE_LANGUAGE;
changeTab() {
if (this.language.includes(CommonConstants.CHINESE_LANGUAGE)) {
this.iconArr = new FooterTabData().tabList;
} else {
this.iconArr = new FooterTabDataEn().tabList;
}
}
这里的中英文 Tab 数据是两套独立的 ViewModel:FooterTabData 读取 HomeConstants.FOOTER_TOPIC_LIST(中文话题列表),FooterTabDataEn 读取 FOOTER_TOPIC_LIST_EN(英文话题列表),图标则共用同一套:
// FooterTabData.ets
export class FooterTabData {
constructor() {
HomeConstants.FOOTER_TOPIC_LIST.forEach((item: string, index: number) => {
this.tabList.push(new FooterTab(item, HomeConstants.FOOTER_TOPIC_ICONS[index],
HomeConstants.FOOTER_TOPIC_ICONS_SELECTED[index]));
});
}
}
export class FooterTabDataEn {
constructor() {
HomeConstants.FOOTER_TOPIC_LIST_EN.forEach((item: string, index: number) => {
this.tabList.push(new FooterTab(item, HomeConstants.FOOTER_TOPIC_ICONS[index],
HomeConstants.FOOTER_TOPIC_ICONS_SELECTED[index]));
});
}
}
为什么这个话题列表不用 string.json?因为话题名是"业务数据"而非"界面文案",且通过 ViewModel 直接注入 FooterTab 对象。这提示我们:界面文案走资源目录,业务数据走代码分支,两条路各有适用场景。
2. 详情页:整篇内容数据按语言切换
详情页 ContentDetailSamplePage.ets 同样监听 language,但这次换的是整篇业务数据:
@StorageLink(CommonConstants.LANGUAGE) @Watch('changeData') language: string = CommonConstants.CHINESE_LANGUAGE;
changeData() {
this.contentDetailsData = getMockData(this.getUIContext(),
`contentDetailSampleMockData${this.descriptionData!.index % 2 + 1}.json`,
this.language.includes(CommonConstants.CHINESE_LANGUAGE));
...
}
getMockData 是 MockDataUtil.ets 里的工具函数,它从 rawfile 读取 JSON,按第三个参数 isChinese 选择 zh 还是 en 字段:
export function getMockData(uiContext: UIContext, mockFileDir: string,
isChinese: boolean = true): ContentDetailsInfo | undefined {
let data: ContentDetailsInfo | undefined = undefined;
try {
let value = uiContext!.getHostContext()!.resourceManager.getRawFileContentSync(mockFileDir);
let textDecoder = util.TextDecoder.create('utf-8', {
ignoreBOM: true
});
let textDecoderResult = textDecoder.decodeToString(new Uint8Array(value.buffer));
let jsonObj: Record<string, ContentDetailsInfo> =
JSON.parse(textDecoderResult) as Record<string, ContentDetailsInfo>;
data = isChinese ? jsonObj['zh'] : jsonObj['en'];
} catch (error) {
...
}
return data;
}
关键在最后一行:data = isChinese ? jsonObj['zh'] : jsonObj['en']。也就是说,模拟数据文件里同时存了中英两个版本的完整对象,按当前语言取其一。这种"双语数据同文件"的方式很适合 mock 场景,保证两条语言路径的数据结构完全一致。
3. 底部工具栏:逆地理编码的语言联动
BottomToolbar.ets 是"最聪明"的语言响应者——它不光刷新文案,还重新请求一次定位数据。因为地名的语言必须跟着系统语言走:中文系统下位置显示"武汉市",英文系统下要显示 "Wuhan"。
首先它把当前语言缓存到私有成员:
private locale: string = i18n.System.getAppPreferredLanguage();
然后在逆地理编码请求中,把 locale 折算成 zh/en 传给系统:
private location(location: geoLocationManager.Location) {
try {
this.latitude = location.latitude;
this.longitude = location.longitude;
let reverseGeocodeRequest: geoLocationManager.ReverseGeoCodeRequest = {
'locale': this.locale.toString().includes('zh') ? 'zh' : 'en',
'latitude': this.latitude,
'longitude': this.longitude
};
geoLocationManager.getAddressesFromLocation(reverseGeocodeRequest).then(data => {
...
this.currentLocalInfo.push(item.placeName!);
this.currentLocalInfo.push(item.administrativeArea!);
...
});
} catch (err) {
...
}
}
触发重新定位的时机,正是 @Watch 监听到语言变化时:
@Watch('systemLanguage') @StorageLink(CommonConstants.LANGUAGE) systemLanguages: string = '';
systemLanguage(): void {
if (this.latitude !== undefined && this.longitude !== undefined) {
this.locale = i18n.System.getAppPreferredLanguage();
LocationUtil.getCurrentLocation(this.getGeolocation);
}
}
注意这里的细节:@Watch 回调里先更新 locale 缓存,再重新定位。顺序不能反——因为 location 回调里用的是 this.locale,而不是 @StorageLink 变量本身。这一处"缓存 + 重取"的配合,是整篇文章最值得品味的工程细节。
三·补、初始化与切换走同一条路径
观察两个页面的 aboutToAppear,会发现一个共同模式:监听回调函数在初始化时也会被主动调用一次。
ContentBrowsePage.ets:
aboutToAppear(): void {
this.changeTab();
...
}
ContentDetailSamplePage.ets:
aboutToAppear(): void {
...
this.descriptionData =
this.pageInfos.getParamByName(HomeConstants.CONTENT_DETAIL_SAMPLE_PAGE)?.[0] as WaterFlowDescriptionData;
this.changeData();
}
为什么要在 aboutToAppear 里再调一次 changeTab/changeData?因为 @Watch 只在"值变化"时触发,首次初始化赋值不会触发监听。如果只依赖 @Watch,页面首次创建时 Tab 数据和详情数据就是空的,必须手动调用一次"初始化路径"。
这个模式的价值在于单一路径:初始化与运行时切换共用同一个函数,天然保证两种时机下产生的数据结构一致,不会出现"第一次进来是中文、切一次语言就变英文"这类不一致问题。这是国际化实现中非常值得借鉴的工程习惯——把"按语言取数据"的逻辑收敛到一处,而不是散落在各生命周期回调里。
五、两份 string.json 的差异管理
最后看看资源层面。两份 string.json 的 name 完全对齐,但 value 有显著的本地化差异,除了"添加位置/Add Location"这类直译,还有几类值得注意:
占位符:zh_CN 的默认位置是 "武汉XX路%d楼",en_US 是 "Building %d, XX Road, Wuhan"——同样的 %d 占位符,词序完全不同。调用方统一用 $r('app.string.default_location', 1) 传参:
static readonly CURRENT_LOCAL_INFO: ResourceStr[] = [
$r('app.string.default_location', 1),
$r('app.string.default_location', 2),
...
];
语序与长度:中文"添加优质首图"vs 英文"Add Image",中文"内容发布"vs 英文"Content Publish",长度差异很大。布局上稍不留神就会截断,这也是国际化项目必须预留弹性空间的原因。
术语一致性:例如权限描述这类长句,两份资源都保持了完整的句式结构。建议所有翻译键在评审时对照原文逐条核对 name 一致性——一旦某个 name 只存在于一份文件,$r 引用在另一种语言下会直接解析失败。
最后补充两点容易被忽略的资源机制:
base 目录的回退作用。resources/base/element/string.json 是默认语言资源。当设备语言不在任何限定符目录覆盖范围内时,系统会回退到 base 目录。因此工程实践上,base 目录通常放置开发语言(本项目即中文),其余语言各自建目录。$r 引用对 name 的强一致性要求,正是为了在回退发生时不会"缺词"。
应用名的本地化。国际化不止覆盖界面文案,还覆盖应用配置。两份 string.json 里都定义了 EntryAbility_label:
// zh_CN: "value": "内容发布"
// en_US: "value": "Continue Publish"
这个 label 会显示在桌面应用图标下方和任务管理中,用户切换系统语言后,应用名也会跟着变化。验证国际化效果时,不妨从桌面图标的名字变化看起——这是最直观的"第一印象"。
小结
本篇文章完成了国际化动态切换的完整闭环梳理:资源限定符目录提供了"按语言取文案"的底座;i18n.System.getAppPreferredLanguage() + EnvironmentCallback 完成了"读语言、听变化";AppStorage 的 language 键作为中枢,把系统事件广播给所有订阅页面;三个案例分别示范了"界面文案切换($r 资源)"、"业务数据切换(双语 JSON)"和"能力联动(定位语言)"三种响应层次。
至此,模块八的技术主线已经走完。最后一篇,我们把整个《ContinuePublish》项目的能力矩阵、运行约束和学习路径做一次总结,为 50 篇文章画上句号。
更多推荐



所有评论(0)