在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、设计目标

多语言新闻阅读器既要"好看",更要"经得起语言切换的考验"。UI 设计有两个硬约束:

  1. 任何文案长度在不同语言下差异巨大:德语单词长、日韩文信息密度高,固定宽度容器会被撑破。实测同一句"已发布":中文 3 字、英文 “Published on” 12 字符、德语 “Veröffentlicht am” 18 字符、阿拉伯语 “نُشر في” 7 字符——最长的德语比中文多出 6 倍宽度;
  2. 数字/时间/日期要随语言变化而变:不能把格式化结果当静态文本写死在布局里。阅读量 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()NumberFormatDateTimeFormat 全部重渲染,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)

设计要点:

  1. 卡片宽 94% 居中,不用写死像素宽度——适配手机、平板、折叠屏不同屏宽;
  2. 标题不设 maxLines(让长标题完整显示),摘要设 maxLines(2) + Ellipsis(控制卡片高度)——分级截断策略:重要信息完整、次要信息省略;
  3. 点击热区覆盖整张卡片,不是只在标题上——移动端最小触控目标 ≥ 44dp,卡片整体可点既符合规范又方便;
  4. 元信息行用 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 持久化的完整链路。

Logo

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

更多推荐