鸿蒙系统语言跟随切换 — ArkTS技术实现


一、业务需求(为什么这么设计)
本应用是系列的收官之作,回答"App 语言听谁的"这个终极问题。产品是一个设置中心的语言管理页,需要支持三种能力的演示:
- 读取系统信息:系统语言、系统地区、系统时区——
i18n.System系列 API; - 语言策略切换:跟随系统(Toggle 开)vs 应用内覆盖(Toggle 关 + 手动选语言);
- 双语言实时对比:模拟系统语言变化,观察"设置项文案"在两种策略下的不同表现。
核心知识点是**“生效语言”(activeLang)的推导**:它不是单一状态,而是 跟随开关 × 系统语言 × 用户选择 三个变量的函数。本文记录完整技术方案,所有代码与 SystemLanguagePage.ets 一一对应。
二、总体架构
┌─ 系统层:i18n.System.getSystemLanguage/getSystemRegion + i18n.getTimeZone
├─ 策略层:followSystem 开关 + activeLang() 生效语言推导(核心)
├─ 文案层:STRINGS 5 语言表 + t(code, key) 三级降级
├─ 模拟层:simSystemLang 模拟系统语言(真机无法直接改系统语言)
└─ 状态层:@StorageLink currentLocale + @State followSystem / simSystemLang
架构核心是双源取词:currentLocale(用户手动选择的 App 语言)与 simSystemLang(模拟的系统语言)是两个独立状态,activeLang() 按策略合成"真正用于渲染的语言"——全页文案只从这一个函数取词。
三、系统信息 API:i18n.System
private sysLangName(): string {
try {
return i18n.System.getSystemLanguage(); // 'zh-CN' | 'en-US' | ...
} catch (err) {
return 'zh-CN';
}
}
private sysRegionName(): string {
try {
return i18n.System.getSystemRegion(); // 'CN' | 'US' | ...
} catch (err) {
return 'CN';
}
}
private sysTzName(): string {
try {
return i18n.getTimeZone().getID(); // 'Asia/Shanghai' | ...
} catch (err) {
return 'Asia/Shanghai';
}
}
三个 API 的语义:
| API | 返回 | 示例 | 用途 |
|---|---|---|---|
getSystemLanguage() |
BCP-47 语言标签 | zh-CN |
界面跟随的基准 |
getSystemRegion() |
ISO 3166 地区码 | CN |
地区化规则(日期/单位) |
i18n.getTimeZone().getID() |
IANA 时区名 | Asia/Shanghai |
时间显示基准 |
注意 getSystemLanguage() 返回的是 zh-CN(连字符),而系列其他应用的 locale 用 zh_CN(下划线)——两者格式不同但语义等价。本页模拟器的语言码(zh_CN)与系统 API 返回值(zh-CN)格式不同,因此模拟器语言不直接喂给系统 API,而是走 STRINGS 表取词——这是刻意设计的边界:系统 API 读真实值、STRINGS 表管界面渲染,互不混用。
四、核心:activeLang() 生效语言推导
@StorageLink(STORAGE_LOCALE) currentLocale: string = DEFAULT_LOCALE;
@State followSystem: boolean = true;
@State simSystemLang: string = 'zh_CN';
/** 生效语言:跟随系统 → 模拟系统语言;否则 → 用户手动选择 */
private activeLang(): string {
return this.followSystem ? this.simSystemLang : this.currentLocale;
}
private T(key: string): string {
return t(this.activeLang(), key);
}
这是本应用最重要的 3 行代码。生效语言的真值表:
| followSystem | simSystemLang | currentLocale | activeLang() | 界面语言 |
|---|---|---|---|---|
| true | ja_JP | zh_CN | ja_JP | 日语(跟随模拟系统) |
| true | en_US | zh_CN | en_US | 英语 |
| false | ja_JP | zh_CN | zh_CN | 中文(用户选择,无视模拟器) |
| false | ja_JP | ko_KR | ko_KR | 韩语(用户选择) |
两个关键设计:
- 默认
followSystem = true:真实设备上,跟随系统的 App 在用户改系统语言后需要"重新进入页面"(或页面 onShow)才能看到新语言——本页用simSystemLang模拟这个外部变量,让变化即时可见; currentLocale仍持久化:即使跟随系统,手动选择(关闭跟随时的选择)也要记住——用户下次关闭跟随,回到上次的语言,不丢失偏好。
五、双源取词的边界:标题 vs 内容
页面里两套取词路径并存,这是容易踩坑的地方:
// 标题栏/区块标题:始终跟随 currentLocale(App 界面语言)
Text(t(this.currentLocale, K.title))
Text(t(this.currentLocale, K.sysInfo))
// 设置项内容:跟随 activeLang()(生效语言)
Text(t(this.activeLang(), key))
为什么标题不跟随模拟器:标题是"当前页面的语言"的自我说明——如果模拟器切到日文、标题也变日文,用户会困惑"我到底在看什么语言"。标题保持 currentLocale,用户永远知道"App 现在的语言是中文,而下面列表在模拟日语系统下的表现"。元信息(页面语言)与演示内容(模拟语言)分源,是教学页面的正确边界。
六、模拟器:真机限制下的教学妥协
private langLabel(code: string): string {
if (code === 'zh_CN') return '🇨🇳 简体中文';
if (code === 'en_US') return '🇺🇸 English';
if (code === 'ja_JP') return '🇯🇵 日本語';
if (code === 'ko_KR') return '🇰🇷 한국어';
return code;
}
// 模拟器徽章:改 simSystemLang
ForEach(['zh_CN', 'en_US', 'ja_JP', 'ko_KR'] as string[], (code: string) => {
Text(this.langLabel(code))
...
.onClick(() => { this.simSystemLang = code; })
}, (code: string) => code)
真实产品怎么做:系统语言变化会触发资源重匹配(12 的资源限定符自动完成),且页面在 onPageShow 时重新读取系统语言。旧版本有 i18n.System.on('languageChange') 监听,已废弃——现在推荐的做法是页面 onShow 重新取 getSystemLanguage() + 资源自动重匹配。本页用模拟器演示同样的效果,hint 文案诚实说明:“真实设备上,系统语言变化会自动触发资源重匹配;本页用模拟器演示’跟随’与’覆盖’的差异。”
七、状态联动与重渲染
aboutToAppear(): void {
if (!AppStorage.get<string>(STORAGE_LOCALE)) {
AppStorage.setOrCreate(STORAGE_LOCALE, DEFAULT_LOCALE);
}
PersistentStorage.persistProp(STORAGE_LOCALE, DEFAULT_LOCALE);
}
三个状态的联动关系:
followSystem变化 →activeLang()切换取词源 → 设置列表整块重渲染(跟随关时还额外渲染手动语言徽章区);simSystemLang变化 →activeLang()返回值变 → 设置列表内容变(跟随开时);currentLocale变化 → 标题/区块标题/系统信息标签变;同时若跟随关闭,设置列表内容也变。
没有"当前界面语言"这个缓存状态——activeLang() 每次 build 现算,三变量一变即得新结果,不会出现"开关动了但文案没变"的陈旧状态 bug。
八、数据流复盘(一次完整交互)
启动 → aboutToAppear 初始化 + persistProp
→ followSystem=true、simSystemLang=zh_CN
→ 标题"语言设置"(中文)、系统信息(真实值 zh-CN/CN/Asia/Shanghai)
→ 设置列表:无线网络/蓝牙/电池/存储/关于本机(中文)
用户把模拟器切到 🇯🇵 日本語
→ simSystemLang = 'ja_JP'
→ activeLang() = 'ja_JP'(跟随开)
→ 设置列表标题变 "设置 (🇯🇵 日本語)"
→ 5 行设置项变日文:Wi-Fi / Bluetooth / バッテリー / ストレージ / このデバイスについて
→ 标题栏不变(仍中文)——两个取词源各司其职
用户关闭"跟随系统"开关
→ followSystem = false
→ 手动语言徽章区展开(4 个,选中 zh_CN)
→ activeLang() = currentLocale = 'zh_CN'
→ 设置列表回到中文(模拟器被无视)——"覆盖"生效
用户点 🇰🇷 한국어 徽章
→ currentLocale = 'ko_KR'(AppStorage 落盘)
→ 标题栏变韩文;设置列表变韩文(覆盖模式)
→ 重启 App:currentLocale 恢复 ko_KR、followSystem 恢复 true(未持久化)
九、ArkTS 兼容要点
catch (err)不带类型注解,所有i18n.System调用包 try/catch(数据缺失兜底);ForEach(['zh_CN', ...] as string[], ...)数组字面量断言;ForEach(['wifi', 'bluetooth', ...] as string[], (key: string) => key):设置项 key 列表作为 ForEach 数据源,t(activeLang(), key)在回调内取词——key 既是数据标识又是文案键,天然唯一;Toggle({ type: ToggleType.Switch, isOn: this.followSystem }):isOn单向绑定 +onChange回写,避免双向绑定类型问题;langLabel()if 链返回字符串,无联合类型问题;- 对象字面量显式接口(
Keys),STRINGS 构建走buildStrings()工厂。
十、性能与内存
- 每次 build 调用
t()约 20 次(Map 查找),毫秒级;i18n.System只调 3 次且是轻量读取; - 无定时器、无监听器(旧
System.on已弃用),无内存泄漏; activeLang()是纯函数,无缓存需求;- 真实产品的性能注意:若页面极复杂,可在
onPageShow缓存系统语言,避免每次 build 读系统 API。
十一、小结
本应用用 274 行代码收束了整个系列:i18n.System 读取设备信息、activeLang() 合成生效语言、双源取词划分元信息与内容、模拟器把真机限制变成教学优势。它演示的语言策略,正是真实产品(微信、Chrome、Office)都在用的模式——默认跟随系统,高级选项允许覆盖。
| 应用 | 深化维度 |
|---|---|
| 01 | 应用内语言切换(纯手动方案) |
| 12 | 资源限定符(跟随系统的基础设施) |
| 14 | 跟随系统 vs 应用内覆盖(策略层)← 本文 |
给生产环境的三条铁律:① 默认跟随系统(用户零操作即正确本地化),覆盖作为高级选项;② 用户的手动选择必须持久化,且与系统语言独立存储;③ 系统语言变化后,在 onPageShow 刷新界面(替代已废弃的监听 API)——配合资源限定符自动重匹配,做到"用户改系统设置,App 立即响应"。
更多推荐




所有评论(0)