Skip to content

10_Engineering/08 - 国际化(i18n)

1. 国际化概述

国际化(Internationalization, i18n)是指应用支持多语言、多文化的能力。HarmonyOS 提供了完整的国际化方案,通过资源限定符自动切换语言。

1.1 国际化 vs 本地化

概念说明
国际化 (i18n)让应用能够适配不同地区/语言
本地化 (l10n)将应用适配到具体地区的语言和文化
全球化 (g11n)国际化的上层概念,涵盖所有地区

2. 资源限定符体系

2.1 语言/地区限定符

resources/
├── base/                    # 默认语言(回退)
│   └── element/
│       ├── string.json      # 字符串
│       └── color.json       # 颜色
├── zh_CN/                   # 简体中文
│   └── element/
│       └── string.json
├── en_US/                   # 美式英语
│   └── element/
│       └── string.json
├── en_GB/                   # 英式英语
│   └── element/
│       └── string.json
├── ja_JP/                   # 日语
│   └── element/
│       └── string.json
├── ko_KR/                   # 韩语
│   └── element/
│       └── string.json
├── fr_FR/                   # 法语
│   └── element/
│       └── string.json
└── de_DE/                   # 德语
    └── element/
        └── string.json

2.2 语言代码规范

zh_CN  = Chinese (China)
zh_TW  = Chinese (Taiwan)
en_US  = English (United States)
en_GB  = English (United Kingdom)
ja_JP  = Japanese (Japan)
ko_KR  = Korean (Korea)
fr_FR  = French (France)
de_DE  = German (Germany)
es_ES  = Spanish (Spain)
ar_SA  = Arabic (Saudi Arabia)

3. 多语言资源配置

3.1 string.json 配置

json5
// base/element/string.json (默认语言 - 中文)
{
  "string": [
    { "name": "app_name", "value": "我的应用" },
    { "name": "welcome", "value": "欢迎使用" },
    { "name": "login", "value": "登录" },
    { "name": "logout", "value": "退出登录" },
    { "name": "hello_format", "value": "你好,{0}!" },
    { "name": "price_format", "value": "¥{0}" },
    { "name": "confirm", "value": "确认" },
    { "name": "cancel", "value": "取消" },
    { "name": "language", "value": "语言" }
  ]
}

// en_US/element/string.json (美式英语)
{
  "string": [
    { "name": "app_name", "value": "My App" },
    { "name": "welcome", "value": "Welcome" },
    { "name": "login", "value": "Login" },
    { "name": "logout", "value": "Logout" },
    { "name": "hello_format", "value": "Hello, {0}!" },
    { "name": "price_format", "value": "${0}" },
    { "name": "confirm", "value": "Confirm" },
    { "name": "cancel", "value": "Cancel" },
    { "name": "language", "value": "Language" }
  ]
}

// ja_JP/element/string.json (日语)
{
  "string": [
    { "name": "app_name", "value": "マイアプリ" },
    { "name": "welcome", "value": "ようこそ" },
    { "name": "login", "value": "ログイン" },
    { "name": "logout", "value": "ログアウト" },
    { "name": "hello_format", "value": "こんにちは、{0}さん!" },
    { "name": "price_format", "value": "¥{0}" },
    { "name": "confirm", "value": "確認" },
    { "name": "cancel", "value": "キャンセル" },
    { "name": "language", "value": "言語" }
  ]
}

3.2 颜色国际化

json5
// base/element/color.json
{
  "color": [
    { "name": "primary_color", "value": "#FF0080" },
    { "name": "bg_color", "value": "#F1F3F5" },
    { "name": "text_color", "value": "#18181C" }
  ]
}

// en_US/element/color.json
{
  "color": [
    { "name": "primary_color", "value": "#0052CC" },  // 不同品牌色
    { "name": "bg_color", "value": "#FFFFFF" },
    { "name": "text_color", "value": "#1A1A1A" }
  ]
}

3.3 图片国际化

resources/
├── base/media/
│   ├── logo.png              # 默认 logo
│   └── icon_home.png
├── en_US/media/
│   └── logo.png              # 英文版 logo(可能需要不同文字)
└── ja_JP/media/
    └── logo.png              # 日文版 logo

4. 资源访问方式

4.1 UI 模板中访问

typescript
@Entry
@Component
struct I18nPage {
  build() {
    Column() {
      // 通过 $r 装饰器访问
      Text($r('app.string.welcome'))
        .fontSize(24)
        .fontWeight(FontWeight.Bold)

      Text($r('app.string.app_name'))
        .fontSize(18)

      // 带参数的字符串
      Text($r('app.string.hello_format', 'World'))

      // 按钮文本
      Button($r('app.string.login'))
        .width('100%')
        .height(48)
        .onClick(() => { this.navigateToLogin(); })
    }
    .width('100%')
    .height('100%')
  }

  navigateToLogin(): void {
    router.pushUrl({ url: 'pages/LoginPage' });
  }
}

4.2 代码中访问

typescript
import { resourceManager } from '@kit.AbilityKit';

