鸿蒙 ArkTS 国际化实战全攻略:多语言切换、格式本地化与 RTL 布局一步到位

鸿蒙 ArkTS 国际化实战全攻略:多语言切换、格式本地化与 RTL 布局一步到位


应用"出海"、多语言用户群、政府项目合规......凡是做过这些需求的开发者都踩过同一个坑:在主流框架里搞国际化(i18n)往往是加几个 JSON 文件的事,但在鸿蒙 HarmonyOS 里,你要同时搞定 资源系统运行时语言切换日期 / 数字格式化RTL(从右到左)布局 四件事,少一件都是残缺品。

本文基于 HarmonyOS 5.0(API Level 12),带你把这四件事一次性搞清楚。


一、鸿蒙国际化资源系统:不止是翻译文件

鸿蒙的 i18n 资源不靠 JSON,靠的是 限定符目录(Qualifier Directory)。

1.1 目录结构规范

复制代码
entry/src/main/resources/
├── base/                    # 默认资源(必须存在)
│   ├── element/
│   │   └── string.json      # 默认字符串
│   └── profile/
├── zh_CN/                   # 简体中文
│   └── element/
│       └── string.json
├── en_US/                   # 英文(美国)
│   └── element/
│       └── string.json
├── ar/                      # 阿拉伯语(RTL)
│   └── element/
│       └── string.json
└── dark/                    # 也可以是主题限定符

注意 :目录名必须是 BCP 47 语言标签,如 zh_CNen_USar,不能用 zh(会识别失败)。

1.2 string.json 格式

json 复制代码
// base/element/string.json
{
  "string": [
    { "name": "app_name",    "value": "我的应用" },
    { "name": "home_title",  "value": "首页" },
    { "name": "welcome_msg", "value": "欢迎,%s!" },
    { "name": "item_count",  "value": "共 %d 件商品" }
  ]
}
json 复制代码
// en_US/element/string.json
{
  "string": [
    { "name": "app_name",    "value": "My App" },
    { "name": "home_title",  "value": "Home" },
    { "name": "welcome_msg", "value": "Welcome, %s!" },
    { "name": "item_count",  "value": "%d items in total" }
  ]
}

1.3 在 ArkTS 中引用资源

typescript 复制代码
// 静态引用(编译期确定)
Text($r('app.string.home_title'))
  .fontSize(20)

// 带参数占位符(使用 $rawfile 无效,需要用格式化工具)
import { resourceManager } from '@kit.LocalizationKit';

// 获取带参数的字符串
const rm = getContext(this).resourceManager;
const msg = rm.getStringSync($r('app.string.welcome_msg').id, '斌哥');
// 输出:"欢迎,斌哥!" 或 "Welcome, 斌哥!"

二、运行时动态切换语言

这是大多数教程一笔带过、实际开发中最容易翻车的部分。

2.1 系统语言 vs 应用内切换

HarmonyOS 有两种国际化方案:

方案 适用场景 切换生效方式
跟随系统语言 大多数 App 自动,无需代码
应用内独立切换 出海应用、企业应用 需要手动更新配置

本节重点讲应用内独立切换

2.2 实现语言切换服务

typescript 复制代码
// src/main/ets/service/I18nService.ets
import { i18n } from '@kit.LocalizationKit';
import { preferences } from '@kit.ArkData';

const PREF_KEY = 'app_language';

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

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

  // 初始化:从持久化存储读取用户语言设置
  async init(context: Context): Promise<void> {
    this.store = await preferences.getPreferences(context, 'i18n_prefs');
    const savedLang = this.store.getSync(PREF_KEY, '') as string;
    if (savedLang) {
      this.applyLanguage(savedLang);
    }
  }

  // 获取当前应用语言
  getCurrentLanguage(): string {
    return i18n.System.getSystemLanguage();
  }

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

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

  private applyLanguage(langCode: string): void {
    // 设置应用级别语言覆盖
    i18n.System.setAppPreferredLanguage(langCode);
    // ⚠️ 注意:这里只改变了资源查找路径
    // UI 需要重建才能生效(见下节)
  }
}

2.3 语言切换后的 UI 刷新策略

坑点来了setAppPreferredLanguage 改变的是资源查找逻辑,但已渲染的组件不会自动重新渲染!

解决方案有三种,选其一:

typescript 复制代码
// 方案 A:重启 UIAbility(体验最差,但最彻底)
import { common } from '@kit.AbilityKit';

