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

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

产品要做一个全球 App 的注册流程:用户创建账户时选择"国家/地区、语言、货币",之后整个 App 的本地化(界面语言、日期格式、数字格式、结算币种)都基于这次选择。核心需求:

  • 8 个地区 × 8 种语言 × 8 种货币的自由组合,但默认按地区智能联动(选日本 → 建议日语 + 日元);
  • 选择结果即时预览:完整 locale(如 zh-Hans-CN)下的日期格式与数字格式;
  • 地区名、语言名本地化显示(中文界面显示"美国",英文界面显示 “United States”)。

技术难点在于:locale 的拼合规则(带 script 的语言用连字符、短码用下划线)与联动的默认值算法。本文记录完整技术方案,所有代码与 LocaleRegionPicker.ets 一一对应。

二、总体架构

┌─ 地区层:REGIONS 元数据(code/flag/lang 建议/currency 建议)
├─ 语言层:LANGS 语言码数组(8 种)+ 界面 STRINGS 文案表
├─ 名称层:regionName()/langName() 用 getDisplayCountry/getDisplayLanguage 本地化
├─ 拼合层:fullLocale() 语言-地区组合规则(script 连字符 / 短码下划线)
├─ 预览层:fmtDate()/fmtNum() 用完整 locale 格式化日期与数字
└─ 状态层:@StorageLink currentLocale + @State regionIdx/langIdx/currencyIdx

架构核心是**“建议值"与"用户值"的分离**:selectRegion() 提供建议(自动改 langIdx/currencyIdx),但三个 idx 都是独立状态,用户随时覆盖——联动是"初始化建议”,不是"锁定选择"。

三、数据层:地区元数据与语言列表

interface Region {
  code: string;        // 地区码(ISO 3166-1)
  flag: string;
  lang: string;        // 建议语言(BCP-47)
  currency: string;    // 建议货币(ISO 4217)
}

const REGIONS: Region[] = [
  { code: 'CN', flag: '🇨🇳', lang: 'zh-Hans', currency: 'CNY' },
  { code: 'US', flag: '🇺🇸', lang: 'en', currency: 'USD' },
  { code: 'JP', flag: '🇯🇵', lang: 'ja', currency: 'JPY' },
  { code: 'KR', flag: '🇰🇷', lang: 'ko', currency: 'KRW' },
  { code: 'FR', flag: '🇫🇷', lang: 'fr', currency: 'EUR' },
  { code: 'DE', flag: '🇩🇪', lang: 'de', currency: 'EUR' },
  { code: 'GB', flag: '🇬🇧', lang: 'en', currency: 'GBP' },
  { code: 'IN', flag: '🇮🇳', lang: 'hi', currency: 'INR' }
];

const LANGS: string[] = ['zh-Hans', 'zh-Hant', 'en', 'ja', 'ko', 'fr', 'de', 'hi'];

三个设计决策:

  1. lang/currency 作为"建议值"挂在地区上:每个地区一行,注册时"选地区 → 自动带出建议语言与货币";
  2. 多对多关系自然呈现en 同时出现在 US 与 GB、EUR 同时属于 FR 与 DE——数据冗余在这里是刻意的,因为"建议值"本来就是按地区查的;
  3. code 用 ISO 3166-1、currency 用 ISO 4217、lang 用 BCP-47:三个国际标准码,保证下游 getDisplayCountry/getDisplayLanguage/NumberFormat 都能识别。

四、名称本地化:getDisplayCountry / getDisplayLanguage

private regionName(code: string): string {
  try {
    return i18n.getDisplayCountry(code, this.currentLocale, true);
  } catch (err) {
    return code;
  }
}

private langName(code: string): string {
  try {
    return i18n.getDisplayLanguage(code, this.currentLocale, true);
  } catch (err) {
    return code;
  }
}

两个 API 的语义:

API 输入 输出(中文界面) 输出(英文界面)
getDisplayCountry('JP', 'zh_CN') 国家码 日本 Japan
getDisplayLanguage('ja', 'zh_CN') 语言码 日语 Japanese
getDisplayLanguage('zh-Hans', 'zh_CN') 带 script 的语言码 简体中文 Simplified Chinese