class I18nService {
  private rm: ResourceManager;

  constructor() {
    this.rm = getContext(this).resourceManager;
  }

  /** 获取字符串 */
  getString(name: string): string {
    return this.rm.getStringByName(name);
  }

  /** 获取带参数的字符串 */
  getStringFormat(name: string, params: string[]): string {
    return this.rm.getStringByName(name, params);
  }

  /** 获取颜色 */
  getColor(name: string): string {
    return this.rm.getColorByName(name);
  }

  /** 获取当前语言 */
  getCurrentLanguage(): string {
    const locale = this.rm.getLocale();
    return locale.language + '_' + locale.country;
  }

  /** 设置语言 */
  setLanguage(lang: string): void {
    const locale = new Locale();
    if (lang.startsWith('zh')) {
      locale.language = 'zh';
      locale.country = 'CN';
    } else if (lang.startsWith('en')) {
      locale.language = 'en';
      locale.country = 'US';
    }
    // 注意:设置语言需要重新应用
  }
}

5. 运行时语言切换

5.1 语言选择器

typescript
@Entry
@Component
struct LanguageSelector {
  @State currentLang: string = 'zh_CN';
  private languages: string[] = ['zh_CN', 'en_US', 'ja_JP', 'ko_KR'];
  private langNames: Record<string, string> = {
    'zh_CN': '简体中文',
    'en_US': 'English',
    'ja_JP': '日本語',
    'ko_KR': '한국어',
  };

  build() {
    Column() {
      Text($r('app.string.language'))
        .fontSize(20)
        .margin({ bottom: 16 })

      ForEach(this.languages, (lang: string) => {
        Row() {
          Text(this.langNames[lang])
            .fontSize(16)
            .fontColor(lang === this.currentLang ? '#0080FF' : '#333')

          if (lang === this.currentLang) {
            Text('✓')
              .fontColor('#0080FF')
              .fontWeight(FontWeight.Bold)
          }
        }
        .width('100%')
        .height(48)
        .onClick(() => {
          this.switchLanguage(lang);
        })
        .borderRadius(8)
        .backgroundColor(lang === this.currentLang ? '#E8F4FF' : 'transparent')
      })
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }

  switchLanguage(lang: string): void {
    this.currentLang = lang;
    // 通过配置资源管理器切换语言
    const locale = new Locale();
    const parts = lang.split('_');
    locale.language = parts[0];
    locale.country = parts[1];
    // 重新配置 ResourceManager 以应用新语言
    this.applyLanguage(locale);
  }

  applyLanguage(locale: Locale): void {
    // 触发页面重新渲染
    // 实际项目中可通过 Context 或全局状态管理实现
  }
}

6. 国际化最佳实践

6.1 规范

规范说明
字符串参数化使用 {0}, {1} 占位符,而非字符串拼接
默认语言base 目录始终包含所有字符串
遗漏检测确保每种语言都有对应的字符串
复数处理注意不同语言的复数规则(en: 1 item / 2 items)
RTL 语言Arabic/Hebrew 需要 RTL 布局支持
日期/时间使用 Intl.DateTimeFormat 格式化
数字格式不同地区千分位/小数点不同

6.2 遗漏字符串处理

en_US 中缺少某个字符串时,会自动回退到 base 目录的字符串。

查找顺序:设备语言 → 对应语言目录 → base 目录

6.3 RTL(从右到左)语言

typescript
// 阿拉伯语需要 RTL 布局
@Entry
@Component
struct RTLPage {
  build() {
    Row() {
      // RTL 下自动从右到左排列
      Text($r('app.string.app_name'))
        .textDirection(TextDirection.RTL)
    }
    .width('100%')
    .height('100%')
    .direction(DirDirection.RTL)  // 整个布局 RTL
  }
}

7. 面试高频考点

Q1: 如何管理多语言字符串?

回答要点

  • 使用 resources/<lang>/element/string.json 按语言目录管理
  • base 目录放默认语言(回退用)
  • 通过 $r('app.string.name') 访问
  • 支持带参数 {0}, {1} 的字符串
  • 字符串参数化,不硬编码中文

Q2: 运行时如何切换语言?

回答要点

  • 通过 ResourceManager 设置 Locale
  • 重新渲染当前页面
  • 注意 RTL 语言(阿拉伯语/希伯来语)的布局翻转
  • 部分系统语言由设备决定,应用可覆盖

Q3: 资源回退机制是什么?

回答要点

  • 查找顺序:设备语言 → 对应语言目录 → base 目录
  • 如果 en_US 缺少某个字符串,自动回退到 base
  • 确保 base 包含所有字符串作为兜底
  • 这是 HarmonyOS 的默认行为,不需要额外配置

8. Android 对比

概念AndroidHarmonyOS
资源目录res/values-en/resources/en_US/
字符串文件strings.xmlstring.json
资源引用@string/name$r('app.string.name')
语言代码ISO 639-1 + 国家代码相同规范
RTL 支持layoutDirectiondirection + textDirection
格式化String.format$r with params
资源定位器Resources.getString()resourceManager.getStringByName()