鸿蒙旅行翻译助手 - 多语言资源与 Intl 格式化详解


实例:旅行翻译助手|技术:@ohos.i18n / @ohos.intl(Intl/DateTimeFormat/NumberFormat)、资源限定符(qualifiers)、多语言资源管理、@kit.ArkData(preferences)
一、本篇范围
旅行翻译助手是「国际化 + 文本处理」的综合性应用:内置一个多语种常用语词典(问候、点餐、问路、购物四大类),支持中英日韩互译;用户界面本身完全国际化——跟随系统语言显示中文/英文,日期时间、数字、货币按用户区域习惯格式化;常用短语可收藏进 preferences。
本篇拆「国际化服务层」,核心问题:
- 多语言资源怎么组织?
base/en_US/zh_CN资源目录 + 资源限定符的匹配机制; - 代码里怎么读多语言?
$r()资源引用与getContext().resourceManager动态取资源; - Intl 格式化三件套:
DateTimeFormat、NumberFormat、Collator的用法与区域差异; - 本地词典数据结构怎么设计?多语种短语的 key-value 模型与查找算法。
二、多语言资源体系:限定符的魔力
2.1 资源目录结构
HarmonyOS 的资源文件按「目录名 = 限定符」自动匹配。一个支持中英双语的翻译应用,资源目录长这样:
entry/src/main/resources/
├── base/ # 默认资源(必选,兜底语言)
│ ├── element/string.json # 默认语言字符串(英文)
│ ├── media/icon.png
│ └── profile/main_pages.json
├── zh_CN/ # 简体中文限定符目录
│ └── element/string.json # 中文覆盖
├── en_US/ # 美式英语(显式声明,虽然 base 也是英文)
│ └── element/string.json
└── zh_HK/ # 繁体(可选)
└── element/string.json
匹配优先级规则:系统语言是简体中文 → 优先 zh_CN;系统是英语 → 优先 en_US;系统是法语且没有 fr 目录 → 落回 base。这就是「限定符回退」机制——base 目录必须存在,否则法语用户拿到的是空资源。
2.2 string.json 的资源键设计
base/element/string.json(默认英文):
{
"string": [
{ "name": "app_name", "value": "Travel Translator" },
{ "name": "tab_phrases", "value": "Phrases" },
{ "name": "tab_favorites", "value": "Favorites" },
{ "name": "search_hint", "value": "Search phrases..." },
{ "name": "greeting_title", "value": "Greetings" },
{ "name": "dining_title", "value": "Dining" },
{ "name": "asking_title", "value": "Asking Directions" },
{ "name": "shopping_title", "value": "Shopping" },
{ "name": "add_favorite", "value": "Add to favorites" },
{ "name": "removed_favorite", "value": "Removed from favorites" },
{ "name": "language_label", "value": "Language" },
{ "name": "translate_hint", "value": "Tap a phrase to translate" }
]
}
zh_CN/element/string.json(中文覆盖,只写不同的键):
{
"string": [
{ "name": "app_name", "value": "旅行翻译" },
{ "name": "tab_phrases", "value": "常用语" },
{ "name": "tab_favorites", "value": "收藏" },
{ "name": "search_hint", "value": "搜索短语…" },
{ "name": "greeting_title", "value": "问候" },
{ "name": "dining_title", "value": "点餐" },
{ "name": "asking_title", "value": "问路" },
{ "name": "shopping_title", "value": "购物" },
{ "name": "add_favorite", "value": "已加入收藏" },
{ "name": "removed_favorite", "value": "已取消收藏" },
{ "name": "language_label", "value": "语言" },
{ "name": "translate_hint", "value": "点击短语查看译文" }
]
}
覆盖机制:zh_CN 只声明与 base 不同的键,未覆盖的键自动回退 base——避免重复维护。注意资源值里不要硬编码占位逻辑,需要拼接的地方用格式化占位符(见第四节)。
2.3 代码里读取:$r() 与 resourceManager
UI 代码里用 $r('app.string.xxx') 直接引用,系统自动按当前语言解析:
// 静态引用(编译期检查 key 是否存在)
Text($r('app.string.tab_phrases')).fontSize(16)
动态场景(比如拼字符串、根据变量取资源)用 resourceManager:
import { common } from '@kit.AbilityKit';
export class I18nUtil {
/**
* 动态获取本地化字符串。
*/
static async getString(context: common.UIAbilityContext, key: string): Promise<string> {
const mgr = context.resourceManager;
try {
return await mgr.getStringByName(key);
} catch {
return key; // 资源缺失时回退为 key 本身,便于排查
}
}
/**
* 获取带占位符的格式化字符串。
* 用法:formatString(ctx, 'welcome_user', ['张三'])
*/
static async formatString(context: common.UIAbilityContext, key: string,
args: Array<string | number>): Promise<string> {
const mgr = context.resourceManager;
const raw = await mgr.getStringByName(key);
// 按 %s %d 顺序替换(资源里写 %1$s 位置参数更稳,此处演示顺序替换)
return raw.replace(/%\d*\$?[sd]/g, () => String(args.shift() ?? ''));
}
}
关键区别:$r() 是编译期静态解析,适合 UI 里写死的资源;resourceManager.getStringByName 是运行时动态获取,适合「key 由变量决定」或非 UI 层的国际化。两者读的是同一套资源目录。
格式化占位符最佳实践:资源值里用位置参数 %1$s、%2$d,因为不同语言的语序不同——中文「欢迎 %1s」,英文"Welcome,s」,英文 "Welcome, %1s」,英文"Welcome,s" 都是 1 号参数,但某些语言(如日语)可能需要调换顺序,位置参数让每种语言自己决定顺序,这是国际化的硬性要求。
三、Intl 三件套:区域感知的格式化
@ohos.intl 模块提供 ECMA-402 标准的国际化 API。旅行场景最常见的三个:日期时间、数字、货币。
3.1 DateTimeFormat:日期时间的本地化
import { intl } from '@kit.ArkTS';
export class DateTimeFmt {
/**
* 按区域格式化日期。zh-CN: 2025年1月1日;en-US: Jan 1, 2025
*/
static formatDate(date: Date, locale: string = 'zh-CN'): string {
const dtf = new intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
});
return dtf.format(date.getTime());
}
/**
* 24小时制时间。zh-CN: 14:30;en-US: 2:30 PM
*/
static formatTime(date: Date, locale: string = 'zh-CN'): string {
const dtf = new intl.DateTimeFormat(locale, {
hour: '2-digit',
minute: '2-digit',
hour12: false
});
return dtf.format(date.getTime());
}
/**
* 相对时间(翻译助手显示"刚刚/5分钟前"用)。
*/
static formatRelative(date: Date, now: number = Date.now()): string {
const diffMs = now - date.getTime();
const min = Math.floor(diffMs / 60000);
if (min < 1) return '刚刚';
if (min < 60) return `${min}分钟前`;
const h = Math.floor(min / 60);
if (h < 24) return `${h}小时前`;
return `${Math.floor(h / 24)}天前`;
}
}
DateTimeFormat 的关键是让引擎决定格式:你只声明「要年、月、日、星期几」,引擎按区域输出。中文 “2025年1月1日 星期三”、英文 “Wednesday, January 1, 2025”、日语 “2025年1月1日 水曜日”——语言和格式全部自动适配,这是手写模板永远做不到的。
date.getTime():format 接收的是时间戳(number),传 Date 对象也可以但推荐显式 getTime()。
3.2 NumberFormat:数字与货币
export class NumberFmt {
/**
* 千分位数字。zh-CN: 1,234.5;en-US: 1,234.5(差异在符号位置)
*/
static formatNumber(n: number, locale: string = 'zh-CN'): string {
const nf = new intl.NumberFormat(locale, {
style: 'decimal',
maximumFractionDigits: 2
});
return nf.format(n);
}
/**
* 货币:旅行预算换算展示用。
* 注意:这只是「格式化为货币样式」,汇率换算需自行计算。
*/
static formatCurrency(amount: number, currency: string, locale: string = 'zh-CN'): string {
const nf = new intl.NumberFormat(locale, {
style: 'currency',
currency: currency, // 'CNY' / 'USD' / 'JPY' / 'KRW'
minimumFractionDigits: 0,
maximumFractionDigits: 2
});
return nf.format(amount);
}
}
货币格式化的坑:currency 用 ISO 4217 代码(CNY/USD/JPY),不是符号(¥/$)。引擎按区域决定符号样式——zh-CN 显示 “¥100”、en-US 显示 “$100.00”。日元 JPY 默认 0 位小数,但我们声明了 0–2 位,引擎按区域规矩处理。
3.3 Collator:区域感知排序
多语言短语列表按字母/拼音排序,直接用 < 比较对中日文不准,要用 Collator:
export class SortUtil {
static collator: intl.Collator = new intl.Collator('zh-CN', { sensitivity: 'base' });
/**
* 区域感知排序:英文按字母、中文按拼音。
*/
static compare(a: string, b: string): number {
return SortUtil.collator.compare(a, b);
}
static sortByLocale<T>(list: T[], keyFn: (item: T) => string): T[] {
return [...list].sort((x, y) => SortUtil.compare(keyFn(x), keyFn(y)));
}
}
Collator 按区域规则比较字符串——中文按拼音、日文按五十音、英文按字母。sensitivity: 'base' 忽略大小写与重音(“café” 与 “cafe” 视为相同),适合搜索排序场景。
四、本地词典数据模型
翻译助手不依赖在线翻译 API,内置一个四语种短语词典。数据结构设计:
export interface PhraseEntry {
id: string; // 稳定唯一 id
category: Category; // 分类
translations: Record<string, string>; // locale -> 文本
favorite: boolean; // 是否收藏(本地状态)
}
export enum Category {
GREETING = 'greeting',
DINING = 'dining',
ASKING = 'asking',
SHOPPING = 'shopping'
}
export class PhraseDictionary {
/** 内置词典:每条短语 4 种语言 */
private static readonly DATA: PhraseEntry[] = [
{
id: 'p001',
category: Category.GREETING,
translations: {
'zh-CN': '你好',
'en-US': 'Hello',
'ja-JP': 'こんにちは',
'ko-KR': '안녕하세요'
},
favorite: false
},
{
id: 'p002',
category: Category.GREETING,
translations: {
'zh-CN': '谢谢',
'en-US': 'Thank you',
'ja-JP': 'ありがとうございます',
'ko-KR': '감사합니다'
},
favorite: false
},
{
id: 'p003',
category: Category.DINING,
translations: {
'zh-CN': '这个多少钱?',
'en-US': 'How much is this?',
'ja-JP': 'これはいくらですか?',
'ko-KR': '이것은 얼마예요?'
},
favorite: false
},
{
id: 'p004',
category: Category.DINING,
translations: {
'zh-CN': '请给我菜单',
'en-US': 'Menu, please',
'ja-JP': 'メニューをお願いします',
'ko-KR': '메뉴판 주세요'
},
favorite: false
},
{
id: 'p005',
category: Category.ASKING,
translations: {
'zh-CN': '车站怎么走?',
'en-US': 'How do I get to the station?',
'ja-JP': '駅はどう行きますか?',
'ko-KR': '역은 어떻게 가요?'
},
favorite: false
},
{
id: 'p006',
category: Category.ASKING,
translations: {
'zh-CN': '洗手间在哪里?',
'en-US': 'Where is the restroom?',
'ja-JP': 'トイレはどこですか?',
'ko-KR': '화장실은 어디예요?'
},
favorite: false
},
{
id: 'p007',
category: Category.SHOPPING,
translations: {
'zh-CN': '可以便宜一点吗?',
'en-US': 'Can you make it cheaper?',
'ja-JP': 'もう少し安くなりませんか?',
'ko-KR': '좀 깎아주세요'
},
favorite: false
},
{
id: 'p008',
category: Category.SHOPPING,
translations: {
'zh-CN': '我要这个',
'en-US': 'I will take this',
'ja-JP': 'これをください',
'ko-KR': '이거 주세요'
},
favorite: false
}
];
static all(): PhraseEntry[] {
return PhraseDictionary.DATA.map((p) => ({ ...p, translations: { ...p.translations } }));
}
/**
* 按分类查询。
*/
static byCategory(category: Category): PhraseEntry[] {
return PhraseDictionary.all().filter((p) => p.category === category);
}
/**
* 全文搜索:在目标语言文本里 LIKE 匹配。
*/
static search(keyword: string, locale: string): PhraseEntry[] {
const kw = keyword.trim().toLowerCase();
if (kw.length === 0) return PhraseDictionary.all();
return PhraseDictionary.all().filter((p) =>
Object.values(p.translations).some((t) => t.toLowerCase().includes(kw)) ||
p.id.includes(kw));
}
/**
* 翻译一条短语到目标语言。
*/
static translate(entry: PhraseEntry, targetLocale: string): string {
return entry.translations[targetLocale] ?? entry.translations['en-US'] ?? '';
}
}
4.1 词典设计要点
translations 用 Record<string, string>(locale → 文本):新增语言只加一个 key,不改数据结构。翻译时按目标 locale 取值,缺失回退英文(兜底语言)。
深拷贝防串改:all() 里 { ...p, translations: { ...p.translations } } 做两层浅拷贝,避免 UI 层修改 favorite 污染静态数据(静态 DATA 是 readonly 语义,但对象本身可变,拷贝更稳妥)。
搜索在目标语言全文匹配:Object.values(p.translations) 遍历所有语言文本,任一语言命中即算命中——旅行场景「用中文搜英文短语」也支持。
五、收藏持久化:preferences
收藏状态要跨启动保留:
export class FavoriteStore {
private static readonly KEY: string = 'favorite_ids';
static async load(context: common.UIAbilityContext): Promise<Set<string>> {
const store = await preferences.getPreferences(context, 'translator');
const raw = await store.get(FavoriteStore.KEY, '[]');
try {
const arr = typeof raw === 'string' ? JSON.parse(raw) as string[] : [];
return new Set(arr);
} catch {
return new Set();
}
}
static async save(context: common.UIAbilityContext, ids: Set<string>): Promise<void> {
const store = await preferences.getPreferences(context, 'translator');
await store.put(FavoriteStore.KEY, JSON.stringify(Array.from(ids)));
await store.flush();
}
}
Set 序列化成数组存储——JSON 不支持 Set,这是常见处理。收藏操作「读 Set → 增删 → 回写」,配合页面层刷新。
六、区域检测:跟随系统还是手动切换
import { i18n } from '@kit.ArkTS';
export class LocaleService {
/**
* 获取系统当前语言(如 'zh-CN'、'en-US')。
*/
static getSystemLocale(): string {
return i18n.System.getSystemLanguage();
}
/**
* 判断是否为中文环境(翻译界面的目标语言默认策略)。
*/
static isChineseEnv(): boolean {
return LocaleService.getSystemLocale().startsWith('zh');
}
/**
* 支持的语言列表(词典覆盖范围)。
*/
static supportedLocales(): string[] {
return ['zh-CN', 'en-US', 'ja-JP', 'ko-KR'];
}
}
应用 UI 语言跟随系统(资源限定符自动处理);翻译的目标语言则由用户手动选择(中文用户出国默认目标是本地语言,但可手动改)。两套机制互不冲突:UI 本地化走资源限定符,业务数据本地化走词典模型。
七、代码定位表
| 代码块 | 关注点 | 改动入口 |
|---|---|---|
| base/zh_CN 资源目录 | 限定符匹配与回退 | 新增语言时加资源目录 |
| I18nUtil | $r 与 resourceManager | 动态取资源时用 |
| DateTimeFmt/NumberFmt/SortUtil | Intl 三件套 | 调整格式时改 options |
| PhraseDictionary | 词典数据与搜索翻译 | 增词时加 DATA 条目 |
| FavoriteStore | 收藏持久化 | 调整存储格式时改这里 |
| LocaleService | 系统区域检测 | 扩展支持语言时改 supportedLocales |
八、本篇小结
国际化服务层的三大支柱:资源限定符(目录名自动匹配语言)、Intl API(引擎决定格式细节)、词典模型(Record<locale, text> 天然支持多语种)。三者各管一段:界面文案、数据格式、业务内容,互不越界。
关键记忆点:base 目录必须存在兜底;格式化用位置参数 %1$s;currency 用 ISO 代码;Collator 管排序;UI 语言与数据语言解耦。
更多推荐


所有评论(0)