const context = getContext(this) as common.UIAbilityContext;
context.terminateSelf();
// 配合 autoStartupManager 实现重启,不推荐

// 方案 B:全局状态驱动重渲染(推荐 ✅)
// AppStorage 作为全局语言状态
AppStorage.setOrCreate('currentLang', 'zh-Hans');

// 切换时
AppStorage.set('currentLang', newLang);

// 组件中监听
@StorageProp('currentLang') currentLang: string = 'zh-Hans';

// 方案 C:NavPathStack 重推页面(适合 Navigation 架构)
this.navStack.replacePath({ name: 'HomePage' });

推荐方案 B 的完整实现

typescript 复制代码
// 语言设置页面
@Entry
@Component
struct LanguageSettingPage {
  @StorageProp('currentLang') @Watch('onLangChanged') currentLang: string = 'zh-Hans';
  private i18nService = I18nService.getInstance();

  private onLangChanged(): void {
    // 触发当前页面的文本刷新
    // 通过重新绑定 $r() 实现
  }

  build() {
    Column({ space: 12 }) {
      Text($r('app.string.language_setting'))
        .fontSize(18)
        .fontWeight(FontWeight.Bold)

      ForEach(this.i18nService.getSupportedLanguages(), (lang: { code: string; label: string }) => {
        Row() {
          Text(lang.label).fontSize(16).layoutWeight(1)
          Radio({ value: lang.code, group: 'lang' })
            .checked(this.currentLang === lang.code)
            .onChange(async (isChecked: boolean) => {
              if (isChecked) {
                await this.i18nService.switchLanguage(lang.code);
                AppStorage.set('currentLang', lang.code);
              }
            })
        }
        .width('100%')
        .padding({ left: 16, right: 16, top: 8, bottom: 8 })
        .backgroundColor(Color.White)
        .borderRadius(8)
      })
    }
    .width('100%')
    .padding(16)
  }
}

三、日期、时间与数字的本地化格式化

这块是真正体现"国际化专业度"的地方,CSDN 上 90% 的教程都只停在翻译文本这一步。

3.1 日期格式化

