我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~

前言

实话讲,我第一次给 HarmonyOS 做多语言时,心里那个轻松:“翻译下文案不就完了?” 三天后被德语长句、阿语 RTL、复数规则、货币格式、图片带字、分布式多端一致性轮番教育,才知道——国际化不是锦上添花,而是地基工程。这篇就来把 鸿蒙国际化适配(i18n) 从设计到工程一次掰开揉碎,有原则、有工具、有代码、能落地;看完你就能少掉两周坑。🙂

一、先立规矩:语言≠地区≠脚本

  • 语言 Languageenzhar
  • 地区 RegionUSGBCNSA …(日期/货币/单位各不相同)
  • 脚本 ScriptHans(简体)、Hant(繁体)、Latn(拉丁)、Cyrl(西里尔)

反问一句:“en 就够用了吧?”
不够。en-US 的日期是 Apr 05, 2026en-GB05 Apr 2026货币符号位置也会变。别偷懒。

二、资源怎么放:目录、命名、回退策略一次定死

1)目录结构(示例)

/resources
  /base/element/string.json        # 默认词条(建议英文或中文,二选一要统一)
  /base/media/                      # 与语言无关的图
  /en_US/element/string.json
  /zh_Hans_CN/element/string.json
  /zh_Hant_TW/element/string.json
  /ar_SA/element/string.json
  /en_US/media/hero.png             # 语言相关图片(如带字图)

2)词条命名(别再 title1/title2 了)

  • 规则:模块_页面_用途[_状态],如 account_login_titleorder_detail_empty_desc
  • 占位符全部命名参数{user}{count},拒绝 {0} {1}
{
  "string": [
    { "name": "app_name", "value": "Roaming Notes" },
    { "name": "welcome", "value": "Welcome, {user}!" },
    { "name": "items_one", "value": "{count} item left" },
    { "name": "items_other", "value": "{count} items left" },
    { "name": "delete_confirm", "value": "Delete “{name}”?" }
  ]
}

3)优雅回退

  • 匹配顺序:lang_Script_Region → lang_Script → lang → base
  • 永不崩溃:缺词条用 key 展示并埋点;必要时回退英文。

三、ArkTS 实战:应用内切换语言(不依赖系统设置)

很多产品要“应用内语言切换”。做法:自建 i18n 管理器 + 资源读取 + 全局状态刷新。

// /common/i18n/I18n.ts
import resourceManager from '@ohos.resourceManager';
export type Locale = { language: string; region?: string; script?: string };

export class I18n {
  private rm?: resourceManager.ResourceManager;
  private static _cur: Locale = { language: 'en', region: 'US' };
  private cache = new Map<string, string>();

  async init(ctx: UIAbilityContext, locale?: Locale) {
    this.rm = (ctx as any).resourceManager;
    if (locale) I18n._cur = locale;
    this.cache.clear();
  }

  setLocale(locale: Locale) { I18n._cur = locale; this.cache.clear(); }
  get locale(): Locale { return I18n._cur; }

  async t(key: string, params?: Record<string, string | number>): Promise<string> {
    if (this.cache.has(key)) return this.format(this.cache.get(key)!, params);
    const v = await this.tryGet(key);
    this.cache.set(key, v);
    return this.format(v, params);
  }

  private async tryGet(key: string): Promise<string> {
    if (!this.rm) return key;
    // 新版 SDK 可直接传 locale;老版可手工选择目录,本处演示逻辑
    try { return await this.rm.getStringByName(key, I18n._cur as any); }
    catch { try { return await this.rm!.getStringByName(key); } catch { return key; } }
  }

  private format(tpl: string, params?: Record<string, string | number>): string {
    if (!params) return tpl;
    return tpl.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? ''));
  }
}
export const i18n = new I18n();

页面侧糖封装(同步体验)

// /common/i18n/useT.ts
import { i18n } from './I18n';
export function tSync(key: string, params?: Record<string, string|number>): string {
  // 轻量同步:命中缓存直接返回;miss 时先返回 key,再异步刷新状态
  return (i18n as any).cache?.get(key) ?? key.replace(/_/g, ' ');
}

使用示例

// /pages/Settings.ets
import { i18n } from '../common/i18n/I18n';

@Entry
@Component
struct SettingsPage {
  @State localeText: string = 'en-US';

