HarmonyOS 6.1 国际化与多语言适配 - i18n与intl与$r三件套
对应Demo: I18nDemo | 难度: 中级 | 关键词: i18n, intl, $r资源引用, 多语言适配
去年我们团队做过一个出海项目,App要同时支持中文、英文、阿拉伯文。中文和英文还好说,阿拉伯文一上来直接懵了——文字从右到左排列,布局要镜像翻转,数字的千位分隔符用逗号还是点号每个国家不一样,日期格式更是五花八门。当时用了各种硬编码if-else判断语言,代码里到处是locale等于ar就rtl否则ltr这种补丁,维护到后来没人敢碰。后来重构时系统学习了HarmonyOS NEXT的国际化体系,发现官方已经提供了i18n、intl、$r三个层面的解决方案,只是我之前没用对。今天就把这三个工具的能力边界和使用方法讲清楚,帮后来者少走弯路。
国际化这个词听起来很大,很多开发者觉得"就是把文字翻译成英文嘛"。实际上文字翻译只是国际化的冰山一角——水面下还有数字格式差异(千位分隔符、小数点、货币符号位置)、日期格式差异(年月日顺序、十二小时制还是二十四小时制)、复数规则差异(中文没有复数变化、英语有两种、阿拉伯语有六种)、排版方向差异(阿拉伯语从右到左)。如果你只做了文字翻译就声称"支持国际化",那用户切换到阿拉伯语环境后看到的App一定是千疮百孔的。真正的国际化是从系统层面理解不同语言环境的使用习惯,然后用合适的工具逐一适配。HarmonyOS提供的i18n加intl加$r三件套就是干这个的。