关键点:

  • 第三个参数 true:完整名称(zh_CN 显示"简体中文"而非"中文"),与 11 应用的国家名同一模式;
  • getDisplayLanguage 支持带 script 的码zh-Hans(简体)与 zh-Hant(繁体)都能正确显示,不会都显示成"中文"——script 是语言码的一部分,不是后缀装饰;
  • try/catch 兜底返回原码:未知国家/语言码时显示码本身,不崩溃。

五、核心:fullLocale() 拼合规则

/** 组合完整 locale:language-region(如 zh-Hans-CN / ja-JP) */
private fullLocale(): string {
  const lang = this.curLang();
  // 已有 script 的语言(zh-Hans)直接拼,否则用短码
  if (lang.includes('-')) {
    return `${lang}-${this.curRegion().code}`;
  }
  return `${lang}_${this.curRegion().code}`;
}

这是本应用最容易被忽略的技术点——locale 拼合的两种写法:

语言码 拼合结果 说明
zh-Hans(带 script) zh-Hans-CN BCP-47 连字符风格
ja(短码) ja_JP CLDR 下划线风格(兼容 intl)

为什么两种风格混用?因为 intl.DateTimeFormat/intl.NumberFormat 的 locale 参数:

  • zh-Hans-CN(BCP-47 连字符)是标准写法,intl 完全支持;
  • ja_JP(下划线)是旧式 CLDR 写法,intl 也接受;
  • zh_Hans_CN(下划线 + script)在某些实现下解析不稳,ja-JP(连字符短码)也可能有兼容问题。

实践结论:带 script 的语言(zh-Hans)用连字符拼 zh-Hans-CN,无 script 的短码(ja)用下划线拼 ja_JP——本应用用 includes('-') 判断,保证两种风格各自落到最兼容的写法。生产环境统一用 BCP-47(zh-Hans-CNja-JP)更规范,本应用展示两种写法是因为真实代码里两种风格并存。

六、预览层:日期与数字格式化

private fmtDate(): string {
  try {
    const fmt = new intl.DateTimeFormat(this.fullLocale(), { dateStyle: 'full' });
    return fmt.format(new Date(2026, 7, 19));
  } catch (err) {
    return new Date(2026, 7, 19).toString();
  }
}

private fmtNum(): string {
  try {
    const fmt = new intl.NumberFormat(this.fullLocale(), { maximumFractionDigits: 2 });
    return fmt.format(1234567.89);
  } catch (err) {
    return '1234567.89';
  }
}

完整 locale 决定一切——同一日期、同一数字,随 regionIdx/langIdx 组合变化:

组合 日期(2026-08-19) 数字(1234567.89)
zh-Hans-CN 2026年8月19日星期三 1,234,567.89
ja-JP 2026年8月19日水曜日 1,234,567.89
fr-FR mercredi 19 août 2026 1 234 567,89
de-DE Mittwoch, 19. August 2026 1.234.567,89
hi-IN बुधवार, 19 अगस्त 2026 १२,३४,५६७.८९

注意印地语(hi-IN)的数字显示印度式分组(१२,३४,५६७.८९)——数字的本地化不只是小数点与千分位,还包括数字系统本身(梵文数字)。格式化器按 locale 的 CLDR 数据自动选择,业务代码零干预。

七、联动算法:selectRegion()

private selectRegion(i: number): void {
  this.regionIdx = i;
  // 联动:地区 → 建议语言 + 货币
  const r = REGIONS[i];
  const li = LANGS.findIndex((l: string) => l === r.lang);
  if (li >= 0) {
    this.langIdx = li;
  }
  const ci = REGIONS.findIndex((x: Region) => x.currency === r.currency);
  if (ci >= 0) {
    this.currencyIdx = ci;
  }
}

