鸿蒙国际化技术RTL 文本方向 — 技术实现篇



一、RTL 是什么
RTL(Right-To-Left) 是从右向左书写的文字方向,全球使用者约 4 亿人,覆盖阿拉伯语(ar)、希伯来语(he)、波斯语(fa)、乌尔都语(ur)、普什图语(ps)等。支持 RTL 绝不是「把文字右对齐」这么简单,而是整个布局逻辑的镜像:
- 阅读顺序:从右上角开始,逐行向下;
- 行内顺序:
Row的首元素位于最右侧; - 对齐语义:
TextAlign.Start在 RTL 环境下表现为右对齐; - 图标/箭头:前进箭头应向左指(镜像翻转);
- 手势方向:左滑/右滑的语义同样需要镜像。
如果不做镜像,RTL 用户看到的界面顺序与阅读习惯完全相反,产品会被判定为「不可用」。本应用以「中东新闻」为演示场景,用中文(LTR)与阿拉伯语(RTL)双语言展示:语言切换的瞬间,文本方向、Row 排布、对齐方式全部镜像,让开发者直观理解 RTL 的完整技术栈。
二、数据模型:方向显式携带
2.1 语言配置
本应用的做法是数据模型显式携带方向字段,而不是在代码里散落硬编码判断:
interface LangCfg {
code: string; // locale 代码,如 zh_CN / ar_AE
name: string; // 语言显示名
flag: string; // 国旗 emoji
dir: string; // 'ltr' | 'rtl',方向元数据
}
const LANGS: LangCfg[] = [
{ code: 'zh_CN', name: '简体中文', flag: '🇨🇳', dir: 'ltr' },
{ code: 'ar_AE', name: 'العربية', flag: '🇦🇪', dir: 'rtl' }
];
方向字段 dir 有两个作用:
- 状态展示:语言徽章上直接显示
(LTR)/(RTL),用户对当前方向一目了然; - 逻辑判定:
isRtl()只需一行即可收敛方向判断,供全页复用:
private isRtl(): boolean {
return this.currentLocale === 'ar_AE';
}
把方向判断收敛到单一方法里,是 RTL 工程化的第一原则——不要在页面各处写 if (locale === 'ar') 这种散落的判断,否则后续新增波斯语、希伯来语时,每处都要改。用 dir 字段驱动判断,新增语言只需在 LANGS 里加一行。
2.2 新闻数据双语文案
演示数据同时携带中阿双语,由 isRtl() 决定取哪份:
interface NewsItem {
id: string;
zhTitle: string;
arTitle: string;
zhSummary: string;
arSummary: string;
timeZh: string;
timeAr: string;
categoryZh: string;
categoryAr: string;
viewsZh: string;
viewsAr: string;
}
private titleOf(n: NewsItem): string {
return this.isRtl() ? n.arTitle : n.zhTitle;
}
private summaryOf(n: NewsItem): string {
return this.isRtl() ? n.arSummary : n.zhSummary;
}
注意这里的抽取逻辑:UI 永远只调用 titleOf(n),不关心当前语言。切换语言后 @State currentLocale 变化触发整页重渲染,所有 xxxOf(n) 方法重新执行,文本层瞬间切换——业务代码与方向判断完全解耦。
三、文本层:bidi 混排(系统负责)
3.1 什么是 bidi
一个字符串同时含阿拉伯语、英文、数字、标点时,需要 bidi(Bidirectional Text,双向文本)算法决定每一段的显示顺序。Unicode 标准用 UAX#9 定义了这一算法,ICU 在其基础上实现了完整方案。
经典例子:
'Order #1001 costs $50.50' // 在 RTL 环境下:整句从右读,数字段内部保持 LTR
'مرحبا بالعالم رقم ١٢٣' // 阿语文本 + 阿拉伯-印度数字混排
关键规律:数字与英文内嵌段的方向不受外层段落方向影响。#1001 即使身处 RTL 段落,仍按 1001 从左到右读;$50.50 内部仍是 LTR 序列。这是因为 bidi 算法为每个字符分配方向类别,再用「隔离/嵌入」规则确定显示顺序。
3.2 Text 组件自动处理
HarmonyOS 的 Text 组件内部基于 ICU 的 bidi 算法自动排布,阿语文本无需任何额外处理即可正确显示。这是文本层的核心结论:Text 负责 bidi,开发者不需要手写方向解析。
本应用的 bidi 混排演示区:
Text(this.isRtl()
? (this.showBidi
? 'نموذج مختلط: الإصدار iOS 17.2 يدعم 120 دولة و 30 لغة.'
: 'هاتفك الجديد iPhone 15 Pro متوفر الآن بسعر 999$ فقط.')
: (this.showBidi
? '混排样例:iOS 17.2 支持 120 个国家、30 种语言。'
: '你的新手机 iPhone 15 Pro 现在仅售 $999。'))
.fontSize(15).fontColor('#1F2937')
.textAlign(this.isRtl() ? TextAlign.End : TextAlign.Start)
.width('100%')
iOS 17.2、120、iPhone 15 Pro、999$ 这些内嵌的 LTR 段在整句 RTL 中保持自身方向——这正是 bidi 算法的价值。Toggle 开关在两组样例间切换,方便用户反复观察「数字/英文段在阿语句子里的位置变化」。
3.3 混排的坑
- 不要手工拼接方向标记:有些开发者试图用 Unicode 控制符(如 RLE
U+202B、PDFU+202C)手动强制方向,这极易出错且难以维护,交给系统算法即可; - 测试要覆盖真实数据:订单号、版本号、价格这类「数字+字母」混合串是最容易出问题的,必须用真机 + 阿语键盘实测;
- emoji 与国旗:emoji 是方向中立的,但 ZWJ 序列(如家庭 emoji)在 RTL 下可能有细微差异,常规场景可忽略。
四、布局层:镜像(开发者负责)
4.1 Row 顺序镜像
文本层由系统搞定,布局层必须开发者手动处理。最典型的是 Row 的顺序:LTR 下第一个子组件在左,RTL 下应镜像到右。
新闻列表行:
Row() {
Text(this.catOf(n)).fontSize(10).fontColor('#B45309')
.padding({ left: 8, right: 8, top: 3, bottom: 3 }).backgroundColor('#FEF3C7').borderRadius(8)
Text(this.timeOf(n)).fontSize(11).fontColor('#9CA3AF')
.layoutWeight(1).textAlign(this.isRtl() ? TextAlign.Start : TextAlign.End)
}.width('100%')
- LTR:分类徽标在左,时间在右;
- RTL(镜像):分类徽标在右,时间在左。
4.2 详情页返回键镜像
返回键是最容易被忽略的镜像点。LTR 下「← 返回」在左,RTL 下「→ عودة」在右:
Row() {
Text(this.T(K.back)).fontSize(14).fontColor('#B45309')
Text(this.T(K.readMore)).fontSize(12).fontColor('#B45309').layoutWeight(1)
.textAlign(this.isRtl() ? TextAlign.End : TextAlign.Start)
}.width('100%')
.onClick(() => { this.selectedId = ''; })
注意文案本身也镜像了:中文「← 返回」vs 阿语「→ عودة」,箭头方向相反。这提醒我们:文案翻译也要遵循方向习惯,翻译团队应知晓目标语言的方向。
4.3 生产环境:容器级 direction
本应用为演示「手动镜像」的细节,用 isRtl() 逐处判断。生产环境更推荐容器级 direction 属性——给根容器设置 direction: Direction.Rtl,整个子树自动镜像:
Column()
.direction(Direction.Rtl) // 整棵子树镜像,Row 顺序、对齐全部反转
两种方式的取舍:
| 方式 | 优点 | 缺点 |
|---|---|---|
手动 isRtl() 判断 |
精细控制、逐个组件验证 | 代码分散、易漏改 |
容器级 direction |
一处声明全局生效、零遗漏 | 第三方组件可能不跟随、调试需全局视角 |
实际项目建议:框架性容器用 direction,业务组件用 Start/End 逻辑对齐,两者配合。
五、对齐:用 Start/End 而非 Left/Right
5.1 逻辑方向 vs 物理方向
TextAlign.Left / TextAlign.Right 是物理方向,写死不随语言变化;TextAlign.Start / TextAlign.End 是逻辑方向,LTR 下 Start=左,RTL 下 Start=右。
.textAlign(this.isRtl() ? TextAlign.End : TextAlign.Start)
本应用所有对齐都遵循这一原则。段落/列表项在 RTL 下右对齐,数字/百分比等「方向中立」内容由 bidi 算法保证按数字方向显示。
5.2 为什么是硬编码契约
把 Start/End 列为团队编码规范,比在 code review 时逐个检查 Left/Right 高效得多。lint 规则可以直接禁止 TextAlign.Left / TextAlign.Right 出现,从工具链层面杜绝回归。
六、状态管理与重渲染
6.1 语言状态持久化
@StorageLink(STORAGE_LOCALE) currentLocale: string = DEFAULT_LOCALE;
aboutToAppear(): void {
if (!AppStorage.get<string>(STORAGE_LOCALE)) {
AppStorage.setOrCreate(STORAGE_LOCALE, DEFAULT_LOCALE);
}
PersistentStorage.persistProp(STORAGE_LOCALE, DEFAULT_LOCALE);
}
@StorageLink + PersistentStorage 的组合:语言选择持久化到磁盘,下次启动恢复上次的语言——RTL 用户不需要每次打开 App 重新切语言。
6.2 状态驱动全页镜像
@State currentLocale / selectedId / showBidi 三个状态驱动整页:
currentLocale变化 → 文本层(xxxOf)与布局层(isRtl)同步刷新;selectedId变化 → 列表/详情切换;showBidi变化 → 混排样例切换。
语言一切换,方向状态、混排样例、布局演示同步变化,直观展示镜像效果。这正是 ArkUI 声明式 UI 的优势:状态是唯一事实,UI 是状态的投影,RTL 切换无需任何命令式刷新代码。
七、ArkTS 兼容要点
catch不需要类型注解:本页几乎没有可能抛错的 I/O 与intl调用,try/catch可省略;- UI 分支内不声明
const:所有派生数据抽成私有方法(titleOf()/summaryOf()/detail()),保证@Builder与 UI 分支的 ArkTS 约束; ForEachkey 生成器用稳定值:新闻n.id、语言l.code,避免重渲染时组件复用错乱;??空值合并:curCfg()/detail()用?? LANGS[0]兜底,保证非空返回,避免 UI 判空分支;- 模板字符串:
${l.flag} ${l.name} (${l.dir.toUpperCase()})的用法在 ArkTS 中受支持,但注意字符串拼接较多时应抽方法。
八、生产 RTL 检查清单
| 检查项 | 说明 | 本应用对应 |
|---|---|---|
| Row 顺序 | 首元素在 RTL 下应在右 | 列表行、详情元信息行 |
| TextAlign | 用 Start/End 而非 Left/Right | 所有 .textAlign() |
| 图标/箭头 | 前进/返回箭头需水平翻转 | 返回键文案镜像 |
| 输入框光标 | 光标插入位置、选中方向 | 本应用无输入场景 |
| 滚动条 | 滚动条位置、滚动方向 | Scroll 默认跟随 |
| 手势 | 左滑/右滑语义镜像 | 本应用无手势 |
九、扩展与进阶
- 自动检测系统方向:
i18n.System.getSystemLanguage()返回系统语言,判断其方向后自动设置全局direction,用户无需手动切换; - 数字本地化联动:阿语环境可选用阿拉伯-印度数字(
٠١٢٣٤٥٦٧٨٩),intl.NumberFormat的numberingSystem选项可控制; - TTS 朗读顺序:无障碍朗读应遵循 RTL 顺序,配合
Accessibility服务配置; - 动画镜像:滑入/滑出、进度条填充方向的动画同样需要镜像。
十、小结
RTL 支持 = 文本 bidi 自动排布(系统负责) + 布局镜像(开发者负责)。本应用把「方向状态 → 混排展示 → 布局镜像」三步演示闭环:Text 组件按 UAX#9 自动处理阿语与数字英文混排;Row 顺序、TextAlign 对齐、返回键方向由 isRtl() 显式镜像。理解了这一层,出海中东/以色列的电商、社交、新闻类产品都能快速落地正确的 RTL 体验。
更多推荐




所有评论(0)