鸿蒙国际化示例——多语言新闻阅读器 — UI 设计篇



一、设计目标
多语言新闻阅读器既要"好看",更要"经得起语言切换的考验"。UI 设计有两个硬约束:
- 任何文案长度在不同语言下差异巨大:德语单词长、日韩文信息密度高,固定宽度容器会被撑破。实测同一句"已发布":中文 3 字、英文 “Published on” 12 字符、德语 “Veröffentlicht am” 18 字符、阿拉伯语 “نُشر في” 7 字符——最长的德语比中文多出 6 倍宽度;
- 数字/时间/日期要随语言变化而变:不能把格式化结果当静态文本写死在布局里。阅读量 23145 在
en_US下是 “23,145”,在德语下是 “23.145”,在阿拉伯语下是 “٢٣٬١٤٥”。
因此本页 UI 的核心原则是:所有容器自适应内容,禁止写死宽度;所有文本通过 t() 与格式化函数取值,布局只负责位置与层级。这也是本系列所有 14 个应用共同遵循的 UI 戒律。
二、页面结构(组件树)
MultiLangSwitch(@Entry 根组件)
└─ Column(主容器,宽 100%,背景 #F8FAFC)
├─ Row:App 标题(环球新闻)+ 副标题 + 语言按钮(右上角)
├─ [if langPanel] 语言选择面板(卡片,FlexWrap 换行 8 语言)
├─ [if isRtl()] RTL 方向提示条(黄色警告条)
├─ 内容区(三选一):
│ ├─ DetailView(selectedNews()) ← selectedId !== '' 时
│ ├─ HomeView() ← tab === 'home'
│ ├─ DiscoverView() ← tab === 'discover'
│ └─ MineView() ← tab === 'mine'
└─ Row:底部导航(🏠 首页 / 🧭 发现 / 👤 我的)
与普通新闻 App 的区别:语言按钮常驻顶部(而非藏在设置里),这是 i18n 产品的核心交互决策——语言切换是高频操作,必须一键可达。
三、色彩与字号规范
背景 #F8FAFC(浅灰蓝),卡片纯白,构成"卡片悬浮在浅底"的层次感:
| 元素 | 色值 | 字号 | 字重 |
|---|---|---|---|
| App 标题 | #0F172A | 22 | Bold |
| 新闻标题 | #0F172A | 16 | Bold |
| 详情标题 | #0F172A | 20 | Bold |
| 正文 | #1F2937 | 15 | Normal |
| 摘要/副标题 | #64748B | 13 | Normal |
| 时间/统计 | #94A3B8 | 11~12 | Normal |
| 分类徽标 | 文字 #3B82F6 / 底 #EFF6FF | 10 | Normal |
| 选中 Tab | #3B82F6 蓝底白字 | 12 | Normal |
| RTL 提示条 | 文字 #B45309 / 底 #FEF3C7 | 11 | Normal |
四、语言选择面板的实现
语言面板是本页 UI 最核心的交互。采用下拉展开卡片而非弹窗,减少层级、保持上下文:
// 语言元数据:code(locale 代码)+ name(母语名)+ flag(国旗 emoji)
interface LocaleInfo {
code: string; // locale 代码,如 zh_CN
name: string; // 母语名称
flag: string; // 国旗 emoji
}
const LOCALES: LocaleInfo[] = [
{ code: 'zh_CN', name: '简体中文', flag: '🇨🇳' },
{ code: 'zh_TW', name: '繁體中文', flag: '🇭🇰' },
{ code: 'en_US', name: 'English', flag: '🇺🇸' },
{ code: 'ja_JP', name: '日本語', flag: '🇯🇵' },
{ code: 'ko_KR', name: '한국어', flag: '🇰🇷' },
{ code: 'ar_AE', name: 'العربية', flag: '🇦🇪' },
{ code: 'de_DE', name: 'Deutsch', flag: '🇩🇪' },
{ code: 'es_ES', name: 'Español', flag: '🇪🇸' }
];
面板渲染的核心是 FlexWrap 自动换行——8 个语言标签在窄屏上自动折行,无需人工排布:
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
ForEach(LOCALES, (item: LocaleInfo) => {
Row({ space: 6 }) {
Text(item.flag).fontSize(16)
Text(item.name).fontSize(12)
if (this.currentLocale === item.code) {
Text('✓').fontSize(12).fontColor('#10B981').fontWeight(FontWeight.Bold)
}
}
.padding({ left: 10, right: 10, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor(this.currentLocale === item.code ? '#E0F2FE' : '#F1F5F9')
.onClick(() => {
this.currentLocale = item.code; // ← 核心:只改一个状态
this.langPanel = false;
promptAction.showToast({ message: `${item.flag} ${item.name}`, duration: 1200 });
})
}, (item: LocaleInfo) => item.code)
}
.width('100%')
关键设计点:
- 选中态双重反馈:蓝色背景 + 绿色 ✓ 符号,色弱用户也能辨识;
- 点击即生效 + Toast 确认:
currentLocale是@StorageLink,赋值瞬间全页面所有t()、NumberFormat、DateTimeFormat全部重渲染,Toast 告诉用户"确实切成功了"; - ForEach key 用
item.code:稳定且唯一,语言列表重排时不会触发多余重建。
五、底部导航的本地化
导航 Tab 文案来自文案表,TabItem 是一个接收 key 的复用 Builder:
@Builder
TabItem(key: string, tabId: string, icon: string) {
Column({ space: 2 }) {
Text(icon).fontSize(18)
Text(this.T(key)).fontSize(10)
.fontColor(this.tab === tabId && this.selectedId === '' ? '#3B82F6' : '#94A3B8')
.fontWeight(this.tab === tabId ? FontWeight.Bold : FontWeight.Normal)
}
.layoutWeight(1).justifyContent(FlexAlign.Center)
.onClick(() => {
this.selectedId = '';
this.tab = tabId;
})
}
调用处只需传 key,不传具体文案。注意:图标用 emoji 而非图片资源。emoji 随系统字体渲染,天然适配多语言设备,无需为每种语言准备一套图标,也不需要做 RTL 镜像(emoji 方向性中性)。
六、新闻卡片布局:容器自适应,文本截断
卡片是"内容流动"原则的最佳示范:
ForEach(this.newsOfCat(), (n: NewsItem) => {
Column() {
Row() {
Text(newsTitle(this.currentLocale, n)) // 标题:自适应宽度
.fontSize(16).fontWeight(FontWeight.Bold).fontColor('#0F172A')
.layoutWeight(1).textAlign(TextAlign.Start)
}.width('100%')
Text(newsSummary(this.currentLocale, n)) // 摘要:最多 2 行
.fontSize(13).fontColor('#64748B').margin({ top: 6 })
.maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
.textAlign(TextAlign.Start)
Row({ space: 10 }) { // 元信息行
Text(this.catLabel(n.cat)) // 分类徽标(本地化)
.fontSize(10).fontColor('#3B82F6').padding({ left: 8, right: 8, top: 2, bottom: 2 })
.backgroundColor('#EFF6FF').borderRadius(8)
Text(relativeTime(this.currentLocale, n.timeOffsetMin)) // 相对时间
.fontSize(11).fontColor('#94A3B8')
Blank()
Text(`${this.num(n.readCount)} ${this.T(K.readCount)}`) // 阅读量(本地化数字)
.fontSize(11).fontColor('#94A3B8')
}.width('100%').margin({ top: 8 })
}
.width('94%').padding(14).backgroundColor(Color.White).borderRadius(14)
.onClick(() => { this.selectedId = n.id; })
}, (n: NewsItem) => n.id)
设计要点:
- 卡片宽 94% 居中,不用写死像素宽度——适配手机、平板、折叠屏不同屏宽;
- 标题不设 maxLines(让长标题完整显示),摘要设
maxLines(2) + Ellipsis(控制卡片高度)——分级截断策略:重要信息完整、次要信息省略; - 点击热区覆盖整张卡片,不是只在标题上——移动端最小触控目标 ≥ 44dp,卡片整体可点既符合规范又方便;
- 元信息行用
Blank()弹性撑开:分类在左、阅读量在右,中间留白自适应。
七、RTL 方向的处理
本应用内置了阿拉伯语(ar_AE)。虽然 ArkUI 对阿拉伯文本会自动做字形与方向处理,但整个布局不会自动镜像,因此 UI 层要显式提示:
private isRtl(): boolean {
return this.currentLocale.startsWith('ar');
}
// 构建时:
if (this.isRtl()) {
Text(this.T(K.rtlTip)).fontSize(11).fontColor('#B45309')
.width('94%').padding(8).backgroundColor('#FEF3C7').borderRadius(10).margin({ top: 6 })
.textAlign(TextAlign.Center)
}
这是 i18n UI 设计的重要一课:支持 RTL 语言 ≠ 把文案翻译成阿拉伯语。真实产品还需要镜像布局(导航翻转、文本右对齐、图标左右互换),完整方案见应用 07(RTL 方向适配)。
八、详情页设计:单页分支,语言联动
详情页不跳新路由,而是同一组件内的 selectedId 分支。这是"全局语言"体验的关键——语言在详情页切换时,正文、时间、统计全部即时刷新,无需返回列表:
// 内容区路由逻辑
if (this.selectedId !== '') {
this.DetailView(this.selectedNews()) // 详情
} else if (this.tab === 'home') {
this.HomeView() // 首页列表
} else if (this.tab === 'discover') {
this.DiscoverView() // 发现
} else {
this.MineView() // 我的
}
详情页元信息区展示了 i18n 的全维度联动(发布于时间、阅读量、字数统计全部本地化):
Column({ space: 6 }) {
Row() {
Text(`${this.T(K.published)} `).fontSize(12).fontColor('#94A3B8')
Text(this.fullDate(n.timeOffsetMin)).fontSize(12).fontColor('#64748B')
}.width('100%').justifyContent(FlexAlign.Start)
Row() {
Text(`${this.num(n.readCount)} ${this.T(K.readCount)}`).fontSize(12).fontColor('#94A3B8')
Blank()
Text(this.Tf(K.wordCount, newsBody(this.currentLocale, n).length))
.fontSize(12).fontColor('#94A3B8')
}.width('100%')
}
.width('100%').padding(12).backgroundColor(Color.White).borderRadius(10)
“发布于 2026年8月19日星期三 14:30”(中文)与 “Published on Wednesday, August 19, 2026, 2:30 PM”(英文)——同一时间戳,完全不同的呈现,全部由 intl.DateTimeFormat 完成。
九、排版验证清单(多语言下自查)
| 检查项 | 方法 | 验证结果 |
|---|---|---|
| 长文案不溢出 | 摘要 maxLines(2) + Ellipsis,卡片宽 94% 自适应 |
德语标题在 360dp 屏完整显示 |
| 数字随语言变化 | 阅读量走 num()(NumberFormat) |
23145 → 23,145 / 23.145 / ٢٣٬١٤٥ |
| 时间随语言变化 | 列表走 relativeTime(),详情走 fullDate() |
8 分钟前 / 8 min ago / vor 8 Min. |
| 空态 | ForEach 空数组自然不渲染 + noNews 文案 | 分类无内容时显示"该分类暂无内容" |
| 语言持久化 | 重启应用后保持上次语言 | PersistentStorage.persistProp 生效 |
十、可访问性(多语言 UI 加分项)
- 语言名称用母语显示(简体中文显示"简体中文"、English 显示"English"),而不是翻译成当前界面语言——用户用母语识别自己的语言最可靠;
- 国旗 emoji 仅作辅助装饰,不依赖它传递语言信息(色弱/黑白显示下靠文字区分);
- 所有
Text均显式设置fontColor,避免系统深色模式下默认色与背景色冲突; - 选中态同时使用颜色 + 符号(✓)双重编码,符合 WCAG 不单靠颜色传达信息的原则。
十一、小结
UI 层的全部工作可以概括为一句话:布局是固定的,内容是流动的。无论切到德语、日语还是阿拉伯语,界面都不会破相——这是多语言产品 UI 设计与普通 UI 设计的根本区别。
本文给出的 5 个可复用模式(语言面板 FlexWrap、TabItem 传 key、卡片自适应 + 分级截断、RTL 提示、单页分支联动)可以直接迁移到任何需要多语言支持的业务页面。下一篇文章将深入技术实现,讲解文案表三级降级、intl 本地化与 PersistentStorage 持久化的完整链路。
更多推荐




所有评论(0)