typescript 复制代码
import { intl } from '@kit.LocalizationKit';

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

  // 格式化日期+时间
  static formatDateTime(date: Date): string {
    const locale = i18n.System.getSystemLocale();
    const formatter = new intl.DateTimeFormat(locale, {
      dateStyle: 'medium',
      timeStyle: 'short'
    });
    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.getTime() - Date.now();
    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(DateFormatter.formatDate(now, 'long'));
// zh-Hans: "2026年5月6日"
// en-US:   "May 6, 2026"
// ar:      "٦ مايو ٢٠٢٦"

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

3.2 数字与货币格式化

typescript 复制代码
export class NumberFormatter {
  // 数字格式(带千分位分隔符)
  static formatNumber(value: number, decimals: number = 0): string {
    const locale = i18n.System.getSystemLocale();
    return new intl.NumberFormat(locale, {
      minimumFractionDigits: decimals,
      maximumFractionDigits: decimals
    }).format(value);
  }

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

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

// 使用示例
NumberFormatter.formatNumber(1234567.89, 2);
// zh-Hans: "1,234,567.89"
// de-DE:   "1.234.567,89"(德语用点分千位、逗号分小数)

NumberFormatter.formatCurrency(299.00, 'CNY');
// zh-Hans: "¥299.00"
// en-US:   "CN¥299.00"

NumberFormatter.formatPercent(0.856);
// zh-Hans: "85.6%"
// ar:      "٨٥٫٦٪"(阿拉伯数字)

四、RTL(从右到左)布局适配

适配阿拉伯语、希伯来语等 RTL 语言是很多开发者的噩梦。鸿蒙提供了相对优雅的支持方案。

4.1 自动 RTL 镜像

typescript 复制代码
@Component
struct ProductCard {
  build() {
    Row() {
      // 商品图片
      Image($r('app.media.product_img'))
        .width(80)
        .height(80)
        .borderRadius(8)
      
      // 商品信息
      Column({ space: 4 }) {
        Text($r('app.string.product_name')).fontSize(16)
        Text($r('app.string.product_price')).fontSize(14).fontColor('#FF6B35')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
      .padding({ left: 12 })  // ⚠️ 这里有坑,见下方说明
      
      // 购买按钮
      Button($r('app.string.buy_now'))
        .width(80)
    }
    .width('100%')
    .padding(16)
    // 关键:使用 direction 属性自动处理 RTL
    .direction(this.isRTL ? Direction.Rtl : Direction.Auto)
  }

  // 判断当前是否为 RTL 语言
  private get isRTL(): boolean {
    const lang = i18n.System.getSystemLanguage();
    const rtlLangs = ['ar', 'he', 'fa', 'ur'];
    return rtlLangs.some(l => lang.startsWith(l));
  }
}

⚠️ 关键坑点 :使用固定方向 padding(paddingLeft/paddingRight)在 RTL 下会出错,应改用逻辑方向:

typescript 复制代码
// ❌ 错误:固定物理方向
.padding({ left: 12, right: 0 })  // RTL 下应该是 right: 12

// ✅ 正确:使用逻辑方向(start = LTR时左,RTL时右)
.padding({ start: LengthMetrics.vp(12), end: LengthMetrics.vp(0) })

4.2 图标镜像

方向性图标(箭头、返回键等)在 RTL 下需要镜像翻转:

typescript 复制代码
@Component
struct BackButton {
  build() {
    Image($r('app.media.ic_arrow_left'))
      .width(24)
      .height(24)
      // 阿拉伯语下水平翻转箭头图标
      .scale({ x: this.isRTL ? -1 : 1, y: 1 })
      .onClick(() => {
        router.back();
      })
  }

  private get isRTL(): boolean {
    return i18n.isRTL(i18n.System.getSystemLanguage());
  }
}

4.3 封装全局 RTL 工具

typescript 复制代码
// src/main/ets/utils/RTLUtils.ets
import { i18n } from '@kit.LocalizationKit';

export class RTLUtils {
  private static _isRTL: boolean | null = null;

  static get isRTL(): boolean {
    if (RTLUtils._isRTL === null) {
      RTLUtils._isRTL = i18n.isRTL(i18n.System.getSystemLanguage());
    }
    return RTLUtils._isRTL;
  }

  // 语言切换后清除缓存
  static reset(): void {
    RTLUtils._isRTL = null;
  }

  // 获取文本对齐方向
  static get textAlign(): TextAlign {
    return RTLUtils.isRTL ? TextAlign.End : TextAlign.Start;
  }

  // 获取 Flex 主轴方向
  static get flexDirection(): FlexDirection {
    return RTLUtils.isRTL ? FlexDirection.RowReverse : FlexDirection.Row;
  }

  // 镜像 Scale(用于方向性图标)
  static get mirrorScale(): ScaleOptions {
    return { x: RTLUtils.isRTL ? -1 : 1, y: 1 };
  }
}

五、综合实战:多语言商品详情页

把上面所有技巧整合到一个真实场景里:

typescript 复制代码
import { i18n, intl, resourceManager } from '@kit.LocalizationKit';
import { RTLUtils } from '../utils/RTLUtils';
import { DateFormatter, NumberFormatter } from '../utils/Formatters';

interface Product {
  name: string;
  price: number;
  currency: string;
  stock: number;
  listingDate: Date;
  rating: number;
}

@Entry
@Component
struct ProductDetailPage {
  @StorageProp('currentLang') currentLang: string = 'zh-Hans';
  
  private product: Product = {
    name: 'ArkTS 开发实战课程',
    price: 299,
    currency: 'CNY',
    stock: 1234,
    listingDate: new Date('2026-01-15'),
    rating: 0.964
  };

  build() {
    Scroll() {
      Column({ space: 16 }) {
        // 商品图片
        Image($r('app.media.product_banner'))
          .width('100%')
          .aspectRatio(1.78)
          .borderRadius(12)

        // 价格区域(RTL 自适应)
        Row() {
          Text(NumberFormatter.formatCurrency(this.product.price, this.product.currency))
            .fontSize(28)
            .fontColor('#FF6B35')
            .fontWeight(FontWeight.Bold)
          
          Text(NumberFormatter.formatPercent(this.product.rating))
            .fontSize(14)
            .fontColor('#52C41A')
            .backgroundColor('#F6FFED')
            .padding({ left: 8, right: 8, top: 2, bottom: 2 })
            .borderRadius(4)
            .margin({ start: LengthMetrics.vp(8) }) // ← 逻辑方向
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceBetween)
        .direction(RTLUtils.isRTL ? Direction.Rtl : Direction.Auto)

        // 商品元信息
        Column({ space: 8 }) {
          this.buildInfoRow($r('app.string.label_stock'),
            NumberFormatter.formatNumber(this.product.stock) + ' ' + $r('app.string.unit_pcs'))
          this.buildInfoRow($r('app.string.label_listed'),
            DateFormatter.formatDate(this.product.listingDate, 'long'))
          this.buildInfoRow($r('app.string.label_updated'),
            DateFormatter.formatRelative(new Date()))
        }
        .width('100%')
        .padding(16)
        .backgroundColor(Color.White)
        .borderRadius(12)

        // 购买按钮
        Button($r('app.string.buy_now'))
          .width('100%')
          .height(50)
          .fontSize(18)
          .fontColor(Color.White)
          .backgroundColor('#FF6B35')
          .borderRadius(12)
          .onClick(() => {
            this.handlePurchase();
          })
      }
      .padding(16)
      // 整页 RTL 方向
      .direction(RTLUtils.isRTL ? Direction.Rtl : Direction.Auto)
    }
    .scrollable(ScrollDirection.Vertical)
  }

  @Builder
  buildInfoRow(label: Resource, value: string) {
    Row() {
      Text(label)
        .fontSize(14)
        .fontColor('#8C8C8C')
      Text(value)
        .fontSize(14)
        .fontColor('#262626')
    }
    .width('100%')
    .justifyContent(FlexAlign.SpaceBetween)
    .direction(RTLUtils.isRTL ? Direction.Rtl : Direction.Auto)
  }

  private handlePurchase(): void {
    // 购买逻辑
  }
}

六、常见踩坑速查表

坑位 错误写法 正确写法
目录名格式 zh / en zh_CN / en_US
带参数字符串 $r() 直接传参 rm.getStringSync(id, ...args)
切换后不刷新 只调 setAppPreferredLanguage 同时更新 AppStorage 触发重渲染
RTL padding paddingLeft: 12 start: LengthMetrics.vp(12)
方向性图标 硬编码左箭头 .scale({ x: isRTL ? -1 : 1 })
货币格式 手动拼 "¥" + price intl.NumberFormat + style:'currency'
日期格式 date.toLocaleString() intl.DateTimeFormat(locale).format()
RTL 判断 手写语言列表 i18n.isRTL(lang) 官方 API

总结

鸿蒙 ArkTS 的国际化能力是完备的,但坑也相当集中:

  1. 资源目录命名要用 BCP 47 完整标签,不能省略地区代码
  2. 运行时语言切换需要配合 AppStorage 驱动 UI 重渲染
  3. 日期/数字/货币格式化 必须用 intl 模块,别手写
  4. RTL 布局 要全程使用逻辑方向属性(start/end),避免物理方向(left/right
  5. i18n.isRTL(lang) 是官方提供的 RTL 判断 API,不要自己维护语言列表

做好这五件事,你的鸿蒙应用基本能打通全球化需求。下一步可以探索 复数规则intl.PluralRules)和 排序规则intl.Collator),对于新闻类、社区类 App 来说很有价值。


如有疑问欢迎评论区交流,点赞收藏是最大的鼓励 🚀

相关推荐
空中海1 小时前
04 Stage 模型、系统能力与数据架构
架构·鸿蒙
月光技术杂谈1 小时前
openEuler各镜像目录区别、部署差异及5G基站平台稳定高性能系统构建方案
5g·华为·信创·镜像·openeuler·国产·欧拉
空中海2 小时前
05 鸿蒙APP 测试、性能、安全、发布与生产实践
安全·华为·harmonyos
●VON2 小时前
鸿蒙Widget开发实战:3张卡片实现桌面-App全链路同步
华为·app·harmonyos·鸿蒙·von
nashane2 小时前
HarmonyOS 6学习:Web组件本地资源跨域访问全解析与实战
前端·学习·harmonyos·harmonyos 5
特立独行的猫a2 小时前
HarmonyOS / OpenHarmony 鸿蒙PC平台三方库移植:AI自动化编译框架build_in_harmonyos介绍及使用
人工智能·自动化·harmonyos·三方库移植·鸿蒙pc·opendesk
key_3_feng2 小时前
Pura X Max 鸿蒙深度优化方案
华为·鸿蒙
极客范儿2 小时前
华为HCIP网络工程师认证—静态路由
网络·华为·智能路由器
爱网络爱Linux2 小时前
华为HCIP——BGP 基础配置
服务器·前端·华为·hcip·hcip datacom·华为数通认证