在这里插入图片描述

每日一句正能量

不是每一颗贝壳里都有珍珠,但是珍珠一定在贝壳里,不是每个努力的人都能成功,但是成功的人一定很努力。
努力是成功的必要非充分条件,正如贝壳是珍珠的必要非充分载体。把努力当作增加概率的方式,而非兑换结果的支票。享受成为“有珍珠潜质的贝壳”本身,而不执着于每一枚贝壳都必须产出珍珠。

一、前言:为什么国际化不是简单的"翻译文件"

在全球化应用分发的大趋势下,HarmonyOS 应用面向 170+ 国家与地区的用户。字符串资源国际化(i18n)绝非将中文文本逐条翻译成英文后硬编码到代码中那么简单。一个真正具备工程化水准的国际化方案,需要解决以下核心问题:

  • 资源分级管理:如何按语言、地区、设备类型组织字符串资源?
  • 运行时动态切换:用户如何在应用内独立切换语言,且无需重启进程?
  • 格式化本地化:日期、货币、数字如何随区域自动适配?
  • RTL 布局适配:阿拉伯语、希伯来语等从右到左语言的完整支持。

本文基于 HarmonyOS API 12+ 与 ArkTS 声明式语法,从资源目录架构、字符串引用机制、应用内语言切换、格式化工具链到 RTL 适配,提供一套可直接落地的完整工程方案。


二、资源目录架构:BCP-47 标准与兜底机制

2.1 目录结构设计

HarmonyOS 的国际化资源系统采用**资源限定词(Qualifiers)**机制,在 src/main/resources/ 下按 语言_地区 格式创建独立目录。系统运行时根据设备 Locale 自动匹配最佳资源。

在这里插入图片描述

标准目录结构如下:

src/main/resources/
├── base/                      # 默认兜底资源(必须存在)
│   ├── element/
│   │   └── string.json        # 中文字符串(默认语言)
│   └── media/
│       └── logo.png
├── zh_CN/                     # 简体中文(中国大陆)
│   ├── element/
│   │   └── string.json
│   └── media/
│       └── logo.png
├── en_US/                     # 美式英语
│   ├── element/
│   │   └── string.json
│   └── media/
│       └── logo.png
├── ja_JP/                     # 日语
│   ├── element/
│   │   └── string.json
│   └── media/
│       └── logo.png
└── ar_SA/                     # 阿拉伯语(沙特阿拉伯,RTL)
    ├── element/
    │   └── string.json
    └── media/
        └── logo.png

关键规范:目录命名必须遵循 BCP-47 标准,格式为 语言代码_地区代码(如 zh_CNen_US)。仅写 zhen 在部分 API 版本下会导致匹配失败。

2.2 字符串资源文件定义

base/element/string.json 为例:

{
  "string": [
    { "name": "app_name", "value": "智行出行" },
    { "name": "welcome_title", "value": "欢迎使用智行出行" },
    { "name": "login_btn", "value": "登录" },
    { "name": "logout_btn", "value": "退出登录" },
    { "name": "price_format", "value": "¥%s" },
    { "name": "item_count", "value": "共 %d 件商品" }
  ]
}

对应 en_US/element/string.json

{
  "string": [
    { "name": "app_name", "value": "SmartTrip" },
    { "name": "welcome_title", "value": "Welcome to SmartTrip" },
    { "name": "login_btn", "value": "Login" },
    { "name": "logout_btn", "value": "Logout" },
    { "name": "price_format", "value": "$%s" },
    { "name": "item_count", "value": "%d items in total" }
  ]
}

三、字符串引用与参数化:静态绑定与动态读取

3.1 静态引用(编译期绑定)

在 ArkTS 组件中,通过 $r() 语法糖直接引用资源,编译器会自动将资源 ID 与当前语言环境绑定:

@Entry
@Component
struct HomePage {
  build() {
    Column({ space: 16 }) {
      // 直接引用字符串资源
      Text($r('app.string.welcome_title'))
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#262626')

      Button($r('app.string.login_btn'))
        .width('80%')
        .height(48)
        .fontSize(16)
        .backgroundColor('#1A73E8')
        .fontColor(Color.White)
    }
    .width('100%')
    .padding(24)
  }
}

