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

一、它解决什么场景问题

一款面向全球发行的新闻 App,语言不是一个设置项,而是产品形态本身。本文不讨论"怎么翻译",而是讨论:当用户把语言从中文切成英文,产品里到底哪些东西该变、哪些不该变、怎么变才自然。

本应用以"多语言新闻阅读器"为业务载体,完整演示了 8 种语言切换时,一个真实内容产品的全链路响应。它回答的是三个生产级问题:

  1. 语言选择体验:用户怎么选语言、选完是否记住、切换是否即时生效;
  2. 内容本地化:新闻标题、摘要、正文、分类、时间、数字、阅读量,哪些要跟着语言变;
  3. 降级兜底:当某条文案没翻译、某语言没覆盖时,用户看到什么而不是崩溃。

二、典型用户故事

故事 A:留学生小林(zh_CN → en_US)
小林刚到美国留学,系统是英文的,但看新闻还是习惯中文。他把 App 语言切到简体中文后,发现连"18 分钟前""12.8 万阅读"这种细节都跟着变成中文习惯——而不是简单地把界面文字翻译一遍。这正是 i18n 与"翻译"的区别。

故事 B:日本商务人士田中(en_US → ja_JP)
田中常驻东京,但英文新闻源更全。他在英文界面看财经新闻,需要切回日语确认关键术语时,发现新闻详情页也在同一个语言体系里即时刷新——他刚才读到一半的文章,标题、正文、评论数全部变成日语,而不是回到列表重新找。这正是"单页分支 + 全局语言状态"设计带来的体验优势。

故事 C:阿拉伯语用户 Ahmed(默认 → ar_AE 未覆盖)
Ahmed 的系统语言是阿拉伯语,但本应用只内置 8 种语言,没有阿拉伯语。此时发生两件事:界面文案降级到默认中文;新闻正文降级到英文(国际语种)。他依然能完整阅读,只是语言不是母语——这就是降级策略的价值:宁可显示第二优先语言,绝不显示空白或报错。

故事 D:德国用户 Klaus(de_DE)
Klaus 用德语界面阅读科技新闻。他发现德语标题特别长(“HarmonyOS 6.1 veröffentlicht: KI bis in die Systemebene”),但卡片没有撑破布局——因为摘要被 maxLines(2) 截断、标题自适应换行。同时数字格式也变了:23145 显示为 “23.145”(德语用点做千分位),时间显示 “vor 8 Min.”。他意识到这个 App 不是"翻译了界面",而是"整个产品说德语"。

三、适用使用环境

环境类型 适配说明
多语言国家/地区发行 用户可自选语言,不依赖系统设置
跨境电商工具 业务人员看中英文切换的界面术语
教育演示 课堂上直观展示不同语言/日期/数字格式差异
旅游出行类 App 游客随时切到母语
开发者自测 快速验证文案与本地化覆盖是否完整

四、目标用户画像

  1. 海外用户:母语非中文,需要英文/法/德/西等语言;
  2. 跨境从业者:中英切换查看术语;
  3. 演示/教学者:需要多语言并行展示;
  4. 测试人员:回归验证 i18n 覆盖。

五、关键设计决策

1. 持久化

用户切换一次后,下次启动仍是同样的语言。若每次重启都回到默认,体验会非常差。用 @StorageLink + PersistentStorage 实现:

@StorageLink(STORAGE_LOCALE) currentLocale: string = DEFAULT_LOCALE;

aboutToAppear(): void {
  if (!AppStorage.get<string>(STORAGE_LOCALE)) {
    AppStorage.setOrCreate(STORAGE_LOCALE, DEFAULT_LOCALE);
  }
  PersistentStorage.persistProp(STORAGE_LOCALE, DEFAULT_LOCALE);
  this.timer = setInterval(() => { this.now = Date.now(); }, 60000);
}

@StorageLink 让语言成为全局响应式状态——任何页面、任何组件读取它都会在变化时自动重渲染;PersistentStorage.persistProp 把它落盘,重启后从磁盘恢复。

2. 即时生效

不要求退出重进,切换那一秒界面就变,反馈强、零门槛。因为 currentLocale 是响应式状态,赋值瞬间所有依赖它的 t()NumberFormatDateTimeFormat 全部重新执行:

private T(key: string): string {
  return t(this.currentLocale, key);   // 语言一变,这里返回的新文案驱动重渲染
}

3. 降级兜底

即使某条文案漏翻,也显示默认语言而非空白,保证"永远可读":

/** 三级降级:目标语言 → 默认语言 → key 本身 */
function t(code: string, key: string): string {
  const v = STRINGS.get(code)?.get(key);
  if (v !== undefined) {
    return v;
  }
  return STRINGS.get(DEFAULT_LOCALE)?.get(key) ?? key;
}

4. 内容层与 UI 层分开降级

UI 文案缺翻译 → 回退默认语言(面向用户);
新闻正文缺翻译 → 回退英文(面向全球,英文是事实标准)。
两个回退目标刻意不同,这是真实产品最常见的配置:

function newsBody(code: string, n: NewsItem): string {
  return n.bodies.get(code) ?? n.bodies.get(DEFAULT_LOCALE) ?? '';
}

六、相对时间与数字的本地化(场景核心)

新闻列表的"8 分钟前""12.8 万阅读"是 i18n 最容易漏掉的地方。本应用用两个函数统一处理:

/** 相对时间:按分钟数分档,每档文案来自文案表 */
function relativeTime(code: string, min: number): string {
  if (min < 1) {
    return t(code, K.justNow);          // 刚刚 / just now / gerade eben
  }
  if (min < 60) {
    return tf(code, K.minAgo, min);     // {n} 分钟前 / {n} min ago / vor {n} Min.
  }
  const hours = Math.floor(min / 60);
  if (hours < 24) {
    return tf(code, K.hourAgo, hours);  // {n} 小时前 / {n} hr ago
  }
  return tf(code, K.dayAgo, Math.floor(hours / 24));  // {n} 天前
}

注意阿拉伯语的词序:قبل {n} دقيقة(“在 n 分钟之前”)与中文的"n 分钟前"结构完全不同——所以不能把中文模板硬编码成"n 分钟前",必须让每种语言用自己的占位符模板。

数字格式化交给 intl.NumberFormat

private num(n: number): string {
  try {
    return new intl.NumberFormat(this.currentLocale).format(n);
  } catch (err) {
    return String(n);   // 兜底:格式化失败时直接转字符串
  }
}

23145 在不同语言下的呈现:23,145(英文)、23.145(德语)、٢٣٬١٤٥(阿拉伯语)。数字不是"翻译"出来的,是"格式化"出来的——这是 i18n 的核心认知。

七、边界与降级

  • 不支持的语言:App 只内置 8 种,用户系统语言不在列表时默认简体中文——可用系统语言检测后再选最接近项;
  • 文案缺失:走三级降级(目标→默认→key);
  • 日期极老/未来Intl.DateTimeFormat 对合法 Date 均能格式化,越界请在传入前校验;
  • RTL 语言:本应用文案本身方向变化由系统排布接管,复杂排版见应用 07。

八、竞品对比(同类方案取舍)

方案 优点 缺点 本应用选择
系统资源限定符(应用 12) 官方标准、零代码 动态切换需重启或特殊处理 应用内切语言场景用文案表
运行时文案表(本应用) 即时生效、可热更新 需自行维护 key 对应关系
云端热更新 无需发版补翻 依赖网络、需配置 CDN 扩展方向

九、扩展方向

  • 语言记忆到云端:登录后跨设备同步语言偏好;
  • 自动跟随系统语言:见应用 14;
  • 文案热更新:把字符串放到远端 JSON,无需发版即可补翻;
  • 引用语义化多语言:与资源限定符方案结合(应用 12)。

九、一次语言切换的完整数据流(场景复盘)

把"用户点击阿拉伯语"到"界面全变"的每一步拆开,能看清 i18n 的响应式本质:

用户点击「العربية」标签
   │ 1. 触发 onClick
   ▼
this.currentLocale = 'ar_AE'          // @StorageLink 赋值
   │ 2. AppStorage 广播变更
   ▼
PersistentStorage 落盘                 // 重启后恢复
   │ 3. 依赖 currentLocale 的 UI 订阅
   ▼
t() / tf() 重新执行                    // 文案:导航、分类、按钮
intl.NumberFormat 重新执行             // 数字:阅读量 ٢٣٬١٤٥
intl.DateTimeFormat 重新执行           // 时间:قبل 8 دقيقة
newsTitle/Summary/Body 重新执行        // 内容:标题/摘要/正文
   │ 4. ArkUI 增量渲染
   ▼
界面整体刷新,无空白、无闪烁

关键点:语言是一个"状态",不是"一次操作"。开发者只需要保证所有 UI 文本都从 currentLocale 派生,切换时框架自动完成重渲染——不需要手动遍历每个控件去改文本。这正是声明式 UI(ArkUI)相对命令式 UI 的优势:状态驱动视图,而非代码驱动视图。

十、生产级注意事项

  1. 文案表与代码解耦STRINGS 表集中管理所有 key,翻译人员只改表不动代码;若接入翻译平台,key 就是唯一契约;
  2. 占位符规范:含数字的文案统一用 {n} 占位(如 {n} 分钟前),不要写死在模板里——不同语言数字位置不同;
  3. 语言代码规范:用 BCP-47(zh_CNen_US),不要自造 zh-cn 之类的变体,否则 intl 无法识别;
  4. 缺省语言必配DEFAULT_LOCALE 必须保证所有 key 都有值,它是降级的最终兜底;
  5. 测试矩阵:每个语言都要过一遍"列表→详情→切语言→返回"全流程,重点验证长文案溢出、数字格式、时间格式三处。

十一、结语

多语言切换器不是"入门第一课",而是 i18n 的骨架课——它把语言选、存、用、显四个环节全部打通,并且用真实新闻产品演示了"一次切换、五层联动"(导航、列表、详情、时间、数字)。

Logo

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

更多推荐