联动算法两个细节:

  1. 语言按值匹配LANGS.findIndex(l => l === r.lang)——把地区建议的语言码在语言列表里定位,命中才改 langIdxif (li >= 0) 防御:建议语言不在列表时保持用户原选择);
  2. 货币按币种码匹配REGIONS.findIndex(x => x.currency === r.currency)——法国/德国都返回第一个 EUR 的索引(FR),用户看到的币种徽章是"🇫🇷 EUR",虽然币种正确但地区关联是法国的——这是演示级简化,真实产品应独立维护货币列表而不是复用 REGIONS;
  3. 联动不锁定:改语言/货币后,再点地区才重新联动;用户选完语言再选货币,语言保持——三个 idx 各自独立,只有"点地区"这一个入口触发联动。

八、状态管理与持久化

@StorageLink(STORAGE_LOCALE) currentLocale: string = DEFAULT_LOCALE;
@State regionIdx: number = 0;
@State langIdx: number = 0;
@State currencyIdx: number = 0;

aboutToAppear(): void {
  if (!AppStorage.get<string>(STORAGE_LOCALE)) {
    AppStorage.setOrCreate(STORAGE_LOCALE, DEFAULT_LOCALE);
  }
  PersistentStorage.persistProp(STORAGE_LOCALE, DEFAULT_LOCALE);
}
  • currentLocale(界面语言)持久化,三个步骤 idx 不持久化——注册流程是瞬态的,重进页面回到默认地区 CN 合理;
  • 界面语言与账户语言再次分离currentLocale 只控制页面文案与名称显示语言;账户的"默认语言"由 langIdx 决定——注册完成后,账户语言应单独存库(fullLocale() 结果),本页用 Toast 演示这一传递。

九、数据流复盘(一次完整交互)

启动 → aboutToAppear 初始化 → 中文界面、默认 CN
  → 地区徽章:中国(选中)/美国/日本…
  → 预览卡:zh-Hans-CN · CNY / 2026年8月19日星期三 / 1,234,567.89

用户点 🇯🇵 日本
  → selectRegion(2):regionIdx=2
  → 联动:LANGS 找到 'ja' → langIdx=3(日语选中)
  → 联动:REGIONS 按 currency=JPY → currencyIdx=2(JPY 选中)
  → 预览卡:ja_JP · JPY / 2026年8月19日水曜日 / 1,234,567.89

用户把语言改选 English(langIdx=2)
  → 预览卡:en_JP · JPY / Wednesday, August 19, 2026 / 1,234,567.89
  → 语言被覆盖,货币仍 JPY——三个状态各自独立

用户点完成注册
  → Toast:注册配置: 日本 · English · JPY(名称本地化 + 币种码)

十、ArkTS 兼容要点

  1. catch (err) 不带类型注解,所有 i18n/intl 调用包 try/catch;
  2. ForEach(['zh_CN', ...] as string[], ...) 数组字面量断言;
  3. ForEach key 唯一性:地区 r.code、语言 l、货币 `${r.code}-${idx}`(REGIONS 重复遍历需要区分);
  4. fullLocale()includes('-') 判断 script——字符串方法无类型问题;
  5. 对象字面量显式接口(Region),REGIONS/LANGS 编译期常量;
  6. selectRegion()findIndex 的防御判断(li >= 0/ci >= 0)避免越界赋值。

十一、性能与内存

  • 每次 build 创建 2 个格式化器(日期 + 数字)+ 若干 getDisplay* 调用,毫秒级;
  • REGIONS/LANGS 常量无运行时开销;
  • 页面无定时器、无监听器,无内存泄漏;
  • 生产优化:getDisplayCountry/getDisplayLanguage 结果可按 code + locale 缓存(名称不随状态变,只随界面语言变),避免列表滚动时重复查询。

十二、小结

本应用把"注册流程"这个最普通的业务场景做成了 i18n 技术展示:getDisplayCountry/getDisplayLanguage 名称本地化、fullLocale() 拼合规则、selectRegion() 联动算法、完整 locale 驱动的日期数字预览。技术上最值得记的三点:locale 拼合要区分 script 语言与短码、联动是"建议"不是"锁定"、格式化永远用完整 locale(语言+地区)而不是只有语言。

应用 深化维度
01 语言切换与持久化(语言维度)
03 货币格式化(货币维度)
08 语言 + 地区 + 货币三维联动(组合维度)← 本文
11 getDisplayCountry 国家名(名称维度)
Logo

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

更多推荐