3.2 动态读取与参数格式化(运行时)

当需要运行时动态获取字符串(如从网络数据拼接文案),需通过 resourceManager 接口:

import { resourceManager } from '@kit.LocalizationKit';
import { BusinessError } from '@kit.BasicServicesKit';

class StringResourceHelper {
  private context: Context;

  constructor(context: Context) {
    this.context = context;
  }

  /**
   * 获取带参数的格式化字符串
   * @param resId 资源 ID
   * @param args 格式化参数
   */
  async getFormattedString(resId: Resource, ...args: Array<string | number>): Promise<string> {
    try {
      const rm = this.context.resourceManager;
      // getStringSync 支持传入资源 ID 和格式化参数
      const result = rm.getStringSync(resId.id, ...args);
      return result;
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[i18n] 获取字符串失败: code=${error.code}, message=${error.message}`);
      return '';
    }
  }
}

// 使用示例
const helper = new StringResourceHelper(getContext(this));
const priceText = await helper.getFormattedString($r('app.string.price_format'), '2999.00');
// 中文环境: "¥2999.00"  英文环境: "$2999.00"

const countText = await helper.getFormattedString($r('app.string.item_count'), 5);
// 中文环境: "共 5 件商品"  英文环境: "5 items in total"

避坑指南$r() 在编译期解析,不支持运行时传参。带占位符的字符串必须通过 resourceManager.getStringSync() 在运行时动态格式化。


四、运行时语言切换:从系统跟随到应用内独立控制

4.1 系统语言自动适配(零代码)

HarmonyOS 默认行为是跟随系统语言。只要按规范放置资源文件,系统会在应用启动时自动匹配最佳语言,开发者无需编写任何切换逻辑。

4.2 资源匹配优先级机制

当系统语言为 zh_CN 时,资源加载器按以下优先级逐级匹配:

在这里插入图片描述

优先级 匹配规则 示例
1 精确匹配 语言_地区 zh_CNzh_CN/string.json
2 降级匹配 语言 zh_CNzh/string.json
3 脚本变体匹配 zh_CNzh-Hans/string.json
4 默认回退 base/string.json

工程建议:务必提供完整的 base 兜底目录。若用户系统语言为 fr_FR(法语),而应用未提供法语资源,系统将自动回退到 base 目录,避免应用崩溃或显示空白。

4.3 应用内独立切换语言(核心难点)

很多出海应用需要在设置页内独立切换语言,而不影响系统语言。实现方案如下:

步骤一:创建 I18nService 单例服务
// src/main/ets/service/I18nService.ets
import { i18n } from '@kit.LocalizationKit';
import { preferences } from '@kit.ArkData';
import { BusinessError } from '@kit.BasicServicesKit';

const PREF_KEY_LANGUAGE = 'app_preferred_language';
const PREF_KEY_REGION = 'app_preferred_region';

export class I18nService {
  private static instance: I18nService;
  private store: preferences.Preferences | null = null;
  private initialized: boolean = false;

  static getInstance(): I18nService {
    if (!I18nService.instance) {
      I18nService.instance = new I18nService();
    }
    return I18nService.instance;
  }

  /**
   * 初始化:从持久化存储恢复用户语言偏好
   */
  async init(context: Context): Promise<void> {
    if (this.initialized) return;
    this.store = await preferences.getPreferences(context, 'i18n_prefs');

    const savedLang = this.store.getSync(PREF_KEY_LANGUAGE, '') as string;
    if (savedLang) {
      this.applyLanguage(savedLang);
    }
    this.initialized = true;
  }

  /**
   * 获取当前应用生效的语言
   */
  getCurrentLanguage(): string {
    return i18n.System.getAppPreferredLanguage() || i18n.System.getSystemLanguage();
  }

  /**
   * 获取支持的语言列表
   */
  getSupportedLanguages(): Array<{ code: string; label: string; localLabel: string }> {
    return [
      { code: 'zh-Hans', label: '简体中文', localLabel: '简体中文' },
      { code: 'zh-Hant', label: '繁體中文', localLabel: '繁體中文' },
      { code: 'en',      label: 'English',  localLabel: 'English' },
      { code: 'ja',      label: '日本語',   localLabel: '日本語' },
      { code: 'ar',      label: 'العربية',  localLabel: 'العربية' },
      { code: 'ko',      label: '한국어',   localLabel: '한국어' },
    ];
  }

  /**
   * 切换语言(核心方法)
   */
  async switchLanguage(langCode: string): Promise<void> {
    // 1. 持久化保存用户选择
    if (this.store) {
      this.store.putSync(PREF_KEY_LANGUAGE, langCode);
      await this.store.flush();
    }
    // 2. 应用到运行时
    this.applyLanguage(langCode);
  }

  private applyLanguage(langCode: string): void {
    try {
      // 设置应用级语言覆盖(API 12+)
      i18n.System.setAppPreferredLanguage(langCode);
      console.info(`[I18nService] 语言已切换至: ${langCode}`);
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[I18nService] 切换语言失败: ${error.message}`);
    }
  }
}
步骤二:全局状态驱动 UI 重渲染

