鸿蒙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 资源匹配规则

当系统语言切换时,鸿蒙按照以下优先级加载资源:

  1. 精确匹配:优先查找完全匹配语言和地区的资源目录(如zh_CN
  2. 语言匹配:若无精确匹配,查找仅语言匹配的目录(如zh
  3. 回退匹配:查找父语言(如zh_Hans回退到zh
  4. 默认资源:若以上都未找到,使用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适配不生效,按以下顺序排查:

  1. 是否写死了left/right? → 全部替换为start/end
  2. 是否写死了对齐方式? → 用TextAlign.Start替换Left/Right
  3. 父容器是否强制指定了FlexDirection? → 尽量不指定,或使用RowDirection
  4. 绝对定位是否使用了x/y? → 用start/end替代
  5. 自定义View是否处理了镜像? → Canvas需要手动重绘

2.9 RTL适配自测方法

在DevEco Studio中,可以通过以下方式测试RTL效果:

  1. 修改模拟器语言:设置 > 系统 > 语言和输入法 > 添加阿拉伯语并设为默认
  2. 代码中临时切换(调试用):
// 在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 可选权限的处理

对于非必需权限,应该:

  1. 不默认申请
  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必须在用户同意后才能初始化。

最后,记住国际化的核心心法:

  • 代码层面:面向语言编程,不是面向地区编程
  • 布局层面:相信系统,不要写死方向
  • 合规层面:用户同意是第一原则
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