  async aboutToAppear() {
    await i18n.init(getContext(this), { language: 'en', region: 'US' });
  }

  build() {
    Column({ space: 12 }) {
      Text('Language').fontSize(20).fontWeight(FontWeight.Bold)
      Row() {
        Button('English').onClick(() => this.switchTo({ language: 'en', region: 'US' }))
        Button('简体中文').onClick(() => this.switchTo({ language: 'zh', script: 'Hans', region: 'CN' }))
        Button('العربية').onClick(() => this.switchTo({ language: 'ar', region: 'SA' }))
      }
      Text(this.localeText)
      // 示例文案
      Text($rawfile('icon_info') ? '' : '') // 占位行,仅示意可混合资源
    }.padding(24)
  }

  private async switchTo(loc) {
    i18n.setLocale(loc)
    this.localeText = `${loc.language}${loc.script ? '-' + loc.script : ''}${loc.region ? '-' + loc.region : ''}`
    // 刷新页面:实际可结合全局状态/路由重建根组件
  }
}

四、复数 / 性别 / 条件:别让英语俄语阿语“将就中文语序”

1)轻量复数策略(one / other)

function pluralKey(cnt: number, locale: string) {
  if (locale.startsWith('en')) return cnt === 1 ? 'one' : 'other';
  if (locale.startsWith('zh')) return 'other';
  // 可按 CLDR 规则扩展 ru、ar …
  return 'other';
}
async function itemsText(cnt: number) {
  const key = `items_${pluralKey(cnt, i18n.locale.language)}`;
  return await i18n.t(key, { count: cnt });
}

2)ICU Message(进阶玩法)

如果你接入 ICU 消息解析(或自写简化版),可以写:

"{count, plural, one{# item left} other{# items left}}"

同理还有 select 支持性别:{gender, select, male{He} female{She} other{They}} …

五、日期 / 数字 / 货币 / 单位:请交给 @ohos.intl

手搓字符串是灾难:千分位、货币符号位置、小数点、负号、比例格式都不一样。

import intl from '@ohos.intl';

export const fmt = {
  date(ts: number, locale: string) {
    return new intl.DateTimeFormat(locale, { year:'numeric', month:'short', day:'2-digit' }).format(new Date(ts));
  },
  currency(v: number, currency: 'USD'|'EUR'|'CNY', locale: string) {
    return new intl.NumberFormat(locale, { style:'currency', currency }).format(v);
  },
  number(v: number, locale: string, max=2) {
    return new intl.NumberFormat(locale, { maximumFractionDigits: max }).format(v);
  }
}

// 单位示例:千米↔英里
export function kmOrMi(km: number, locale: string) {
  const useMile = ['US','GB','LR','MM'].includes(i18n.locale.region ?? '');
  const v = useMile ? km * 0.621371 : km;
  const unit = useMile ? 'mi' : 'km';
  return `${fmt.number(v, locale)} ${unit}`;
}

六、RTL(从右到左):不仅是“把 UI 镜像一下”

  • 布局属性用逻辑方向paddingStart/paddingEndmarginStart/marginEnd,少用 left/right
  • 图标要镜像:返回箭头、进度箭头、播放/快进符号注意方向;徽标数字位置也要随方向切换。
  • 入场动效:LTR 习惯从左入场,RTL 建议从右入场;动效保持克制(≤300ms)。
// 小工具:根据语言自动切换 start/end 的间距
function inset(start: number, end: number) {
  const rtl = i18n.locale.language === 'ar' || i18n.locale.language === 'he';
  return rtl ? { start: end, end: start } : { start, end };
}

Row() {
  Image($r('app.media.icon_back'))
  Text('Title').fontSize(18)
}.padding(inset(16, 8))

七、图片 / 图标 / 颜色:非文字资源也“需要会外语”

  • 带字图片:放语言子目录;能用矢量图 + 文本叠加就别把字烤进 PNG。
  • 颜色含义:红色在西方常警示,但在节庆语境是喜庆;情境不同,文案/颜色搭配要过目
  • 插图与暗色模式:准备浅/深两套或使用着色滤镜,别让暗色下“白底刺眼”。