关键认知setAppPreferredLanguage 仅改变资源查找路径,已渲染的组件不会自动刷新!必须通过全局状态触发重建。

// 在 EntryAbility.onCreate 中初始化
import { I18nService } from '../service/I18nService';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 初始化国际化服务
    I18nService.getInstance().init(this.context);

    // 创建全局语言状态
    AppStorage.setOrCreate('currentLanguage', I18nService.getInstance().getCurrentLanguage());
  }
}
步骤三:语言设置页实现
// src/main/ets/pages/LanguageSettingPage.ets
import { I18nService } from '../service/I18nService';

@Entry
@Component
struct LanguageSettingPage {
  @StorageProp('currentLanguage') @Watch('onLanguageChanged') currentLanguage: string = 'zh-Hans';
  private i18nService = I18nService.getInstance();

  private onLanguageChanged(): void {
    // 语言变化时,通过 router 替换当前页面实现资源重载
    // 或使用 Navigation 的 replacePath
  }

  build() {
    Navigation() {
      Column({ space: 12 }) {
        Text($r('app.string.language_setting_title'))
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .margin({ top: 16, bottom: 8 })

        ForEach(this.i18nService.getSupportedLanguages(), (lang: { code: string; label: string; localLabel: string }) => {
          Row() {
            Column() {
              Text(lang.localLabel)
                .fontSize(16)
                .fontColor('#262626')
              Text(lang.code)
                .fontSize(12)
                .fontColor('#8C8C8C')
                .margin({ top: 2 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)

            Radio({ value: lang.code, group: 'lang_group' })
              .checked(this.currentLanguage === lang.code)
              .onChange(async (isChecked: boolean) => {
                if (isChecked && this.currentLanguage !== lang.code) {
                  await this.i18nService.switchLanguage(lang.code);
                  AppStorage.set('currentLanguage', lang.code);

                  // 方案 A:使用 Navigation replacePath 重载当前页(推荐)
                  // this.pathStack.replacePath({ name: 'LanguageSettingPage' });

                  // 方案 B:全局事件通知各页面刷新
                  emitter.emit({ eventId: 0x0001 }, { data: { lang: lang.code }});
                }
              })
          }
          .width('100%')
          .padding(16)
          .backgroundColor(Color.White)
          .borderRadius(12)
          .margin({ left: 16, right: 16 })
        })
      }
      .width('100%')
      .backgroundColor('#F5F5F5')
    }
    .title($r('app.string.language_setting_title'))
  }
}

在这里插入图片描述


五、日期、数字与货币的本地化格式化

字符串翻译只是国际化的第一步。日期格式、数字千分位、货币符号在不同区域差异巨大,必须使用 intl 模块进行本地化格式化。

5.1 日期时间格式化

import { intl, i18n } from '@kit.LocalizationKit';

export class DateTimeFormatter {
  /**
   * 根据当前 Locale 格式化日期
   */
  static formatDate(date: Date, style: 'short' | 'medium' | 'long' | 'full' = 'medium'): string {
    const locale = i18n.System.getSystemLocale(); // 如 "zh-Hans-CN"
    const formatter = new intl.DateTimeFormat(locale, { dateStyle: style });
    return formatter.format(date);
  }

  /**
   * 格式化相对时间(如"3分钟前")
   */
  static formatRelative(date: Date): string {
    const locale = i18n.System.getSystemLocale();
    const rtf = new intl.RelativeTimeFormat(locale, { numeric: 'auto' });

    const diffMs = Date.now() - date.getTime();
    const diffSec = Math.round(diffMs / 1000);
    const diffMin = Math.round(diffSec / 60);
    const diffHour = Math.round(diffMin / 60);
    const diffDay = Math.round(diffHour / 24);

    if (Math.abs(diffSec) < 60) return rtf.format(-diffSec, 'second');
    if (Math.abs(diffMin) < 60) return rtf.format(-diffMin, 'minute');
    if (Math.abs(diffHour) < 24) return rtf.format(-diffHour, 'hour');
    return rtf.format(-diffDay, 'day');
  }
}

// 使用示例
const now = new Date();
console.log(DateTimeFormatter.formatDate(now, 'long'));
// zh-Hans: "2026年7月27日"
// en-US:   "July 27, 2026"
// ja:      "2026年7月27日"

console.log(DateTimeFormatter.formatRelative(new Date(Date.now() - 180000)));
// zh-Hans: "3分钟前"
// en-US:   "3 minutes ago"

5.2 数字与货币格式化

export class NumberFormatter {
  /**
   * 格式化数字(自动千分位)
   */
  static formatNumber(value: number): string {
    const locale = i18n.System.getSystemLocale();
    return new intl.NumberFormat(locale).format(value);
  }

  /**
   * 格式化货币
   */
  static formatCurrency(value: number, currencyCode: string): string {
    const locale = i18n.System.getSystemLocale();
    return new intl.NumberFormat(locale, {
      style: 'currency',
      currency: currencyCode,
    }).format(value);
  }

  /**
   * 格式化百分比
   */
  static formatPercent(value: number): string {
    const locale = i18n.System.getSystemLocale();
    return new intl.NumberFormat(locale, { style: 'percent' }).format(value);
  }
}

// 使用示例
console.log(NumberFormatter.formatCurrency(2999, 'CNY'));
// zh-Hans: "¥2,999.00"
// en-US:   "CN¥2,999.00"
// ja:      "¥2,999"

console.log(NumberFormatter.formatNumber(1234567.89));
// zh-Hans: "1,234,567.89"
// de-DE:   "1.234.567,89"

六、RTL 布局适配:阿拉伯语与希伯来语的完整支持

阿拉伯语(ar)、希伯来语(he)、乌尔都语(ur)等语言采用从右到左(RTL)的阅读与布局方向。若不做适配,界面将出现严重的排版错乱。

6.1 RTL 检测与全局适配

import { i18n } from '@kit.LocalizationKit';

export class RTLUtils {
  /**
   * 判断当前语言是否为 RTL
   */
  static isRTL(): boolean {
    const lang = i18n.System.getAppPreferredLanguage() || i18n.System.getSystemLanguage();
    return i18n.isRTL(lang);
  }

  /**
   * 获取当前布局方向
   */
  static getDirection(): Direction {
    return this.isRTL() ? Direction.Rtl : Direction.Ltr;
  }

  /**
   * 获取逻辑方向边距(start/end 替代 left/right)
   */
  static getMargin(start: number, end: number): Margin {
    return {
      start: LengthMetrics.vp(start),
      end: LengthMetrics.vp(end),
    };
  }
}

6.2 组件级 RTL 适配

@Entry
@Component
struct ProductDetailPage {
  build() {
    Scroll() {
      Column({ space: 16 }) {
        // 顶部导航栏
        Row() {
          Image($r('app.media.ic_back'))
            .width(24)
            .height(24)
            // RTL 时水平翻转返回箭头
            .scale({ x: RTLUtils.isRTL() ? -1 : 1 })

          Text($r('app.string.product_detail'))
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .layoutWeight(1)
            .textAlign(TextAlign.Center)
        }
        .width('100%')
        .padding(RTLUtils.getMargin(16, 16))

        // 价格与评分区域
        Row() {
          Text(NumberFormatter.formatCurrency(2999, 'CNY'))
            .fontSize(28)
            .fontColor('#E53935')
            .fontWeight(FontWeight.Bold)

          Text('4.9 ★')
            .fontSize(14)
            .fontColor('#FF9800')
            // 使用逻辑方向 margin
            .margin({ start: LengthMetrics.vp(8) })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        // 关键:设置组件方向
        .direction(RTLUtils.getDirection())

        // 操作按钮
        Row({ space: 12 }) {
          Button($r('app.string.add_to_cart'))
            .layoutWeight(1)
            .backgroundColor('#FF9800')

          Button($r('app.string.buy_now'))
            .layoutWeight(1)
            .backgroundColor('#E53935')
        }
        .width('100%')
        .direction(RTLUtils.getDirection())
      }
      .padding(16)
      // 整页方向控制
      .direction(RTLUtils.getDirection())
    }
  }
}

在这里插入图片描述

核心原则:全程使用逻辑方向属性(start/end)替代物理方向(left/right),并通过 .direction() 控制组件排列方向。


七、工程化最佳实践与性能优化

7.1 资源文件管理规范

规范项 要求
Key 命名 采用 模块_功能_类型 格式,如 home_welcome_titlelogin_btn_submit
占位符 统一使用 %s(字符串)、%d(整数)、%f(浮点数)
兜底资源 base 目录必须包含所有 Key,缺失时编译器会报错
注释 复杂文案需添加 comment 字段,辅助翻译人员理解语境

7.2 性能优化策略

// ❌ 错误:每次 build 都创建新的 formatter
build() {
  Text(new intl.DateTimeFormat('zh-CN').format(new Date()))
}

// ✅ 正确:缓存 formatter 实例
private dateFormatter: intl.DateTimeFormat | null = null;

aboutToAppear() {
  const locale = i18n.System.getSystemLocale();
  this.dateFormatter = new intl.DateTimeFormat(locale, { dateStyle: 'medium' });
}

build() {
  Text(this.dateFormatter!.format(new Date()))
}

7.3 常见踩坑速查表

问题现象 根因 解决方案
切换语言后 UI 未刷新 setAppPreferredLanguage 不触发组件重绘 配合 AppStorage + emitter 全局通知
带参数字符串显示 %s 误用 $r() 传参 改用 rm.getStringSync(id, ...args)
阿拉伯语布局错乱 未适配 RTL,使用 left/right 使用 start/end + .direction(RTLUtils.getDirection())
新增语言后编译报错 base 目录缺少对应 Key 确保所有语言文件的 Key 完全一致
日期格式未本地化 手写 yyyy-MM-dd 使用 intl.DateTimeFormat

八、总结

本文从 HarmonyOS ArkTS 字符串资源国际化的完整链路出发,覆盖了:

  1. 资源目录架构:基于 BCP-47 标准的多语言目录组织与兜底机制;
  2. 静态与动态引用$r() 编译期绑定与 resourceManager 运行时读取;
  3. 应用内语言切换I18nService 单例 + AppStorage 全局状态驱动 UI 重渲染;
  4. 本地化格式化intl 模块实现日期、数字、货币的自动区域适配;
  5. RTL 完整支持:从检测、布局方向到组件级镜像翻转的工程化方案。

国际化是一项系统工程,做好资源管理、动态切换、格式化与 RTL 适配这四个维度,你的 HarmonyOS 应用才能真正具备服务全球用户的能力。


转载自:https://blog.csdn.net/u014727709/article/details/163239544
欢迎 👍点赞✍评论⭐收藏,欢迎指正

Logo

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

更多推荐