在这里插入图片描述
在这里插入图片描述

一、业务需求(为什么这么设计)

本应用是系列的收官之作,回答"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 韩语(用户选择)

两个关键设计:

  1. 默认 followSystem = true:真实设备上,跟随系统的 App 在用户改系统语言后需要"重新进入页面"(或页面 onShow)才能看到新语言——本页用 simSystemLang 模拟这个外部变量,让变化即时可见;
  2. 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 兼容要点

  1. catch (err) 不带类型注解,所有 i18n.System 调用包 try/catch(数据缺失兜底);
  2. ForEach(['zh_CN', ...] as string[], ...) 数组字面量断言;
  3. ForEach(['wifi', 'bluetooth', ...] as string[], (key: string) => key):设置项 key 列表作为 ForEach 数据源,t(activeLang(), key) 在回调内取词——key 既是数据标识又是文案键,天然唯一;
  4. Toggle({ type: ToggleType.Switch, isOn: this.followSystem })isOn 单向绑定 + onChange 回写,避免双向绑定类型问题;
  5. langLabel() if 链返回字符串,无联合类型问题;
  6. 对象字面量显式接口(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 立即响应"。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