八、分布式一致:手机↔平板↔手表↔车机的“同一种语言”

  • 语言偏好同步:把 locale 存到分布式 KV,多端自动跟随(允许用户手动覆盖)。
  • 小屏文案:手表/车机不适合长句,同 key 提供 _compact 版本
  • 接力场景:任务从手机接力到平板,保持 locale 一致,避免“跳语言”。
// 伪代码:KV 同步语言
import distributedKVStore from '@ohos.data.distributedKVStore';
kv.put('i18n/locale', JSON.stringify(i18n.locale));
// 其他设备监听 'i18n/locale',调用 i18n.setLocale() 刷新

九、性能与包体:让多语言“吃不胖、跑得快”

  • 按需装载:主打市场语言随包,长尾语言用在线增量包(下载+SHA 校验+缓存)。
  • 词典缓存:i18n 管理器内存缓存 + 冷启动预热(≤50ms)。
  • 资源去重:相同图标别在每个语言目录复制粘贴。
  • 容错兜底:缺词条显示 key 并埋点;媒体资源失败回退通用图。

十、测试与上架 Checklist:不翻车靠习惯

1)伪本地化(Pseudo-Localization)

一键把文本拉长、加变体字符,提前暴露布局问题

// /scripts/pseudo_localize.ts(Node)
import fs from 'fs';
const map: Record<string, string> = { a:'á', e:'ë', i:'ï', o:'ô', u:'ü', A:'Å', E:'Ê', I:'Ï', O:'Ø', U:'Û' };
function pseudo(s: string) { return '[!! ' + s.replace(/[A-Za-z]/g, ch => map[ch] || ch) + ' !!]'; }

const j = JSON.parse(fs.readFileSync('resources/base/element/string.json','utf-8'));
j.string = j.string.map((x: any) => ({ ...x, value: pseudo(x.value) }));
fs.writeFileSync('resources/pseudo/element/string.json', JSON.stringify(j, null, 2));
console.log('pseudo done.');

2)覆盖用例

  • 应用内切换语言,页面、弹层、路由标题同步刷新
  • 复数/性别/条件规则正确(≥3 种语言抽测)
  • 日期、货币、数字、单位格式符合地区习惯
  • RTL:导航、列表、返回箭头、入场动效自然
  • 离线:无网加载词典与图片是否回退
  • 分布式:多端语言一致,断网重连不丢
  • 性能:切语言重建 ≤ 300ms;首屏 i18n 预热 ≤ 50ms

十一、可直接拿走的工程脚手架

harmony-i18n-kit/
├─ common/i18n/
│  ├─ I18n.ts                 # 管理器(缓存、回退、切换)
│  ├─ useT.ts                 # 同步 t() 包装
│  └─ plural.ts               # CLDR 规则扩展点
├─ resources/
│  ├─ base/element/string.json
│  ├─ en_US/element/string.json
│  ├─ zh_Hans_CN/element/string.json
│  ├─ ar_SA/element/string.json
│  └─ pseudo/element/string.json
├─ scripts/
│  ├─ extract_keys.ts         # 扫描 $t('key') 导出缺失清单
│  ├─ check_missing.ts        # 校验各语言键集合一致
│  └─ pseudo_localize.ts      # 伪本地化生成
└─ pages/
   └─ Settings.ets

extract_keys.ts 思路(片段)

// 用简单正则从 .ets / .ts 里找 $t('key');严谨可用 AST
const pat = /\$t\(\s*['"`]([a-z0-9_\.]+)['"`]/ig;

十二、常见“秒翻车”清单(我先替你踩了)

  • 把日期/货币手拼字符串 → 千分位、货币符号、负号样式全错
  • 词条硬编码在组件里 → 翻译永远漏
  • 按钮固定宽度 → 德语/芬兰语直接爆布局
  • 箭头不镜像 → RTL 中“返回”像“前进”
  • 只存语言不存地区 → en-US 用户看到 en-GB 日期
  • 多端不同步 → 手机改英语,手表还中文

尾巴压个重点:国际化是“持续工程”,不是“一次翻译”

在鸿蒙里把 i18n 打磨到位,你会发现:令牌化设计、逻辑方向布局、标准化格式化、复数/性别模板、分布式一致与兜底回退,一旦“工程化”,后续加语言就是搬砖而不是重建。下次再有人说“把中文改成英文就好了吧”,你就笑笑:“试试德语和阿语先?” 😉

(未完待续)

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