鸿蒙APP国际化与本地化:不只是翻译
,},},"value": "我的应用"},"value": "欢迎使用我的应用"},"value": "登录"},"value": "首页"阿拉伯语(使用最广泛的RTL语言)希伯来语(以色列官方语言)波斯语(伊朗)乌尔都语(巴基斯坦)据统计,全球约有4亿人使用RTL语言。如果你的应用要进入中东、北非市场,RTL适配是必选项,而不是可选项。Canvas绘制的内容不会自动镜像,需要开发者监听语言变化
鸿蒙APP国际化与本地化:不只是翻译
前言
随着鸿蒙生态的全球化进程加速,越来越多的应用需要走出国门,服务不同语言、文化和地区的用户。很多开发者在做国际化时存在一个误区:认为国际化就是把中文翻译成英文。实际上,真正的国际化远不止翻译文本这么简单。
国际化(Internationalization,简称i18n)是应用架构层面的设计,而本地化(Localization,简称l10n)是针对特定地区的适配。两者结合,才能让应用真正融入目标市场。鸿蒙系统在设计之初就将国际化作为核心能力,从资源管理到布局适配,再到隐私合规,提供了一整套完整的解决方案。
本文将从资源文件管理、从右至左语言(RTL)布局适配、各地区合规要求三个维度,系统讲解鸿蒙APP国际化的完整实践。
一、资源文件管理机制
1.1 资源目录结构设计
鸿蒙采用资源与代码分离的设计原则,所有需要本地化的内容都存放在resources目录下。这种设计让新增语言支持变得非常简单——只需要添加对应的资源文件,无需修改代码逻辑。
1.1.1 标准资源目录结构
resources/
├── base/ # 默认资源目录(必选)
│ ├── element/ # 字符串、颜色、数值等元素资源
│ │ └── string.json # 默认语言字符串(通常是英文)
│ ├── media/ # 图片、音频等媒体资源
│ └── profile/ # 配置文件
├── en_US/ # 美式英语
│ └── element/
│ └── string.json # 英语字符串
├── zh_CN/ # 简体中文
│ └── element/
│ └── string.json
├── zh_HK/ # 繁体中文(香港)
│ └── element/
│ └── string.json
├── ar_AE/ # 阿拉伯语(阿联酋)
│ └── element/
│ └── string.json
└── fr_FR/ # 法语
└── element/
└── string.json
核心原则:base目录是必须存在的默认资源目录,当系统找不到匹配语言的资源时,会回退到base目录加载资源。因此,务必确保base目录包含所有资源定义。
1.2 字符串资源文件格式
鸿蒙使用JSON格式管理字符串资源,键值对的结构清晰易懂。
1.2.1 基础字符串定义
base/element/string.json(默认语言,建议使用英文):
{
"string": [
{
"name": "app_name",
"value": "MyApp"
},
{
"name": "welcome_text",
"value": "Welcome to MyApp"
},
{
"name": "login_button",
"value": "Login"
},
{
"name": "home_title",
"value": "Home"
}
]
}
zh_CN/element/string.json(简体中文):
{
"string": [
{
"name": "app_name",
"value": "我的应用"
},
{
"name": "welcome_text",
"value": "欢迎使用我的应用"
},
{
"name": "login_button",
"value": "登录"
},
{
"name": "home_title",
"value": "首页"
}
]
}
1.2.2 带占位符的字符串
实际开发中,很多字符串需要动态插入内容,如“你好,{用户名}”。鸿蒙支持带占位符的字符串定义。
base/element/string.json:
{
"string": [
{
"name": "greeting",
"value": "Hello, %s"
},
{
"name": "order_count",
"value": "You have %d orders"
},
{
"name": "welcome_back",
"value": "Welcome back, %s! Last login: %s"
}
]
}
zh_CN/element/string.json:
{
"string": [
{
"name": "greeting",
"value": "你好,%s"
},
{
"name": "order_count",
"value": "您有%d个订单"
},
{
"name": "welcome_back",
"value": "欢迎回来,%s!上次登录:%s"
}
]
}
1.3 在代码中引用资源
1.3.1 基础引用方式
ArkTS中通过$r或$string语法引用资源:
@Entry
@Component
struct WelcomePage {
build() {
Column() {
// 使用$r引用资源,格式:$r('app.string.资源名')
Text($r('app.string.welcome_text'))
.fontSize(24)
.fontWeight(FontWeight.Bold)
// $string是简化写法
Button($string('login_button'))
.onClick(() => {
console.log('登录按钮点击');
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
1.3.2 带占位符的字符串使用
@Entry
@Component
struct GreetingPage {
@State userName: string = '张三';
@State orderCount: number = 5;
@State lastLogin: string = '2026-03-09 10:30';
build() {
Column() {
// 单个占位符
Text($r('app.string.greeting', this.userName))
.fontSize(18)
// 多个占位符按顺序传入
Text($r('app.string.welcome_back', this.userName, this.lastLogin))
.fontSize(16)
.margin({ top: 8 })
// 数字占位符
Text($r('app.string.order_count', this.orderCount))
.fontSize(16)
.margin({ top: 8 })
}
.padding(16)
.width('100%')
}
}
1.4 复数形式处理
不同语言对复数的表达规则差异很大。比如英语有单数和复数之分(1 apple vs 2 apples),而中文没有复数变化。鸿蒙通过plural类型支持复数处理。
base/element/string.json:
{
"plural": [
{
"name": "message_count",
"value": [
{
"quantity": "one",
"value": "%d message"
},
{
"quantity": "other",
"value": "%d messages"
}
]
}
]
}
使用示例:
Text($r('app.plural.message_count', 1)) // 输出: "1 message"
Text($r('app.plural.message_count', 5)) // 输出: "5 messages"
1.5 资源匹配规则
当系统语言切换时,鸿蒙按照以下优先级加载资源:
- 精确匹配:优先查找完全匹配语言和地区的资源目录(如
zh_CN) - 语言匹配:若无精确匹配,查找仅语言匹配的目录(如
zh) - 回退匹配:查找父语言(如
zh_Hans回退到zh) - 默认资源:若以上都未找到,使用
base目录资源
这种机制确保了即使用户设置了不常见的语言组合,应用也能有合理的显示内容。
1.6 图片资源本地化
不同文化背景下,同一概念可能需要不同的图片表达。例如“邮箱”图标,在中国可能用类似网易邮箱的图标,而在国外可能更通用的是信封图标。
资源目录结构:
resources/
├── base/media/
│ └── icon_mail.png # 默认邮箱图标
├── zh_CN/media/
│ └── icon_mail.png # 中文用户看到的图标
└── en_US/media/
└── icon_mail.png # 英文用户看到的图标
代码引用(与字符串引用方式一致):
Image($r('app.media.icon_mail'))
.width(24)
.height(24)
1.7 配置文件的国际化
module.json5中也可以使用资源引用,实现应用名称等配置的国际化:
{
"module": {
"name": "entry",
"label": "$string:app_name", // 引用字符串资源
"description": "$string:module_desc",
"icon": "$media:app_icon" // 可本地化的图标
}
}
二、从右至左语言(RTL)布局适配
2.1 什么是RTL?
RTL(Right-to-Left)是指从右向左书写的语言,主要包括:
- 阿拉伯语(使用最广泛的RTL语言)
- 希伯来语(以色列官方语言)
- 波斯语(伊朗)
- 乌尔都语(巴基斯坦)
据统计,全球约有4亿人使用RTL语言。如果你的应用要进入中东、北非市场,RTL适配是必选项,而不是可选项。
2.2 鸿蒙的RTL支持机制
鸿蒙系统对RTL提供原生支持,当系统语言切换为RTL语言时,系统会自动:
- 整体布局方向切换为RTL
- 文本阅读方向自动调整
Row/Flex等容器组件子元素顺序镜像- 列表、导航组件的交互方向反转
前提条件:你的代码必须“语义化”,不能写死方向相关的属性。
2.3 核心原则:永远不要写死left/right
这是RTL适配中最常见、最致命的问题。
❌ 错误示例
// 错误:写死了左边距
Text('返回')
.margin({ left: 16 })
// 错误:绝对定位写死x坐标
Image()
.position({ x: 20, y: 100 })
这段代码在LTR环境下完全正常,但在RTL环境下:
- 系统已经整体翻转了布局
- 你强行加了
left边距 - 结果就是布局错乱
✅ 正确示例
// 正确:使用语义化的start/end
Text('返回')
.margin({ start: 16 })
// 正确:使用LocalizedEdges参数
Image()
.position({ start: LengthMetrics.px(20), top: LengthMetrics.px(100) })
start在LTR下等价于left,在RTL下等价于right。你不用关心具体方向,系统会根据当前语言自动计算。
2.4 基础RTL自适应组件
2.4.1 带图标的按钮
@Entry
@Component
struct RtlButtonDemo {
build() {
Row() {
Image($r('app.media.arrow_back'))
.width(24)
.height(24)
Text($string('back_button'))
.margin({ start: 8 }) // 关键:使用start
}
.padding({ start: 16, end: 16 })
.height(48)
.backgroundColor('#f5f5f5')
.borderRadius(8)
}
}
运行效果:
- LTR下:箭头在左,文字在右,箭头和文字间距8px
- RTL下:箭头在右,文字在左,箭头和文字间距8px(自动镜像)
2.4.2 列表项布局
设置页常见的左右结构(左边标题,右边箭头或开关):
@Component
struct SettingItem {
private title: string = '';
private showArrow: boolean = true;
build() {
Row() {
Text(this.title)
.fontSize(16)
.layoutWeight(1) // 占满剩余空间
.textAlign(TextAlign.Start) // 关键:使用Start
if (this.showArrow) {
Image($r('app.media.arrow_forward'))
.width(16)
.height(16)
.margin({ start: 8 }) // 间距用start
} else {
Toggle({ type: ToggleType.Switch })
.margin({ start: 8 })
}
}
.padding({ start: 16, end: 16, top: 12, bottom: 12 })
.width('100%')
.backgroundColor(Color.White)
}
}
2.5 文本对齐的正确方式
很多人习惯这样写:
// ❌ 错误:写死对齐方向
Text('مرحبا')
.textAlign(TextAlign.Left)
问题是:Left在RTL语言中并不是阅读起点。正确做法:
// ✅ 正确:使用Start
Text('مرحبا')
.textAlign(TextAlign.Start)
// 或让系统自动处理(不设置textAlign时默认Start)
Text('مرحبا')
2.6 图标镜像处理
对于方向性极强的图标(如返回箭头、前进箭头),在RTL下需要镜像显示,以符合用户的认知习惯。
// 自动镜像:当系统为RTL时,图标水平翻转
Image($r('app.media.arrow_back'))
.width(24)
.height(24)
.autoMirror(true) // API 12+ 支持
// 或手动控制
Image($r('app.media.arrow_back'))
.width(24)
.height(24)
.mirror(this.isRTL()) // 自定义判断
注意:普通装饰性图片(如Logo、头像)不需要镜像,镜像后反而会显得奇怪。
2.7 自定义绘制与Canvas处理
Canvas绘制的内容不会自动镜像,需要开发者监听语言变化并重绘。
import { commonEventManager } from '@kit.BasicServicesKit';
@Entry
@Component
struct CanvasDemo {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
@State currentDirection: Direction = Direction.Auto;
aboutToAppear() {
// 监听系统语言变化
this.listenLocaleChange();
}
listenLocaleChange() {
let subscriber: commonEventManager.CommonEventSubscriber | null = null;
let subscribeInfo: commonEventManager.CommonEventSubscribeInfo = {
events: ["usual.event.LOCALE_CHANGED"]
};
commonEventManager.createSubscriber(subscribeInfo, (err, data) => {
if (err) {
console.error('创建订阅者失败', err);
return;
}
subscriber = data;
commonEventManager.subscribe(subscriber, (err, data) => {
if (!err) {
// 语言变化,重新绘制
this.drawContent();
}
});
});
}
drawContent() {
this.context.clearRect(0, 0, 500, 500);
this.context.font = '30px sans-serif';
this.context.fillStyle = Color.Black;
// 文本绘制方向由Canvas的direction属性控制
this.context.fillText('Hello 123', 50, 50);
}
build() {
Canvas(this.context)
.direction(this.currentDirection)
.width('100%')
.height(200)
.backgroundColor('#f0f0f0')
.onReady(() => {
this.drawContent();
})
}
}
2.8 常见问题排查清单
如果你的RTL适配不生效,按以下顺序排查:
- 是否写死了left/right? → 全部替换为start/end
- 是否写死了对齐方式? → 用TextAlign.Start替换Left/Right
- 父容器是否强制指定了FlexDirection? → 尽量不指定,或使用RowDirection
- 绝对定位是否使用了x/y? → 用start/end替代
- 自定义View是否处理了镜像? → Canvas需要手动重绘
2.9 RTL适配自测方法
在DevEco Studio中,可以通过以下方式测试RTL效果:
- 修改模拟器语言:设置 > 系统 > 语言和输入法 > 添加阿拉伯语并设为默认
- 代码中临时切换(调试用):
// 在EntryAbility中临时设置
onCreate() {
// 强制使用RTL调试
this.context.getApplicationContext().setLocale('ar');
}
三、各地区合规要求
国际化不仅是技术适配,更是法律合规。不同国家和地区对数据隐私有严格要求,最典型的是欧盟的GDPR和中国的《个人信息保护法》。
3.1 隐私政策要求
3.1.1 隐私政策的基本要素
无论上架哪个应用市场,隐私政策都是强制要求。根据华为开发者联盟的要求:
- 独立文本:隐私政策不能作为用户协议的一部分,必须有独立文档
- 易于访问:用户在应用主功能界面中通过≤4次点击可访问
- 清晰易懂:语言通俗,避免歧义
- 完整披露:包含收集信息的目的、方式、范围,处理者名称和联系方式
3.1.2 第三方SDK披露
如果应用集成了第三方SDK(如推送、分析、支付SDK),必须在隐私政策中逐一披露:
| SDK名称 | 提供方 | 收集信息类型 | 使用目的 |
|---|---|---|---|
| 华为推送SDK | 华为软件技术有限公司 | AAID、Token、设备信息 | 推送通知 |
| 地图SDK | 华为软件技术有限公司 | 位置信息、设备信息 | 显示地图 |
隐私政策示例条款:
第三方SDK名称:华为推送SDK
第三方公司名称:华为软件技术有限公司
收集个人信息类型:设备信息(AAID、Token)、网络信息
使用目的:向用户推送通知消息
隐私政策链接:https://......
3.2 权限申请规范
3.2.1 权限声明必须透明
在module.json5中声明权限时,必须提供reason字段说明用途:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.LOCATION",
"reason": "$string:location_reason", // 引用字符串资源
"usedScene": {
"abilities": ["MainAbility"],
"when": "inuse" // 仅在前台使用
}
},
{
"name": "ohos.permission.CAMERA",
"reason": "用于扫描二维码添加好友",
"usedScene": {
"abilities": ["ScanAbility"],
"when": "always"
}
}
]
}
}
3.2.2 延迟初始化原则
为了避免应用在未获用户同意前收集个人信息,所有SDK必须在用户同意隐私政策后才能初始化。
// 首次启动检查
async function checkPrivacyAgreement() {
let agreed = await Preferences.get('privacy_agreed', false);
if (!agreed) {
// 显示隐私政策弹窗
showPrivacyDialog(async (accept) => {
if (accept) {
await Preferences.put('privacy_agreed', true);
// 用户同意后,才能初始化SDK
initSDKs();
} else {
// 用户拒绝,退出应用或仅使用基础功能
exitApp();
}
});
} else {
// 已经同意过,正常初始化
initSDKs();
}
}
function initSDKs() {
// 初始化所有第三方SDK
AnalyticsSDK.init();
PushSDK.init();
// ...
}
3.2.3 可选权限的处理
对于非必需权限,应该:
- 不默认申请
- 在需要该功能时才申请
- 允许用户拒绝且不影响基本功能
// 用户点击"扫一扫"时才申请相机权限
async function onScanClick() {
let granted = await checkPermission('ohos.permission.CAMERA');
if (!granted) {
// 申请权限,并说明用途
let result = await requestPermission('ohos.permission.CAMERA', {
message: '需要相机权限扫描二维码添加好友'
});
if (!result) {
// 用户拒绝,可以提示但不要强制
prompt.showToast('无法使用扫码功能');
return;
}
}
// 打开扫码页面
navigateToScan();
}
3.3 GDPR特别要求
GDPR(通用数据保护条例)是欧盟最严格的隐私法规,对应用有以下核心要求:
3.3.1 数据最小化原则
只收集实现功能所必需的最少数据。例如:
- 不需要定位的应用不要申请定位权限
- 不需要读取联系人的应用不要申请通讯录权限
- 分析数据尽可能匿名化
3.3.2 用户权利保障
必须支持用户行使以下权利:
- 知情权:清晰说明收集哪些数据、用于什么目的
- 访问权:用户可以查看自己被收集的数据
- 删除权:用户可以要求删除自己的数据(“被遗忘权”)
- 撤回同意:用户可以撤回之前同意的授权
// 提供数据删除功能
async function deleteUserData(userId: string) {
// 1. 删除本地数据
await Database.deleteUserData(userId);
// 2. 通知服务器删除
await api.deleteUserAccount(userId);
// 3. 注销SDK(停止数据收集)
AnalyticsSDK.unInit();
// 4. 清除本地缓存
await Preferences.clear();
prompt.showToast('您的数据已删除');
}
3.3.3 数据跨境传输
如果数据要传输到欧盟境外,必须满足以下条件之一:
- 获得用户明确同意
- 数据传输到有充分保护水平国家(如GDPR充分性认定)
- 签署标准合同条款(SCCs)
华为从2016年就开始对标GDPR要求,在研发、运营各环节融入隐私保护和跨境传输要求。
3.4 GDPR合规检查清单
| 检查项 | 要求 | 实现方式 |
|---|---|---|
| 隐私政策 | 独立文本、易于访问 | 设置页添加"隐私政策"入口 |
| 用户同意 | 首次启动获取同意 | 启动弹窗,记录同意状态 |
| 数据最小化 | 只收集必要数据 | 审计所有SDK和权限 |
| 权限透明 | 申请时说明用途 | 所有权限配置reason字段 |
| 延迟初始化 | 同意后才初始化SDK | 同意前不调用SDK初始化 |
| 撤回同意 | 用户可撤回 | 设置页提供"撤回同意"选项 |
| 数据删除 | 提供删除功能 | 实现"注销账号"功能 |
| 数据导出 | 用户可导出数据 | 提供"导出我的数据"功能 |
| 第三方披露 | 列明所有SDK | 隐私政策中逐一列出 |
| 儿童保护 | 未满14周岁需监护人同意 | 注册时验证年龄 |
3.5 中国《个人信息保护法》要求
中国的个人信息保护法与GDPR有许多相似之处,但也有一些特殊要求:
3.5.1 敏感个人信息定义
包括:生物识别、宗教信仰、特定身份、医疗健康、金融账户、行踪轨迹等。处理敏感个人信息需要:
- 取得用户的单独同意
- 告知必要性及对用户的影响
3.5.2 未成年人保护
处理不满十四周岁未成年人个人信息,应取得其父母或其他监护人同意。
async function registerUser(age: number, userInfo: UserInfo) {
if (age < 14) {
// 弹出监护人确认页面
let guardianAgreed = await showGuardianConfirm();
if (!guardianAgreed) {
return { success: false, message: '需监护人同意' };
}
}
// 继续注册流程
}
3.6 应用市场上架审核
3.6.1 常见拒审原因
根据OpenHarmony应用市场2025年数据,32%的应用首次提审被拒:
| 拒审原因 | 解决方案 |
|---|---|
| 隐私政策无法访问 | 确保URL可公开访问,包含完整条款 |
| 权限理由不充分 | module.json5中补充详细reason |
| 超范围收集数据 | 审计SDK,移除不必要的数据收集 |
| 未提供用户数据删除功能 | 添加账号注销功能 |
| 第三方SDK未披露 | 隐私政策中列出所有SDK |
3.6.2 上架材料准备
提交应用市场审核时,通常需要准备:
- HAP包(Release签名)
- 隐私政策URL(可直接访问的网页)
- 权限使用说明(PDF文档,详细解释每个权限用途)
- 多设备截图(手机、平板各3-5张)
- 测试账号(如果应用需要登录)
3.7 合规代码实践
3.7.1 隐私弹窗组件
@Component
export struct PrivacyDialog {
private onAgree: () => void;
private onDisagree: () => void;
build() {
Column() {
Text($string('privacy_title'))
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
Scroll() {
Text($string('privacy_content'))
.fontSize(14)
.lineHeight(20)
}
.height(200)
.padding(12)
.backgroundColor('#f5f5f5')
.borderRadius(8)
.margin({ bottom: 16 })
Row() {
Button($string('disagree'))
.onClick(() => {
this.onDisagree();
})
.backgroundColor('#cccccc')
.layoutWeight(1)
.margin({ end: 8 })
Button($string('agree'))
.onClick(() => {
Preferences.put('privacy_agreed', true);
this.onAgree();
})
.backgroundColor('#007dff')
.layoutWeight(1)
.margin({ start: 8 })
}
.width('100%')
}
.padding(20)
.width('90%')
.backgroundColor(Color.White)
.borderRadius(16)
}
}
3.7.2 权限申请封装
export class PermissionManager {
// 检查并申请权限
static async requestPermission(
permission: string,
reason: string
): Promise<boolean> {
let grantStatus = await abilityAccessCtrl.createAtManager()
.checkPermissions([permission]);
if (grantStatus === 0) { // 0表示已授权
return true;
}
// 显示权限说明
let shouldRequest = await this.showPermissionReason(permission, reason);
if (!shouldRequest) {
return false;
}
// 申请权限
let result = await abilityAccessCtrl.createAtManager()
.requestPermissions([permission]);
return result[permission] === 0;
}
private static async showPermissionReason(
permission: string,
reason: string
): Promise<boolean> {
return new Promise((resolve) => {
AlertDialog.show({
title: $string('permission_required'),
message: reason,
primaryButton: {
value: $string('deny'),
action: () => resolve(false)
},
secondaryButton: {
value: $string('allow'),
action: () => resolve(true)
}
});
});
}
}
// 使用示例
async function onLocationClick() {
let granted = await PermissionManager.requestPermission(
'ohos.permission.LOCATION',
'需要位置权限以显示附近的门店'
);
if (granted) {
// 执行定位操作
}
}
四、实战案例:构建国际化的设置页面
结合以上知识点,实现一个完整支持多语言和RTL的设置页面。
4.1 资源文件
base/element/string.json:
{
"string": [
{
"name": "settings_title",
"value": "Settings"
},
{
"name": "account_section",
"value": "Account"
},
{
"name": "profile",
"value": "Profile"
},
{
"name": "notifications",
"value": "Notifications"
},
{
"name": "privacy",
"value": "Privacy"
},
{
"name": "language",
"value": "Language"
},
{
"name": "dark_mode",
"value": "Dark Mode"
},
{
"name": "logout",
"value": "Log Out"
}
]
}
ar_AE/element/string.json(阿拉伯语):
{
"string": [
{
"name": "settings_title",
"value": "الإعدادات"
},
{
"name": "account_section",
"value": "الحساب"
},
{
"name": "profile",
"value": "الملف الشخصي"
},
{
"name": "notifications",
"value": "الإشعارات"
}
]
}
4.2 设置页面组件
import router from '@ohos.router';
@Component
struct SettingItem {
private icon: Resource;
private title: string;
private showArrow: boolean = true;
private onClick?: () => void;
build() {
Row() {
// 图标
Image(this.icon)
.width(24)
.height(24)
.margin({ end: 16 }) // 图标和文字的间距
Text(this.title)
.fontSize(16)
.layoutWeight(1)
.textAlign(TextAlign.Start)
if (this.showArrow) {
Image($r('app.media.arrow_forward'))
.width(16)
.height(16)
.autoMirror(true) // RTL下自动镜像
.margin({ start: 8 })
}
}
.padding({ start: 16, end: 16, top: 12, bottom: 12 })
.width('100%')
.backgroundColor(Color.White)
.onClick(() => {
this.onClick?.();
})
}
}
@Entry
@Component
struct SettingsPage {
build() {
Column() {
// 标题栏
Row() {
Image($r('app.media.arrow_back'))
.width(24)
.height(24)
.autoMirror(true)
.onClick(() => {
router.back();
})
Text($string('settings_title'))
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ start: 12 })
}
.padding({ start: 16, end: 16, top: 12, bottom: 12 })
.width('100%')
.backgroundColor('#f8f9fa')
// 内容列表
List() {
// 账户分组
ListItem() {
Column() {
Text($string('account_section'))
.fontSize(14)
.fontColor('#666666')
.width('100%')
.padding({ start: 16, bottom: 8, top: 16 })
SettingItem({
icon: $r('app.media.ic_profile'),
title: $string('profile'),
onClick: () => router.pushUrl({ url: 'pages/ProfilePage' })
})
}
}
// 偏好设置分组
ListItem() {
Column() {
Text('Preferences')
.fontSize(14)
.fontColor('#666666')
.width('100%')
.padding({ start: 16, bottom: 8, top: 16 })
SettingItem({
icon: $r('app.media.ic_notifications'),
title: $string('notifications'),
onClick: () => router.pushUrl({ url: 'pages/NotificationsPage' })
})
SettingItem({
icon: $r('app.media.ic_language'),
title: $string('language'),
onClick: () => router.pushUrl({ url: 'pages/LanguagePage' })
})
Row() {
Text($string('dark_mode'))
.fontSize(16)
Toggle({ type: ToggleType.Switch })
.margin({ start: 8 })
}
.padding({ start: 16, end: 16, top: 12, bottom: 12 })
.width('100%')
.backgroundColor(Color.White)
}
}
// 退出登录按钮
ListItem() {
Button($string('logout'))
.width('90%')
.height(48)
.backgroundColor('#ff4444')
.margin({ top: 24 })
.onClick(() => {
// 退出登录逻辑
})
}
.width('100%')
.listItemAlign(ListItemAlign.Center)
}
.width('100%')
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#f5f5f5')
}
}
这个页面在LTR和RTL下都能正常显示:
- 所有间距使用
start/end - 箭头图标设置
autoMirror(true) - 文本对齐使用
TextAlign.Start - 布局完全自适应
五、总结
鸿蒙应用的国际化与本地化是一项系统工程,涉及资源管理、布局适配、合规要求三个核心维度。
资源文件管理是基础,通过合理的目录结构和JSON资源文件,一套代码可以支持任意数量的语言。关键原则是"资源与代码分离",新增语言只需添加资源文件,无需修改业务逻辑。
RTL布局适配是中东市场的入场券,鸿蒙提供了强大的自动镜像能力,开发者只需要遵循"语义化"原则,用start/end替代left/right,用TextAlign.Start替代Left/Right对齐,90%的布局问题都会自动解决。对于Canvas等特殊场景,需要手动监听语言变化并重绘。
合规要求是全球上架的通行证。从GDPR到中国《个人信息保护法》,核心要求是"透明、最小化、用户控制":隐私政策要清晰透明,数据收集要最小化,用户要能控制自己的数据。权限申请必须说明用途,SDK必须在用户同意后才能初始化。
最后,记住国际化的核心心法:
- 代码层面:面向语言编程,不是面向地区编程
- 布局层面:相信系统,不要写死方向
- 合规层面:用户同意是第一原则
更多推荐




所有评论(0)