一、国际化三层架构:i18n管系统信息,intl管格式化,$r管静态文本
HarmonyOS的国际化方案分三个层次,各自解决不同的问题。很多人搞混这三者的职责,要么用$r硬搞一切,要么用i18n做格式化,结果都是事倍功半。
i18n模块(来自@kit.LocalizationKit)提供系统语言环境信息的读取能力。它能告诉你当前系统的语言、地区、日历类型、数字方向等信息。典型API:i18n.System.getSystemLocale()、i18n.System.getSystemLanguage()、i18n.System.getSystemRegion()。i18n是"知道用户在哪"的工具——你不知道用户用什么语言、在哪个国家,后续的格式化和切换都无从谈起。
intl模块(ECMAScript 402标准的HarmonyOS实现)提供格式化能力。它能把数字、日期、排序等按照特定地区的习惯格式化输出。典型API:intl.NumberFormat、intl.DateTimeFormat、intl.Collator。intl是"按照用户的习惯输出"的工具——你拿到了用户语言,接下来要把数字和日期按照这个语言的习惯展示出来,不能在中国用户面前显示3/15/2024这种美式日期格式。
r资源引用机制提供静态文本的多语言切换能力。在resources目录下按语言建子目录(en、zh、ar等),r资源引用机制提供静态文本的多语言切换能力。在resources目录下按语言建子目录(en、zh、ar等),r资源引用机制提供静态文本的多语言切换能力。在resources目录下按语言建子目录(en、zh、ar等),r(‘app.string.xxx’)会根据系统语言自动选择对应目录下的字符串。r是"自动切换到用户的语言"的工具——UI上的文字标签、按钮文案、提示信息,用r是"自动切换到用户的语言"的工具——UI上的文字标签、按钮文案、提示信息,用r是"自动切换到用户的语言"的工具——UI上的文字标签、按钮文案、提示信息,用r引用就不需要写任何语言判断逻辑。
三者的关系可以这样理解:i18n是"知道用户在哪",intl是"按照用户的习惯输出",r是"自动切换到用户的语言"。一个完整的国际化方案需要三者配合使用,缺一不可。只用r是"自动切换到用户的语言"。一个完整的国际化方案需要三者配合使用,缺一不可。只用r是"自动切换到用户的语言"。一个完整的国际化方案需要三者配合使用,缺一不可。只用r不做格式化,数字日期的展示会出错;只用intl不切换文本,界面文字还是中文的;只用i18n读取信息但不应用,等于知道问题但不解决。
二、i18n模块:读取系统语言环境
先看i18n模块。这是国际化的第一步——搞清楚当前用户的语言和地区设置。
import { i18n } from '@kit.LocalizationKit'
@Entry
@Component
struct I18nDemo {
@State systemLocale: string = ''
@State systemLanguage: string = ''
@State systemRegion: string = ''
@State displayLanguage: string = ''
@State displayCountry: string = ''
aboutToAppear(): void {
this.systemLocale = i18n.System.getSystemLocale()
this.systemLanguage = i18n.System.getSystemLanguage()
this.systemRegion = i18n.System.getSystemRegion()
let locale: string = this.systemLocale
this.displayLanguage = i18n.System.getDisplayLanguage(locale, locale, true)
this.displayCountry = i18n.System.getDisplayCountry(locale, locale, true)
}
build() {
Scroll() {
Column({ space: 16 }) {
Text('系统语言环境信息')
.fontSize(24)
.fontWeight(FontWeight.Bold)
this.InfoRow('系统Locale', this.systemLocale)
this.InfoRow('系统语言', this.systemLanguage)
this.InfoRow('系统地区', this.systemRegion)
this.InfoRow('语言显示名', this.displayLanguage)
this.InfoRow('地区显示名', this.displayCountry)
Divider()
Text('格式化演示')
.fontSize(20)
.fontWeight(FontWeight.Medium)
this.FormatDemoSection()
}
.width('100%')
.padding(20)
}
.width('100%')
.height('100%')
}
@Builder
InfoRow(label: string, value: string) {
Row() {
Text(label + ':')
.fontSize(15)
.fontColor('#666666')
.width(120)
Text(value)
.fontSize(15)
.fontColor('#333333')
.layoutWeight(1)
}
.width('100%')
}
}
i18n.System.getSystemLocale()返回系统设置的完整Locale标识,比如zh-Hans-CN(中文-简体-中国大陆)、en-US(英文-美国)、ar-SA(阿拉伯文-沙特阿拉伯)。这个标识由三部分组成:语言代码(zh)、脚本代码(Hans表示简体)、国家代码(CN)。脚本代码有时候会省略——比如en-US没有脚本代码,因为英文不区分简繁体。
i18n.System.getSystemLanguage()只返回语言部分,比如zh-Hans、en、ar。注意这里可能包含脚本代码(zh-Hans),也可能不包含(en),取决于语言本身是否需要区分。
i18n.System.getSystemRegion()只返回地区部分,比如CN、US、SA。这是判断用户所在国家最可靠的方式——比从Locale字符串里解析更准确,因为用户可能把语言设成英文但地区设成中国,Locale显示en-US但getSystemRegion()返回CN。
getDisplayLanguage和getDisplayCountry是两个实用方法——它们能把语言代码和地区代码转换成用户可读的名称。比如getDisplayLanguage(‘en’, ‘zh-Hans-CN’, true)返回"英语",而不是冷冰冰的"en"。第三个参数sentenceCase表示是否首字母大写。这两个方法在设置页面的语言选择器里特别有用——显示"英语(美国)"比显示"en-US"友好得多。
注意:i18n的import路径必须是@kit.LocalizationKit,不是旧的@kit.InternationalizationKit。后者已经废弃,用旧路径编译会报模块找不到的错误。这个坑我在开发过程中踩过,浪费了半小时排查,希望你不要重蹈覆辙。
三、intl模块:数字、日期、复数的格式化
3.1 intl.NumberFormat:数字格式化
不同地区对数字的书写习惯差异大到超出你的想象。一千二百三十四点五六这个数字,不同地区的写法:
- 中文环境:1,234.56(逗号作千位分隔,句号作小数点)
- 德语环境:1.234,56(句号作千位分隔,逗号作小数点——和中文完全相反)
- 法语环境:1 234,56(空格作千位分隔,逗号作小数点)
- 阿拉伯语环境:١٬٢٣٤٫٥٦(使用阿拉伯-印度数字,逗号作千位分隔,中间点作小数点)
手动处理这些差异是噩梦——你得为每种语言写一套格式化规则,维护成本爆炸。intl.NumberFormat一行搞定:
@Builder
FormatDemoSection() {
Column({ space: 12 }) {
Text('数字格式化')
.fontSize(16)
.fontWeight(FontWeight.Medium)
let locales: string[] = ['zh-Hans-CN', 'en-US', 'de-DE', 'ar-SA']
let amount: number = 1234567.89
ForEach(locales, (locale: string) => {
Row() {
Text(locale)
.fontSize(13)
.fontColor('#666666')
.width(120)
Text(new intl.NumberFormat(locale, { style: 'currency', currency: 'CNY' }).format(amount))
.fontSize(15)
.fontColor('#333333')
.layoutWeight(1)
}
.width('100%')
})
Divider()
Text('日期格式化')
.fontSize(16)
.fontWeight(FontWeight.Medium)
let now: Date = new Date()
ForEach(locales, (locale: string) => {
Row() {
Text(locale)
.fontSize(13)
.fontColor('#666666')
.width(120)
Text(new intl.DateTimeFormat(locale, { dateStyle: 'long', timeStyle: 'short' }).format(now))
.fontSize(15)
.fontColor('#333333')
.layoutWeight(1)
}
.width('100%')
})
}
}
NumberFormat的style参数决定格式类型:decimal(普通数字,默认值)、currency(货币,需要额外指定currency参数如CNY、USD、EUR)、percent(百分比,数字乘以100后加%号)、unit(带单位的数字,比如"5 liter")。currency风格是最常用的国际化数字格式——同一个金额,不同地区显示的货币符号、位置、小数位数都不一样。
3.2 intl.DateTimeFormat:日期格式化
日期格式化的差异同样惊人,甚至比数字格式化更复杂:
- 中文环境:2024年3月15日 下午3:30
- 美式英文:March 15, 2024, 3:30 PM(月日在年前,12小时制)
- 英式英文:15 March 2024, 15:30(日在月年前,24小时制)
- 德语环境:15. März 2024, 15:30(日用句号分隔)
- 阿拉伯语环境:١٥ مارس ٢٠٢٤، ٣:٣٠ م(从右到左排列,阿拉伯-印度数字)
DateTimeFormat支持dateStyle和timeStyle两个参数,每个参数可选full/long/medium/short四个级别,控制显示的详细程度。full级别会显示星期和完整时区,short级别只显示数字。也可以完全自定义各个部分的格式:
let formatter: intl.DateTimeFormat = new intl.DateTimeFormat('zh-Hans-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
})
let formatted: string = formatter.format(new Date())
自定义格式的好处是你能精确控制输出内容,坏处是不同地区的同一套自定义参数可能产生不同效果——比如hour12设为false在中文环境是24小时制,但在某些locale下可能被忽略。如果只需要"标准格式",用dateStyle/timeStyle更安全;如果需要精确控制格式(比如后端API要求固定格式),用自定义参数。
3.3 复数规则:PluralRules
国际化中有一个容易被忽略的问题:复数。中文没有复数变化,"1条消息"和"5条消息"用同一个"条"字,开发时完全不需要考虑复数。但英语有:1 message vs 5 messages。俄语更复杂,有单数、双数、复数三种形式。阿拉伯语有六种复数形式!如果你只按中文的思维开发,出海到这些语言环境就会出现语法错误。
intl.PluralRules帮你判断应该用哪种复数形式:
let pr: intl.PluralRules = new intl.PluralRules('en')
pr.select(1)
pr.select(5)
let prRu: intl.PluralRules = new intl.PluralRules('ru')
prRu.select(1)
prRu.select(2)
prRu.select(5)
select方法返回的值是字符串常量:zero、one、two、few、many、other。英语只有one和other两种,俄语有one、few、many和其他,阿拉伯语有zero、one、two、few、many和其他。你可以根据返回值选择对应的字符串模板或者拼接规则。
不过HarmonyOS的$r资源系统本身不支持复数选择(不像Android的plurals标签),所以复数逻辑需要代码层面处理。这确实增加了开发量,但换来的是完全可控的复数规则实现。
四、$r资源引用:静态文本的多语言切换
r是ArkUI的资源引用语法,支持string、color、float、media、profile等资源类型。对于国际化,最常用的是r是ArkUI的资源引用语法,支持string、color、float、media、profile等资源类型。对于国际化,最常用的是r是ArkUI的资源引用语法,支持string、color、float、media、profile等资源类型。对于国际化,最常用的是r(‘app.string.xxx’)引用字符串资源。
资源文件的目录结构是理解$r多语言切换机制的关键:
resources/
base/
element/
string.json // 默认语言(通常是英文或中文)
en/
element/
string.json // 英文
zh/
element/
string.json // 中文
ar/
element/
string.json // 阿拉伯文
base目录是兜底——如果当前系统语言在en、zh、ar里都找不到匹配的资源,就用base里的。所以base必须包含所有字符串的默认值,否则$r会找不到资源而报错。语言目录的命名遵循BCP 47规范:两位字母语言代码(zh、en、ar)或者语言加地区代码(zh-Hans、en-US)。
string.json的格式是name-value对数组:
{
"string": [
{ "name": "app_name", "value": "MyDemo" },
{ "name": "greeting", "value": "Hello" },
{ "name": "settings", "value": "Settings" }
]
}
en/element/string.json:
{
"string": [
{ "name": "app_name", "value": "MyDemo" },
{ "name": "greeting", "value": "Hello" },
{ "name": "settings", "value": "Settings" }
]
}
zh/element/string.json:
{
"string": [
{ "name": "app_name", "value": "我的Demo" },
{ "name": "greeting", "value": "你好" },
{ "name": "settings", "value": "设置" }
]
}
使用时只需要一行:
Text($r('app.string.greeting'))
系统会根据当前语言环境自动选择对应目录下的string.json。你不需要写任何语言判断逻辑,r帮你全管了。当系统语言是中文时,r帮你全管了。当系统语言是中文时,r帮你全管了。当系统语言是中文时,r(‘app.string.greeting’)返回"你好";当系统语言是英文时,返回"Hello"。这是最简洁、最标准的国际化方案。
五、$r返回的是Resource不是string
这是r最容易让人困惑的一点,也是国际化开发中最常见的翻车场景:r最容易让人困惑的一点,也是国际化开发中最常见的翻车场景:r最容易让人困惑的一点,也是国际化开发中最常见的翻车场景:r(‘app.string.xxx’)的返回类型是Resource,不是string。
let greeting: Resource = $r('app.string.greeting')
这意味着你不能对$r的返回值做字符串操作——不能拼接、不能substring、不能取length。下面这些写法都会编译报错或者运行时异常:
// 错误:Resource不能赋值给string类型
let text: string = $r('app.string.greeting')
// 错误:Resource不能拼接
let full: string = $r('app.string.greeting') + ' World'
// 错误:Resource没有length属性
let len: number = $r('app.string.greeting').length
正确用法是直接把r传给支持Resource类型的组件属性。Text、Button等组件的content参数和fontColor、fontSize等属性都支持Resource类型,r传给支持Resource类型的组件属性。Text、Button等组件的content参数和fontColor、fontSize等属性都支持Resource类型,r传给支持Resource类型的组件属性。Text、Button等组件的content参数和fontColor、fontSize等属性都支持Resource类型,r返回值可以直接传入:
Text($r('app.string.greeting'))
.fontColor($r('app.color.primary'))
.fontSize($r('app.float.title_size'))
但如果你需要在代码逻辑里操作国际化字符串(比如拼接动态内容),$r就不够用了。这时候需要另一种方案。
方案A:context.resourceManager
如果你需要在TypeScript代码里获取字符串的实际内容,用resourceManager:
import { resourceManager } from '@kit.LocalizationKit'
let resMgr: resourceManager.ResourceManager = getContext(this).resourceManager
let greeting: string = resMgr.getStringSync($r('app.string.greeting').id)
getStringSync通过资源ID获取对应语言的字符串,返回值是string类型,可以随意操作。注意要用r().id而不是r().id而不是r().id而不是r()本身,因为getStringSync需要的是数字类型的资源ID。这种方式的好处是仍然走$r的资源选择逻辑,语言切换自动生效;缺点是多了一步间接调用。
方案B:翻译Map
对于需要动态拼接的场景,可以建一个翻译Map:
class I18nHelper {
private translations: Record<string, Record<string, string>> = {
'zh': {
'welcome': '欢迎',
'unread': '条未读消息',
'confirm_delete': '确认删除'
},
'en': {
'welcome': 'Welcome',
'unread': ' unread messages',
'confirm_delete': 'Confirm Delete'
}
}
getText(key: string, locale: string): string {
let lang: string = locale.split('-')[0]
let dict: Record<string, string> = this.translations[lang] || this.translations['en']
return dict[key] || key
}
formatUnread(count: number, locale: string): string {
let lang: string = locale.split('-')[0]
if (lang === 'zh') {
return String(count) + this.getText('unread', locale)
}
let pr: intl.PluralRules = new intl.PluralRules(locale)
let plural: string = pr.select(count)
let suffix: string = plural === 'one' ? ' unread message' : ' unread messages'
return String(count) + suffix
}
}
翻译Map的优点是完全可控,可以动态加载、支持插值、支持复数。缺点是需要自己维护翻译数据,容易和resources目录下的string.json产生重复和不同步——改了一个忘了改另一个,线上就出现中英文混搭的尴尬场面。
六、$r vs intl API vs 翻译Map:选哪个
| 对比维度 | $r资源引用 | intl格式化API | 翻译Map |
|---|---|---|---|
| 适用场景 | 静态UI文本 | 数字/日期/排序格式化 | 动态拼接文本 |
| 返回类型 | Resource | string | string |
| 可拼接 | 否 | 是 | 是 |
| 运行时切换 | 需重启生效 | 立即生效 | 立即生效 |
| 维护成本 | 低(资源文件管理) | 低(系统内置规则) | 高(手动维护Map) |
| 复数支持 | 无 | PluralRules | 手动实现 |
| 编译期检查 | 有(资源名不存在会报错) | 无 | 无 |
结论:三者不互斥,而是互补。UI标签用r(静态文本零代码切换),数字日期用intl(格式化规则不用手写),动态拼接用翻译Map或者resourceManager(拿到string后自由操作)。不要试图用一个方案解决所有问题——r(静态文本零代码切换),数字日期用intl(格式化规则不用手写),动态拼接用翻译Map或者resourceManager(拿到string后自由操作)。不要试图用一个方案解决所有问题——r(静态文本零代码切换),数字日期用intl(格式化规则不用手写),动态拼接用翻译Map或者resourceManager(拿到string后自由操作)。不要试图用一个方案解决所有问题——r不是万能的,intl也不是,翻译Map更不是。正确的姿势是三者在各自擅长的领域发挥作用,组合起来形成完整的国际化方案。
七、RTL布局适配:阿拉伯语的从右到左
阿拉伯语和希伯来语的书写方向是从右到左(RTL)。这不仅是文字排列方向的变化,整个UI布局都需要镜像翻转。这不是简单的"文字从右往左写",而是整个视觉流的方向反转:
- 左对齐变成右对齐
- 前进箭头从向右变成向左
- 列表图标从左侧移到右侧
- 进度条从右向左填充
- 返回按钮从左上角移到右上角
ArkUI的Flex和Row组件支持direction属性,可以设置为FlexDirection.Rtl。但逐个组件设direction太繁琐,更好的做法是用Direction.Rtl属性控制整个容器的方向:
@Component
struct DirectionAwareContainer {
@Prop content: string = ''
private locale: string = i18n.System.getSystemLocale()
build() {
Row({ space: 12 }) {
SymbolGlyph($r('sys.symbol.ohos_folder_badge_plus'))
.fontSize(20)
.fontColor([Color.Gray])
Text(this.content)
.fontSize(15)
.layoutWeight(1)
}
.width('100%')
.direction(this.locale.startsWith('ar') ? Direction.Rtl : Direction.Ltr)
}
}
Direction.Rtl会让Row的子组件从右到左排列,不需要手动调整顺序。Text组件也会自动切换为RTL排列。但有些细节Direction解决不了——比如图标本身的方向(箭头图标需要水平翻转),比如进度条的填充方向,比如边框和阴影的方向性。这些还是需要代码处理。
RTL适配的最大坑是margin和padding的方向。在LTR布局下,margin({ left: 16 })表示左间距16。切换到RTL后,如果你用left和right这样的绝对方向属性,间距不会自动镜像——左间距还是左间距,但视觉上"左"在RTL布局里应该对应"右"。ArkUI目前的margin和padding属性不支持start和end这样的逻辑方向语义(不像Android的marginStart/marginEnd),只能用逻辑判断手动交换left和right。这是目前RTL适配中最繁琐的部分,期待后续版本改进。
八、踩坑实录:国际化开发中的暗坑
坑一:r返回Resource不能当string用。这个前面重点说了,但还是要再提,因为它太容易犯了。最典型的翻车场景是在网络请求的参数里用r返回Resource不能当string用。这个前面重点说了,但还是要再提,因为它太容易犯了。最典型的翻车场景是在网络请求的参数里用r返回Resource不能当string用。这个前面重点说了,但还是要再提,因为它太容易犯了。最典型的翻车场景是在网络请求的参数里用r——你以为传了个字符串,实际上传了个Resource对象,序列化后变成[object Object],服务端解析直接报错:
// 错误
let params: Record<string, Resource> = { 'name': $r('app.string.user_name') }
// 正确
let resMgr: resourceManager.ResourceManager = getContext(this).resourceManager
let name: string = resMgr.getStringSync($r('app.string.user_name').id)
let params: Record<string, string> = { 'name': name }
坑二:RTL布局下margin和padding的方向混淆。前面说了,ArkUI的margin和padding不支持start/end逻辑方向。在RTL布局下,你需要手动判断方向并交换left和right:
let isRtl: boolean = i18n.System.getSystemLocale().startsWith('ar')
let marginValue: Margin = isRtl ? { right: 16 } : { left: 16 }
这种判断代码散落在各个组件里,维护成本很高。建议封装一个工具函数统一处理方向相关的间距。
坑三:复数规则的地区差异。中文没有复数变化,开发者容易忽略其他语言的复数需求。当你的App只有中文版时没问题,一旦出海到英语、俄语、阿拉伯语地区,"1 items"这种语法错误会让用户觉得很不专业,甚至怀疑App的质量。从第一天起就用PluralRules处理复数,即使中文版暂时用不到——将来出海时你会感谢自己的先见之明。
坑四:日期格式的时间戳精度。intl.DateTimeFormat.format()接受Date对象。但如果你从服务端拿到的是时间戳(毫秒数),需要先new Date(timestamp)转换。注意JavaScript的Date构造函数接受的是毫秒级时间戳,如果你的后端返回的是秒级时间戳(Unix timestamp,这是很多后端框架的默认行为),需要乘以1000:
let serverTimestamp: number = 1710460800
let date: Date = new Date(serverTimestamp * 1000)
let formatted: string = new intl.DateTimeFormat('zh-Hans-CN').format(date)
忘乘1000是最常见的日期格式化Bug——日期会显示在1970年,因为毫秒级时间戳1710460800只相当于1970年1月20日。
坑五:资源文件不生效的排查。$r引用字符串但不显示,可能的原因有:语言目录名拼写错误(比如把zh-Hans写成zh,或者en-US写成en_US——下划线是错的,必须用短横线)、string.json的JSON格式有语法错误(多一个逗号少一个引号)、资源name有重复定义(同一文件里两个string的name相同,后者覆盖前者)。排查时先检查resources目录结构是否正确,再检查JSON文件的合法性——可以用JSON校验工具在线检查。
坑六:运行时切换语言需要重启应用。r的资源选择是在应用启动时根据系统Locale确定的,运行期间如果用户在系统设置里切换了语言,已运行的App不会自动更新r的资源选择是在应用启动时根据系统Locale确定的,运行期间如果用户在系统设置里切换了语言,已运行的App不会自动更新r的资源选择是在应用启动时根据系统Locale确定的,运行期间如果用户在系统设置里切换了语言,已运行的App不会自动更新r的引用——需要重启App才能生效。HarmonyOS目前没有提供应用内语言切换的官方API(不像Android的LocaleList)。如果你需要应用内切换语言(不重启),只能用翻译Map方案,手动维护当前语言和对应的字符串。
坑七:货币格式化的位数差异。intl.NumberFormat的currency风格在不同地区显示的小数位数不一样。日元没有小数(1日元就是1,不存在0.5日元),所以NumberFormat(‘ja-JP’, { style: ‘currency’, currency: ‘JPY’ })格式化后的结果没有小数部分。而人民币和美元默认显示两位小数。如果你在表格里对齐金额列,不同货币的小数位数差异会导致对不齐。解决办法是在NumberFormat的options里显式指定minimumFractionDigits和maximumFractionDigits,统一小数位数。
坑八:阿拉伯数字和阿拉伯-印度数字的混淆。阿拉伯语环境下,数字默认使用阿拉伯-印度数字(٠١٢٣٤٥٦٧٨٩),不是我们熟悉的0123456789。这对用户来说没问题,因为他们习惯了。但如果你的应用需要把格式化后的数字传给后端API,后端可能不认识阿拉伯-印度数字,解析报错。解决办法是在传给后端时用NumberFormat(‘en-US’)重新格式化,确保是西方阿拉伯数字。
九、实战Demo整合:i18n信息加格式化加方向适配
把上面所有知识点整合到一个完整的Demo里:
import { i18n } from '@kit.LocalizationKit'
import { intl } from '@kit.ArkUI'
import { resourceManager } from '@kit.LocalizationKit'
import { common } from '@kit.AbilityKit'
@Entry
@Component
struct I18nDemo {
@State locale: string = ''
@State language: string = ''
@State region: string = ''
@State numberDemo: string = ''
@State dateDemo: string = ''
@State pluralDemo: string = ''
@State isRtl: boolean = false
private resMgr: resourceManager.ResourceManager = getContext(this).resourceManager
aboutToAppear(): void {
this.locale = i18n.System.getSystemLocale()
this.language = i18n.System.getSystemLanguage()
this.region = i18n.System.getSystemRegion()
this.isRtl = this.locale.startsWith('ar')
let amount: number = 9876543.21
this.numberDemo = new intl.NumberFormat(this.locale, { style: 'currency', currency: 'CNY' }).format(amount)
let now: Date = new Date()
this.dateDemo = new intl.DateTimeFormat(this.locale, { dateStyle: 'long', timeStyle: 'medium' }).format(now)
let pr: intl.PluralRules = new intl.PluralRules(this.locale)
this.pluralDemo = 'count=1: ' + pr.select(1) + ', count=5: ' + pr.select(5) + ', count=0: ' + pr.select(0)
}
build() {
Scroll() {
Column({ space: 16 }) {
Text($r('app.string.app_name'))
.fontSize(24)
.fontWeight(FontWeight.Bold)
Text('Locale: ' + this.locale)
.fontSize(15)
Text('Language: ' + this.language)
.fontSize(15)
Text('Region: ' + this.region)
.fontSize(15)
Text('Direction: ' + (this.isRtl ? 'RTL' : 'LTR'))
.fontSize(15)
Divider()
Text('Currency Format:')
.fontSize(16)
.fontWeight(FontWeight.Medium)
Text(this.numberDemo)
.fontSize(15)
Text('Date Format:')
.fontSize(16)
.fontWeight(FontWeight.Medium)
Text(this.dateDemo)
.fontSize(15)
Text('Plural Rules:')
.fontSize(16)
.fontWeight(FontWeight.Medium)
Text(this.pluralDemo)
.fontSize(15)
}
.width('100%')
.padding(20)
}
.width('100%')
.height('100%')
}
}
这个Demo展示了:i18n读取系统信息、intl格式化数字和日期、intl.PluralRules判断复数、isRtl检测RTL方向、$r引用静态文本。一个页面覆盖了国际化的核心能力。你可以把这个Demo作为国际化开发的起点——在你的项目里创建类似的页面,逐步加入业务相关的国际化逻辑。

十、国际化测试策略
开发完国际化功能只是完成了一半,测试是另一半。国际化测试有几个特殊要点需要关注:
伪语言测试。Google和Apple都提供了伪语言(Pseudo Language)用于测试国际化覆盖度——伪语言会在所有字符串前后加标记字符(比如[xxx]),如果界面上有没被包裹的字符串,就说明这个字符串没有走国际化流程。HarmonyOS目前没有内置伪语言支持,但你可以自己模拟——在base/string.json里给所有value加上方括号前缀,然后检查界面上哪些文字没有方括号,那些就是硬编码的字符串。
超长文本测试。德语的复合词可以非常长,比如"Donaudampfschifffahrtsgesellschaft"这种连写词。如果你的UI布局没有做文本自适应(maxLines、textOverflow、自适应宽度),超长的翻译文本可能撑爆布局。建议在测试时用极端长度的字符串验证每个Text组件的布局弹性。
RTL镜像测试。如果你支持阿拉伯语,需要在真机上切换到阿拉伯语环境,逐一检查每个页面的布局是否正确镜像。重点检查:文字是否从右到左排列、图标和文字的相对位置是否翻转、返回按钮的方向是否正确、对话框的按钮顺序是否镜像(LTR下"取消"在左"确认"在右,RTL下应该反过来)。
截图对比测试。对于支持多语言的应用,可以为每种语言录制一套UI截图作为基准。后续版本更新时,自动截图对比,发现布局偏移或文字截断问题。这属于视觉回归测试的范畴,可以用自动化工具实现。
写在最后
国际化的本质不是"翻译文字",而是"适配差异"——数字格式的差异、日期顺序的差异、复数规则的差异、排版方向的差异。i18n告诉你差异在哪,intl帮你处理差异,r帮你切换差异。三件套各司其职,别指望一个r帮你切换差异。三件套各司其职,别指望一个r帮你切换差异。三件套各司其职,别指望一个r搞定一切,也别用翻译Map替代intl的格式化能力。出海的App,国际化不是可选项,是基本功。
更多推荐



所有评论(0)