在这里插入图片描述

📖 引言

经过上一篇的多端适配,我们的「民族图鉴」已经能在手机、平板、折叠屏等多种设备上流畅运行了。但要让更多人用得上、用得好,还有一项重要工作要做——国际化

你可能会问:

  • 「民族图鉴」是介绍中国民族文化的,主要用户是中国人,需要国际化吗?
  • 国际化不就是翻译一下文字吗?有那么难吗?
  • 除了语言,还有什么需要国际化的?
  • 日期、时间、数字、货币,这些怎么适配不同地区?
  • 阿拉伯语、希伯来语这种从右往左写的语言,怎么适配?
  • 翻译质量怎么保证?怎么管理多语言资源?

这些问题都很好。很多开发者对国际化的理解还停留在"把文字翻译成英文"的层面。但实际上,国际化是一个系统工程——它涉及语言文字、日期时间、数字货币、布局方向、文化习俗等方方面面。一个细节没做好,就可能让海外用户觉得"这个应用不是为我做的"。

本文将从为什么需要国际化讲起,系统讲解国际化的完整知识体系,并以「民族图鉴」英文版实现为实战案例,带你从零完成多语言适配。读完本文,你将掌握国际化的核心方法论。


🎯 学习目标

完成本文后,你将能够:

  • ✅ 理解国际化的重要性与完整范围
  • ✅ 掌握字符串资源、图片资源、布局资源的国际化管理
  • ✅ 学会翻译管理流程与质量保证方法
  • ✅ 掌握日期时间国际化:格式、时区、相对时间
  • ✅ 理解数字与货币格式化的要点
  • ✅ 掌握 RTL(从右到左)布局适配方法
  • ✅ 实战实现「民族图鉴」英文版完整适配
  • ✅ 了解国际化的常见问题与解决方案

💡 需求分析

为什么需要国际化?

在动手之前,我们先想清楚:「民族图鉴」是介绍中国56个民族的应用,主要用户应该是中国人吧?为什么还要做国际化?

1. 出海是必然趋势

中国的应用出海,已经不是"选做题",而是"必答题"了:

  • 文化输出:民族文化是中华文化的瑰宝,值得让全世界了解
  • 用户增长:海外市场有更多的用户,更多的机会
  • 品牌价值:一个支持多语言的应用,品牌形象更国际化
  • 政策支持:国家鼓励文化出海,讲好中国故事

「民族图鉴」作为一个介绍中国民族文化的应用,天然具有出海的潜力。外国人对中国的56个民族、丰富多彩的民族文化,同样充满好奇。

2. 国内也有多语言需求

不要以为只有出海才需要国际化。就算只做国内市场,也有多语言需求:

  • 少数民族用户:藏族、维吾尔族、蒙古族等少数民族,可能更习惯用本民族语言
  • 港澳台用户:繁体中文用户
  • 在华外国人:在中国生活、工作、学习的外国人
  • 学习中文的外国人:对中国文化感兴趣,想学中文的外国人
3. 国际化要趁早

很多人觉得"先做好中文版,以后再加英文"。但经验告诉我们:国际化越晚做,成本越高

早期做国际化:写代码的时候就考虑,成本增加 10-20%
后期做国际化:代码写完了再改,成本增加 50-100%
还可能改出 bug,影响已有功能

为什么后期改贵?因为:

  • 硬编码的字符串散落在各处,要一个个找出来
  • 有些写法天生不支持国际化,要重写
  • 布局是按中文长度设计的,英文更长就溢出了
  • 没有统一的规范,每个人写的都不一样

💡 最佳实践:国际化从项目第一天就开始做。哪怕暂时只支持中文,也要按国际化的方式来写。这样以后加语言,成本就很低了。


国际化的范围

国际化(Internationalization,简称 i18n,因为 i 和 n 之间有 18 个字母)不只是翻译文字。它的范围很广:

国际化(i18n)全范围
    │
    ├─ 语言文字
    │   ├── 界面文本翻译
    │   ├── 图片中的文字
    │   ├── 字体(不同语言用不同字体)
    │   ├── 排版规则(换行、空格、标点)
    │   └── 书写方向(LTR / RTL)
    │
    ├─ 日期时间
    │   ├── 日期格式(YYYY-MM-DD / DD/MM/YYYY / MM/DD/YYYY)
    │   ├── 时间格式(24小时制 / 12小时制)
    │   ├── 时区处理(UTC / 本地时间)
    │   ├── 星期起始日(周日 / 周一)
    │   └── 相对时间("刚刚"、"5分钟前")
    │
    ├─ 数字与货币
    │   ├── 千分位分隔符(, / . / 空格)
    │   ├── 小数点符号(. / ,)
    │   ├── 货币符号(¥ / $ / €)
    │   ├── 货币符号位置(前缀 / 后缀)
    │   └── 数字格式(阿拉伯数字 / 其他数字系统)
    │
    ├─ 布局与设计
    │   ├── 文字长度变化(德文、俄文往往更长)
    │   ├── RTL 布局(阿拉伯语、希伯来语)
    │   ├── 图片和图标(有些文化有禁忌)
    │   └── 颜色含义(不同文化颜色含义不同)
    │
    └─ 文化与合规
        ├── 文化习俗(节日、礼仪、禁忌)
        ├── 法律法规(隐私、数据、内容审查)
        └── 内容适配(不同地区内容可能有差异)

看到了吧?国际化的范围比你想的广多了。翻译文字只是最基础的一步。

💡 L10n vs i18n:经常有人把这两个搞混。

  • i18n(Internationalization):国际化,指让应用具备支持多语言多区域的能力(技术工作)
  • L10n(Localization):本地化,指针对某个具体地区做适配(翻译、内容调整等)
    简单说:i18n 是打基础,L10n 是具体落地。

「民族图鉴」国际化方案

了解了范围,我们来制定「民族图鉴」的国际化方案。不是一上来就支持几十种语言,而是有规划、有步骤地来。

语言规划
第一阶段(当前):
    ├── 中文简体(zh-CN):默认语言,最完善
    └── 英文(en):面向海外用户和英语学习者
    
第二阶段(v2.0):
    ├── 中文繁体(zh-TW / zh-HK):港澳台用户
    ├── 日文(ja):日本市场
    └── 韩文(ko):韩国市场
    
第三阶段(v3.0):
    ├── 西班牙文(es)
    ├── 法文(fr)
    ├── 德文(de)
    └── 更多...

为什么先做英文?因为:

  • 英文是国际通用语,覆盖最广
  • 很多非英语国家的人也能看懂英文
  • 英文翻译资源最丰富,机器翻译质量也最好
  • 先把英文做好,再扩展其他语言成本低

区域规划

区域(Locale)和语言不完全是一回事。同一种语言,在不同地区可能有差异:

中文:
    ├── zh-CN:中国大陆(简体)
    ├── zh-TW:中国台湾(繁体)
    ├── zh-HK:中国香港(繁体)
    └── zh-SG:新加坡(简体)
    
英文:
    ├── en-US:美国英语
    ├── en-GB:英国英语
    ├── en-CA:加拿大英语
    └── en-AU:澳大利亚英语

「民族图鉴」的策略:

  • 第一阶段:语言级别区分(中文 / 英文),不区分地区
  • 第二阶段:再细分地区(比如简体繁体分开,美式英式分开)
  • 原则:先粗后细,逐步完善

功能范围

不是所有功能都要第一时间国际化。我们按优先级排个序:

P0(必须有):
    ├── 所有界面文本
    ├── 按钮、菜单、标签
    ├── 错误提示、加载状态
    └── 设置中的语言切换
    
P1(应该有):
    ├── 民族名称的翻译
    ├── 民族介绍的翻译
    └── 日期时间格式化
    
P2(可以后面加):
    ├── 图片中的文字
    ├── TTS 语音播报的多语言
    ├── 音乐歌词的翻译
    ├── AI 对话的多语言
    └── RTL 布局适配

为什么民族介绍是 P1 而不是 P0?因为民族介绍的翻译量很大(56个民族,每个几千字),翻译成本高、周期长。可以先把界面翻译了,内容翻译逐步来。

💡 国际化是个渐进的过程。不要想着"一次做完美"。先把框架搭好,把最核心的翻译了,再逐步完善。框架搭好了,加语言只是加翻译资源的事。


🛠️ 核心实现

步骤1:资源管理——字符串资源国际化

国际化的基础是资源管理。不要把字符串硬编码在代码里,要放到资源文件里,不同语言有不同的资源文件。

1.1 鸿蒙的多语言资源体系

鸿蒙的资源管理是基于限定词的。不同限定词的目录下放不同语言的资源,系统会自动选择合适的:

resources/
├── base/              # 默认资源(找不到匹配的就用这个)
│   ├── element/
│   │   └── string.json
│   ├── media/
│   └── ...
│
├── en/                # 英文资源
│   └── element/
│       └── string.json
│
├── zh_CN/             # 简体中文资源
│   └── element/
│       └── string.json
│
├── zh_TW/             # 繁体中文资源
│   └── element/
│       └── string.json
│
├── ar/                # 阿拉伯语资源
│   └── element/
│       └── string.json
│
└── ...

💡 资源匹配机制:系统会根据当前语言设置,从最匹配的目录找资源。找不到就 fallback 到上一级,最后到 base。所以 base 里一定要有默认资源。


1.2 string.json 的格式

string.json 的格式很简单,就是一个 JSON 数组,每个元素是一个字符串资源:

{
  "string": [
    {
      "name": "app_name",
      "value": "民族图鉴"
    },
    {
      "name": "tab_home",
      "value": "首页"
    },
    {
      "name": "tab_encyclopedia",
      "value": "百科"
    },
    {
      "name": "search_placeholder",
      "value": "搜索民族、节日、美食..."
    }
  ]
}

每个字符串有两个字段:

  • name:资源名,代码里用这个引用
  • value:资源值,实际显示的文字

1.3 代码中引用字符串资源

在 ArkTS 中,用 $r('app.string.xxx') 来引用字符串资源:

// 方式1:直接在组件中用
Text($r('app.string.app_name'))
  .fontSize(20)

// 方式2:在代码逻辑中用
import { StringResource } from '@kit.ArkUI';

// 获取字符串的值
const appName = getContext(this).resourceManager.getStringSync(
  $r('app.string.app_name')
);

⚠️ 注意$r() 是一个资源引用对象,不是字符串本身。要拿到字符串的值,需要用 ResourceManager 的 getString 方法。


1.4 带参数的字符串

有时候字符串里需要插入变量,比如"你好,张三"、“共 56 个民族”。这时候要用格式化字符串

// string.json
{
  "string": [
    {
      "name": "greeting",
      "value": "你好,%s!"
    },
    {
      "name": "ethnic_count",
      "value": "共 %d 个民族"
    },
    {
      "name": "pop_rank",
      "value": "人口排名:第 %1$d 位(共 %2$d 个)"
    }
  ]
}

格式化符号:

  • %s:字符串
  • %d:整数
  • %f:浮点数
  • %1$s:第 1 个参数,字符串类型(指定位置)

代码中使用:

// 方式1:在组件中用
Text($r('app.string.greeting', '张三'))
// 显示:你好,张三!

Text($r('app.string.ethnic_count', 56))
// 显示:共 56 个民族

// 方式2:在代码中格式化
const str = getContext(this).resourceManager.getStringSync(
  $r('app.string.pop_rank'),
  3,   // 第 1 个参数:第 3 位
  56   // 第 2 个参数:共 56 个
);
// 显示:人口排名:第 3 位(共 56 个)

💡 为什么要用 %1$s 这种形式? 因为不同语言的语序可能不一样。比如中文说"我吃苹果",英文说 “I eat apples”,语序一样。但有些语言的语序差异很大,参数顺序可能不同。用位置参数,翻译的时候可以调整顺序。


1.5 「民族图鉴」的字符串资源规范

为了让资源文件好维护,我们定一些规范:

命名规范

页面_模块_描述
比如:
  - home_search_placeholder:首页搜索框占位符
  - detail_favorite_add:详情页添加收藏
  - settings_theme_title:设置页主题标题

分组规范

  • 按页面分组,同一个页面的字符串放在一起
  • 通用的字符串(确认、取消、加载中…)放最前面
  • 按字母顺序或功能顺序排列

注释规范

  • 有歧义的字符串加注释,告诉翻译者上下文
  • 比如 “bank” 是"银行"还是"河岸"?要有注释

步骤2:资源管理——图片与布局国际化

不只是文字,图片和布局有时候也需要国际化。

2.1 图片资源国际化

如果图片里有文字,或者图片内容有文化差异,就需要多语言版本:

resources/
├── base/
│   └── media/
│       ├── logo.png
│       ├── banner.png
│       └── ...
│
├── en/
│   └── media/
│       ├── banner.png      # 英文版的 banner(图中文字是英文)
│       └── ...
│
└── ar/
    └── media/
        ├── banner.png      # 阿拉伯语版的 banner(文字是阿拉伯语,从右往左)
        └── ...

什么时候需要多语言图片?

  • 图片中包含文字(比如 banner、引导页)
  • 图片内容有文化差异(比如手势、符号、颜色)
  • 图片上的 UI 元素(比如带文字的按钮图)

什么时候不需要?

  • 纯照片、插图(没有文字,文化中性)
  • 图标(通用图标,没有文字)

「民族图鉴」的情况:

  • 民族服饰照片:不需要翻译,文化中性
  • Logo、banner 图:如果有文字,需要翻译
  • 图标:大部分不需要,少数可能需要调整

💡 最佳实践:尽量让图片不带文字,用文字叠加在图片上的方式。这样就不用为每种语言做一套图了。


2.2 布局资源国际化(RTL)

有些语言是从右往左写的,比如阿拉伯语、希伯来语、波斯语。这时候整个界面都要镜像翻转:

LTR(从左到右)布局:      RTL(从右到左)布局:
┌───────────────────┐    ┌───────────────────┐
│ ← 返回    标题    + │    │ +    标题    返回 → │
├───────────────────┤    ├───────────────────┤
│ 图标  文字  >     │    │     <  文字  图标  │
├───────────────────┤    ├───────────────────┤
│ 内容...           │    │           ...内容 │
│                   │    │                   │
└───────────────────┘    └───────────────────┘

好在鸿蒙的 ArkUI 对 RTL 有很好的支持。大部分情况下,你只要用对了属性,系统会自动适配:

会自动镜像的属性

  • marginLeft / marginRight → 自动换成 marginStart / marginEnd
  • paddingLeft / paddingRight → 自动换成 paddingStart / paddingEnd
  • left / right(定位)→ 自动换成 start / end
  • TextAlign.Start / TextAlign.End → 自动调整

建议的写法

// ❌ 不好:用 left/right,RTL 时不会自动翻转
Text('内容')
  .padding({ left: 16, right: 16 })
  .textAlign(TextAlign.Left)

// ✅ 好:用 start/end,RTL 时自动翻转
Text('内容')
  .padding({ left: 16, right: 16 }) // 其实 left/right 在 ArkUI 中也会自动适配
  .textAlign(TextAlign.Start)       // 用 Start 更语义化

RTL 适配的关键点

  1. 用 Start/End 代替 Left/Right

    • 语义更清晰:start 是"阅读开始的方向",end 是"结束的方向"
    • LTR 时 start=left, end=right
    • RTL 时 start=right, end=left
  2. 水平布局方向自动翻转

    • Row 布局的方向会自动翻转
    • 第一个子组件在 LTR 时在左边,RTL 时在右边
  3. 图片和图标需要注意

    • 有方向的箭头、指示图标,需要 RTL 版
    • 可以用 rotateY(180deg) 翻转,或者准备两套图
// RTL 时自动翻转图标
Image(arrowIcon)
  .rotate({
    angle: isRTL() ? 180 : 0,
    centerX: '50%',
    centerY: '50%'
  })
  1. 布局方向检测
// 检测当前是否是 RTL 布局
import { I18nUtil } from '@kit.ArkUI';

function isRTL(): boolean {
  const locale = I18nUtil.getSystemLocale();
  // 阿拉伯语、希伯来语、波斯语等是 RTL
  const rtlLangs = ['ar', 'he', 'fa', 'ur'];
  return rtlLangs.includes(locale.language);
}

💡 RTL 适配建议:如果你的应用暂时不需要支持 RTL 语言,可以先不考虑。但写代码的时候养成用 Start/End 的习惯,以后要支持 RTL 就容易多了。


步骤3:「民族图鉴」国际化服务实现

前面我们已经有了一个基础的 I18nService,现在来完善它,让它支持完整的国际化功能。

3.1 语言枚举与存储

首先,扩展语言枚举,支持更多语言选项:

// models/EnumModels.ets

export enum AppLanguage {
  ZH_CN = 'zh_CN',   // 简体中文
  ZH_TW = 'zh_TW',   // 繁体中文
  EN = 'en',         // 英文
  JA = 'ja',         // 日文
  KO = 'ko'          // 韩文
}

// 语言显示名称映射
export const LanguageDisplayNames: Record<AppLanguage, { native: string; english: string }> = {
  [AppLanguage.ZH_CN]: { native: '简体中文', english: 'Chinese (Simplified)' },
  [AppLanguage.ZH_TW]: { native: '繁體中文', english: 'Chinese (Traditional)' },
  [AppLanguage.EN]: { native: 'English', english: 'English' },
  [AppLanguage.JA]: { native: '日本語', english: 'Japanese' },
  [AppLanguage.KO]: { native: '한국어', english: 'Korean' }
};

3.2 I18nService 完善

然后完善 I18nService,增加更多功能:

// services/I18nService.ets

import { AppLanguage, LanguageDisplayNames } from '../models/EnumModels';
import { StorageService } from './StorageService';
import { StorageConstants } from '../common/constants/StorageConstants';

export class I18nService {
  private static instance: I18nService;
  private currentLanguage: AppLanguage = AppLanguage.ZH_CN;
  private listeners: ((lang: AppLanguage) => void)[] = [];

  private constructor() {}

  public static getInstance(): I18nService {
    if (!I18nService.instance) {
      I18nService.instance = new I18nService();
    }
    return I18nService.instance;
  }

  // 初始化:从存储加载设置,没有就用系统语言
  public async init(): Promise<void> {
    const savedLang = await StorageService.getInstance().getString(
      StorageConstants.KEY_APP_LANGUAGE,
      ''  // 默认空,表示跟随系统
    );

    if (savedLang) {
      // 用户手动设置过语言
      const lang = this.validateLanguage(savedLang);
      this.currentLanguage = lang;
    } else {
      // 跟随系统语言
      this.currentLanguage = this.getSystemLanguage();
    }

    AppStorage.setOrCreate<AppLanguage>('currentAppLanguage', this.currentLanguage);
    this.notifyListeners();
  }

  // 校验语言是否合法,防止脏数据
  private validateLanguage(lang: string): AppLanguage {
    const validLangs: AppLanguage[] = Object.values(AppLanguage);
    if (validLangs.includes(lang as AppLanguage)) {
      return lang as AppLanguage;
    }
    // 不合法就返回默认
    return AppLanguage.ZH_CN;
  }

  // 获取系统语言
  private getSystemLanguage(): AppLanguage {
    try {
      // 获取系统首选语言列表
      // 实际项目中用 I18nUtil.getSystemLanguages()
      // 这里简化处理
      return AppLanguage.ZH_CN;
    } catch (e) {
      return AppLanguage.ZH_CN;
    }
  }

  // 设置语言
  public async setLanguage(lang: AppLanguage): Promise<void> {
    if (this.currentLanguage === lang) return;
    
    this.currentLanguage = lang;
    AppStorage.setOrCreate<AppLanguage>('currentAppLanguage', lang);
    
    // 持久化存储
    await StorageService.getInstance().saveString(
      StorageConstants.KEY_APP_LANGUAGE,
      lang
    );
    
    // 通知监听者
    this.notifyListeners();
  }

  // 获取当前语言
  public getLanguage(): AppLanguage {
    return this.currentLanguage;
  }

  // 是否是中文
  public isChinese(): boolean {
    return this.currentLanguage === AppLanguage.ZH_CN 
        || this.currentLanguage === AppLanguage.ZH_TW;
  }

  // 是否是 RTL 语言
  public isRTL(): boolean {
    const rtlLangs: string[] = ['ar', 'he', 'fa', 'ur'];
    return rtlLangs.includes(this.currentLanguage);
  }

  // 获取语言显示名称(母语显示)
  public getLanguageDisplayName(lang: AppLanguage): string {
    return LanguageDisplayNames[lang]?.native || lang;
  }

  // 获取所有支持的语言列表
  public getSupportedLanguages(): AppLanguage[] {
    // 第一阶段先支持简中和英文
    return [AppLanguage.ZH_CN, AppLanguage.EN];
  }

  // ===== 双语内容辅助方法 =====
  // 这些方法用于从"平行字段"的数据结构中取对应语言的内容
  // 比如民族数据中有 nameZh / nameEn 两个字段

  // 根据语言取对应文本
  public getText<T extends { zh: string; en: string }>(data: T): string {
    return this.isChinese() ? data.zh : data.en;
  }

  // 获取民族名称(双语数据结构)
  public getEthnicName(zhName: string, enName: string): string {
    return this.isChinese() ? zhName : enName;
  }

  // 获取民族介绍(双语数据结构)
  public getEthnicDescription(zhDesc: string, enDesc: string): string {
    return this.isChinese() ? zhDesc : enDesc;
  }

  // ===== 监听 =====
  public addListener(listener: (lang: AppLanguage) => void): void {
    this.listeners.push(listener);
  }

  public removeListener(listener: (lang: AppLanguage) => void): void {
    const index = this.listeners.indexOf(listener);
    if (index > -1) {
      this.listeners.splice(index, 1);
    }
  }

  private notifyListeners(): void {
    this.listeners.forEach(listener => listener(this.currentLanguage));
  }
}

3.3 平行字段设计

「民族图鉴」的核心数据是民族信息,这些内容需要支持多语言。我们采用平行字段的设计:

// models/EthnicModels.ets

export interface EthnicGroup {
  id: string;
  pinyin: string;           // 拼音,搜索用,不需要翻译
  population: number;       // 人口数,数字,不需要翻译
  popRank: number;          // 人口排名,数字,不需要翻译
  mainRegion: string;       // 主要分布地区(可能需要翻译)
  
  // ===== 平行字段:每种语言一个字段 =====
  name: string;             // 名称(多语言:nameZh / nameEn?)
  // ... 更多字段
}

等等,这里有个设计决策:是每种语言一个字段,还是用对象结构?

// 方案1:平行字段
interface EthnicGroup {
  nameZh: string;
  nameEn: string;
  descZh: string;
  descEn: string;
  // ... 每个需要翻译的字段都有 zh/en 两个版本
}

// 方案2:嵌套对象
interface EthnicGroup {
  name: {
    zh: string;
    en: string;
  };
  desc: {
    zh: string;
    en: string;
  };
  // ... 每个需要翻译的字段都是一个多语言对象
}

两种方案各有优劣:

方案 优点 缺点
平行字段 结构扁平,访问简单 字段多,有点乱
嵌套对象 结构清晰,每个字段的多语言是一组 访问要多一层

「民族图鉴」选择的是平行字段方案,因为:

  • 访问简单,直接 ethnic.nameZh 就行
  • 列表展示时经常只要一种语言,拿起来就用
  • 增加语言的时候只要加字段,不用改结构

💡 数据设计的原则:国际化不只是界面的事,数据层也要支持。从一开始设计数据模型的时候,就要考虑多语言的问题。不要等中文数据都写好了,再加英文——那时候改数据结构成本很高。


步骤4:日期时间国际化

日期时间是国际化的重灾区。不同地区的人,日期格式、时间格式、星期起始日,都不一样。

4.1 日期格式差异
中国大陆:2026-06-29  或  2026年6月29日  (年-月-日)
美国:06/29/2026  或  June 29, 2026    (月/日/年)
英国:29/06/2026  或  29 June 2026     (日/月/年)
日本:2026/06/29  或  令和8年6月29日    (年/月/日)

看到了吧,光是日期格式就有好几种,搞反了月和日就全错了。


4.2 鸿蒙的日期格式化

鸿蒙提供了 @kit.IntlKit 来做国际化的日期时间格式化:

import { dateTimeFormat } from '@kit.IntlKit';

// 创建格式化器
const formatter = new dateTimeFormat.DateTimeFormat(
  'zh-CN',  // locale
  {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    weekday: 'long'
  }
);

// 格式化日期
const date = new Date(2026, 5, 29); // 注意:月份从 0 开始
const formatted = formatter.format(date);
// 中文:2026年6月29日 星期一
// 英文(en-US):Monday, June 29, 2026
// 英文(en-GB):Monday, 29 June 2026

常用的选项:

  • year: ‘numeric’(2026)/ ‘2-digit’(26)
  • month: ‘numeric’(6)/ ‘2-digit’(06)/ ‘long’(六月/June)/ ‘short’(6月/Jun)
  • day: ‘numeric’(29)/ ‘2-digit’(29)
  • weekday: ‘long’(星期一/Monday)/ ‘short’(周一/Mon)
  • hour: ‘numeric’ / ‘2-digit’
  • minute: ‘numeric’ / ‘2-digit’
  • second: ‘numeric’ / ‘2-digit’

4.3 「民族图鉴」的日期格式化工具

我们封装一个工具函数,方便使用:

// common/utils/DateUtils.ets

import { dateTimeFormat } from '@kit.IntlKit';
import { I18nService } from '../services/I18nService';
import { AppLanguage } from '../models/EnumModels';

export class DateUtils {
  // 获取当前语言的 locale 字符串
  private static getLocale(): string {
    const lang = I18nService.getInstance().getLanguage();
    switch (lang) {
      case AppLanguage.ZH_CN: return 'zh-CN';
      case AppLanguage.ZH_TW: return 'zh-TW';
      case AppLanguage.EN: return 'en-US';
      case AppLanguage.JA: return 'ja-JP';
      case AppLanguage.KO: return 'ko-KR';
      default: return 'zh-CN';
    }
  }

  // 格式化日期(年月日)
  static formatDate(date: Date): string {
    const formatter = new dateTimeFormat.DateTimeFormat(
      this.getLocale(),
      {
        year: 'numeric',
        month: 'long',
        day: 'numeric'
      }
    );
    return formatter.format(date);
  }

  // 格式化日期时间(年月日 时分)
  static formatDateTime(date: Date): string {
    const formatter = new dateTimeFormat.DateTimeFormat(
      this.getLocale(),
      {
        year: 'numeric',
        month: 'short',
        day: 'numeric',
        hour: '2-digit',
        minute: '2-digit'
      }
    );
    return formatter.format(date);
  }

  // 格式化时间(时分)
  static formatTime(date: Date): string {
    const formatter = new dateTimeFormat.DateTimeFormat(
      this.getLocale(),
      {
        hour: '2-digit',
        minute: '2-digit'
      }
    );
    return formatter.format(date);
  }
}

4.4 相对时间格式化

“刚刚”、“5分钟前”、“昨天”、“3天前”——这些相对时间也需要国际化。

鸿蒙也提供了相对时间格式化:

import { RelativeTimeFormat } from '@kit.IntlKit';

const rtf = new RelativeTimeFormat('zh-CN', { numeric: 'auto' });

rtf.format(-5, 'minute');  // 5分钟前
rtf.format(0, 'minute');   // 刚刚 (因为 numeric: 'auto')
rtf.format(-1, 'day');     // 昨天
rtf.format(-3, 'day');     // 3天前
rtf.format(2, 'week');     // 2周后

// 英文版本
const rtfEn = new RelativeTimeFormat('en-US', { numeric: 'auto' });
rtfEn.format(-5, 'minute');  // 5 minutes ago
rtfEn.format(0, 'minute');   // now
rtfEn.format(-1, 'day');     // yesterday

💡 相对时间的坑:相对时间看起来简单,其实有很多细节。比如"昨天"、“上周”、“上个月”,这些不同语言的表达方式差异很大。不要自己拼字符串,一定要用系统的格式化 API。


4.5 时区处理

如果你的应用有跨时区的需求(比如用户在不同时区,显示时间要对应当地时间),那就要注意时区问题。

基本原则

  • 存储和传输用 UTC:数据库存 UTC 时间,网络传输也用 UTC
  • 显示用本地时间:展示给用户的时候,转成本地时间
// 后端返回的时间是 ISO 格式(UTC)
const utcTimeStr = '2026-06-29T12:00:00Z';

// 转成 Date 对象,自动根据系统时区显示
const date = new Date(utcTimeStr);

// 格式化显示(自动转成本地时间)
const localTimeStr = DateUtils.formatDateTime(date);
// 北京时区(UTC+8)显示:2026年6月29日 20:00
// 纽约时区(UTC-4)显示:June 29, 2026, 08:00

「民族图鉴」的时间场景不多,主要是收藏时间、浏览历史记录。用本地时间显示就可以了,不需要太多特殊处理。


步骤5:数字与货币格式化

数字和货币的格式也有地区差异。

5.1 数字格式差异
中文/英文:1,234,567.89  (千分位用逗号,小数点用点)
德文:1.234.567,89      (千分位用点,小数点用逗号)
法文:1 234 567,89      (千分位用空格,小数点用逗号)
印度:12,34,567.89      (不是每三位一分,前三位后每两位一分)

看到了吧,千分位分隔符、小数点符号,不同地区都不一样。


5.2 数字格式化

用系统的 NumberFormat:

import { NumberFormat } from '@kit.IntlKit';

// 格式化数字
const formatter = new NumberFormat('zh-CN');
formatter.format(1234567.89);
// 中文:1,234,567.89

const formatterDE = new NumberFormat('de-DE');
formatterDE.format(1234567.89);
// 德文:1.234.567,89

5.3 货币格式化

货币更复杂,因为不同国家有不同的货币,符号位置也不一样:

人民币:¥1,234.56  或  1,234.56元
美元:$1,234.56
欧元:1.234,56 €  (欧洲很多国家符号在后面)
英镑:£1,234.56
日元:¥1,234  (没有小数位)

货币格式化:

import { NumberFormat } from '@kit.IntlKit';

// 人民币
const cnyFormatter = new NumberFormat('zh-CN', {
  style: 'currency',
  currency: 'CNY'
});
cnyFormatter.format(1234.56);  // ¥1,234.56

// 美元
const usdFormatter = new NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
});
usdFormatter.format(1234.56);  // $1,234.56

// 欧元
const eurFormatter = new NumberFormat('de-DE', {
  style: 'currency',
  currency: 'EUR'
});
eurFormatter.format(1234.56);  // 1.234,56 €

💡 「民族图鉴」的情况:我们的应用暂时没有支付功能,所以不需要货币格式化。但数字格式化还是需要的——比如人口数"12亿"、“1,200万”,不同语言的表达不一样。不过第一阶段我们先简单处理,以后再精细化。


步骤6:翻译管理流程

翻译不只是"找个人把中文翻成英文",它是一个完整的流程。

6.1 翻译工作流

一个规范的翻译流程应该是这样的:

提取 → 翻译 → 校验 → 合入 → 测试
  │       │       │       │       │
  │       │       │       │       └── 在 App 里实际看效果
  │       │       │       │
  │       │       │       └── 代码合入,打包
  │       │       │
  │       │       └── 检查翻译质量
  │       │           · 术语统一
  │       │           · 上下文正确
  │       │           · 没有遗漏
  │       │
  │       └── 翻译人员翻译
  │           · 人工翻译
  │           · 或机器翻译 + 人工校对
  │
  └── 从代码中提取所有需要翻译的字符串
      生成翻译资源文件

6.2 翻译质量保证

翻译质量很重要。翻译得不好,用户会觉得这个应用很业余。

常见的翻译问题

  1. 术语不统一

    • 比如"民族"有时候翻成 ethnic group,有时候翻成 nationality,有时候翻成 minority
    • 用户看了会困惑
  2. 上下文缺失

    • 翻译者不知道这个词用在什么地方
    • 比如 “bank” 可能是"银行"也可能是"河岸"
    • 短词特别容易出问题
  3. 机器翻译痕迹重

    • 读起来很生硬,不像地道的表达
    • 语法错误、用词不当
  4. 文化差异

    • 有些说法在另一种文化里不合适
    • 有些比喻、梗翻译过去人家不懂

怎么保证质量?

  1. 建立术语表

    • 把核心术语列出来,规定统一译法
    • 比如:
      • 民族 → ethnic group
      • 民族图鉴 → Ethnic Chronicles
      • 收藏 → Favorite
      • 百科 → Encyclopedia
  2. 提供上下文

    • 给翻译者提供截图或说明
    • 告诉他们这个字符串用在什么地方
    • 是按钮?是标题?是正文?
  3. 机器翻译 + 人工校对

    • 先用机器翻译出初稿(便宜、快)
    • 再找母语者校对质量(保证地道)
    • 性价比最高
  4. 在 App 里实际预览

    • 翻译完了,在 App 里跑一遍
    • 看看文字有没有溢出、对齐有没有问题
    • 有些问题只有在实际界面里才能发现

💡 翻译质量的重要性:翻译质量直接影响用户对产品的感知。一个翻译很烂的应用,用户会默认它其他方面也很烂。宁愿少支持几种语言,也要把已有的语言做好。


6.3 「民族图鉴」的翻译策略

「民族图鉴」的翻译分两类,策略不同:

界面文本(P0)

  • 量不大(几百条)
  • 更新频繁(每次迭代可能加新功能)
  • 策略:机器翻译 + 人工校对,开发人员自己就能搞定

内容文本(P1)

  • 量大(56个民族,每个几千字,几十万字总量)
  • 更新不频繁(民族介绍相对稳定)
  • 策略:找专业翻译做,或者分阶段做(先翻译几个主要民族的)

步骤7:实战——「民族图鉴」英文版完整实现

理论讲了这么多,终于到实战了。我们来一步步把「民族图鉴」改成支持中英文切换。

7.1 准备英文 string.json

首先,我们需要一个英文版的字符串资源。项目里已经有了,我们来完善它:

// resources/en/element/string.json

{
  "string": [
    {
      "name": "app_name",
      "value": "Ethnic Chronicles"
    },
    {
      "name": "module_desc",
      "value": "Explore the diverse cultures of 56 ethnic groups in China"
    },
    {
      "name": "tab_home",
      "value": "Home"
    },
    {
      "name": "tab_encyclopedia",
      "value": "Encyclopedia"
    },
    {
      "name": "tab_map",
      "value": "Map"
    },
    {
      "name": "tab_quiz",
      "value": "Quiz"
    },
    {
      "name": "tab_profile",
      "value": "Profile"
    },
    {
      "name": "search_placeholder",
      "value": "Search ethnic groups, festivals, cuisines..."
    },
    {
      "name": "loading",
      "value": "Loading..."
    },
    {
      "name": "no_data",
      "value": "No data available"
    },
    {
      "name": "retry",
      "value": "Retry"
    },
    {
      "name": "cancel",
      "value": "Cancel"
    },
    {
      "name": "confirm",
      "value": "Confirm"
    },
    {
      "name": "save",
      "value": "Save"
    },
    {
      "name": "share",
      "value": "Share"
    },
    {
      "name": "back",
      "value": "Back"
    },
    {
      "name": "settings_title",
      "value": "Settings"
    },
    {
      "name": "settings_theme",
      "value": "Theme"
    },
    {
      "name": "settings_language",
      "value": "Language"
    },
    {
      "name": "settings_font_size",
      "value": "Font Size"
    },
    {
      "name": "theme_light",
      "value": "Light"
    },
    {
      "name": "theme_dark",
      "value": "Dark"
    },
    {
      "name": "theme_system",
      "value": "System Default"
    },
    {
      "name": "lang_zh_cn",
      "value": "简体中文"
    },
    {
      "name": "lang_en",
      "value": "English"
    },
    {
      "name": "collection_title",
      "value": "Favorites"
    },
    {
      "name": "favorite_empty",
      "value": "No favorites yet"
    },
    {
      "name": "history_empty",
      "value": "No browsing history"
    },
    {
      "name": "detail_basic_info",
      "value": "Basic Information"
    },
    {
      "name": "detail_population",
      "value": "Population"
    },
    {
      "name": "detail_religion",
      "value": "Religion"
    },
    {
      "name": "detail_language_family",
      "value": "Language Family"
    },
    {
      "name": "detail_distribution",
      "value": "Distribution"
    },
    {
      "name": "detail_introduction",
      "value": "Introduction"
    },
    {
      "name": "detail_add_fav",
      "value": "Add to Favorites"
    },
    {
      "name": "detail_remove_fav",
      "value": "Remove from Favorites"
    },
    {
      "name": "detail_btn_read",
      "value": "Read Aloud"
    },
    {
      "name": "detail_btn_stop",
      "value": "Stop"
    },
    {
      "name": "list_page_title",
      "value": "56 Ethnic Groups"
    },
    {
      "name": "home_featured",
      "value": "Featured"
    },
    {
      "name": "home_all",
      "value": "View All"
    },
    {
      "name": "home_quick_access",
      "value": "Quick Access"
    },
    {
      "name": "map_title",
      "value": "Distribution Map"
    },
    {
      "name": "feedback_title",
      "value": "Feedback"
    },
    {
      "name": "feedback_placeholder",
      "value": "Please share your feedback..."
    },
    {
      "name": "feedback_submit",
      "value": "Submit"
    },
    {
      "name": "feedback_success",
      "value": "Thank you for your feedback!"
    },
    {
      "name": "trivia_daily",
      "value": "Daily Trivia"
    },
    {
      "name": "splash_title",
      "value": "Ethnic Chronicles"
    },
    {
      "name": "splash_subtitle",
      "value": "Discover the rich cultures of 56 ethnic groups"
    }
  ]
}

💡 翻译小贴士:界面上的按钮文字、菜单文字,要尽量简洁。英文往往比中文长,要预留足够的空间。


7.2 检查硬编码字符串

接下来,要检查代码里有没有硬编码的字符串(直接写在代码里的中文字符串)。

// ❌ 不好:硬编码
Text('收藏')
  .fontSize(16)

Button('开始朗读')
  .onClick(() => { ... })

// ✅ 好:用资源
Text($r('app.string.detail_add_fav'))
  .fontSize(16)

Button($r('app.string.detail_btn_read'))
  .onClick(() => { ... })

这一步很枯燥,但很重要。可以用工具来扫描,比如:

  • 全局搜索正则表达式:'[\u4e00-\u9fa5]+' 找中文字符串
  • 或者用 Lint 工具来检查

「民族图鉴」项目目前已经大部分用了资源引用,但可能还有遗漏。这是一个需要持续注意的点。


7.3 民族数据的英文版

民族数据是「民族图鉴」的核心内容。我们的数据模型已经支持双语了(平行字段),但 Mock 数据里可能只有中文。

我们来给几个主要民族加上英文数据:

// mock/EthnicMockData.ets (简化版)

export class EthnicMockData {
  private static ethnics: EthnicGroup[] = [
    {
      id: '01',
      name: '汉族',
      nameEn: 'Han Chinese',
      pinyin: 'hàn zú',
      population: 1286311334,
      popRank: 1,
      mainRegion: '全国各地',
      mainRegionEn: 'Nationwide',
      languageFamily: '汉藏语系-汉语族',
      languageFamilyEn: 'Sino-Tibetan - Sinitic',
      religion: '多种信仰',
      religionEn: 'Various beliefs',
      description: '汉族是中国的主体民族...',
      descriptionEn: 'The Han Chinese are the largest ethnic group in China...',
      coverImage: '/common/rawfile/coverImage/01_han.jpg',
      // ... 更多字段
    },
    {
      id: '02',
      name: '壮族',
      nameEn: 'Zhuang',
      // ...
    },
    // ... 其他民族
  ];
}

💡 内容翻译的策略:56 个民族,每个几千字,全翻译完工作量很大。可以分阶段:

  1. 第一阶段:先翻译 10 个主要民族(汉族、壮族、回族、满族…)
  2. 第二阶段:再翻译 20 个
  3. 第三阶段:全部翻译完

没翻译的怎么办?可以显示英文名称 + 中文介绍,或者提示"暂无英文版"。


7.4 语言切换功能

最后,在设置页加一个语言切换的功能:

// pages/SettingsPage.ets (简化版)

import { I18nService } from '../services/I18nService';
import { AppLanguage, LanguageDisplayNames } from '../models/EnumModels';

@Entry
@Component
struct SettingsPage {
  @State currentLanguage: AppLanguage = AppLanguage.ZH_CN;
  @State showLanguageDialog: boolean = false;
  
  private i18nService: I18nService = I18nService.getInstance();
  
  aboutToAppear(): void {
    this.currentLanguage = this.i18nService.getLanguage();
    // 监听语言变化
    this.i18nService.addListener((lang: AppLanguage) => {
      this.currentLanguage = lang;
    });
  }
  
  aboutToDisappear(): void {
    // 移除监听
  }

  build() {
    Column() {
      // 标题
      Text($r('app.string.settings_title'))
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .padding(20)
      
      // 设置项列表
      List() {
        // 语言设置
        ListItem() {
          this.buildSettingItem(
            $r('app.string.settings_language'),
            LanguageDisplayNames[this.currentLanguage].native,
            () => {
              this.showLanguageDialog = true;
            }
          )
        }
        
        // 主题设置
        ListItem() {
          // ...
        }
        
        // ... 其他设置项
      }
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
  
  @Builder
  buildSettingItem(title: ResourceStr, value: string, onClick: () => void): void {
    Row() {
      Text(title)
        .fontSize(16)
        .fontColor('#333333')
        .layoutWeight(1)
      
      Text(value)
        .fontSize(14)
        .fontColor('#999999')
      
      Image($r('app.media.arrow_right'))
        .width(16)
        .height(16)
        .fillColor('#CCCCCC')
    }
    .width('100%')
    .height(56)
    .padding({ left: 20, right: 20 })
    .backgroundColor('#FFFFFF')
    .onClick(onClick)
  }
  
  // 语言选择弹窗
  buildLanguageDialog(): void {
    // 显示语言列表,点击切换
    // ...
  }
  
  private async onLanguageSelected(lang: AppLanguage): Promise<void> {
    await this.i18nService.setLanguage(lang);
    this.showLanguageDialog = false;
    // 切换语言后,可能需要刷新页面或重启应用
    // 鸿蒙的资源系统会自动更新,但有些手动获取的字符串需要刷新
  }
}

7.5 语言切换后的刷新问题

有个问题:切换语言后,界面上的文字会自动更新吗?

分两种情况:

  1. $r('app.string.xxx') 直接引用的:会自动更新。因为 $r 是响应式的,语言变了会自动重新加载资源。

  2. 在代码里手动获取的字符串:不会自动更新。因为已经取出来存到变量里了。

// ✅ 会自动更新
Text($r('app.string.settings_title'))

// ❌ 不会自动更新
@State title: string = '';

aboutToAppear(): void {
  this.title = getContext(this).resourceManager.getStringSync(
    $r('app.string.settings_title')
  );
}

// 解决方法:监听语言变化,手动更新
// 或者尽量用 $r() 直接引用

最佳实践

  • 尽量直接在组件里用 $r() 引用资源
  • 少在代码逻辑里手动取字符串
  • 如果必须手动取,记得监听语言变化,重新获取

💡 冷知识:为什么叫 i18n?因为 Internationalization 这个单词太长了,i 和 n 之间有 18 个字母,所以简称 i18n。类似的还有 k8s(Kubernetes)、a11y(Accessibility)等等。


⚠️ 常见问题与解决方案

问题1:文字溢出,显示不下

现象:中文显示正常,换成英文后文字太长,超出容器,被截断或换行。

为什么会这样?

  • 英文普遍比中文长(尤其是德文、俄文更长)
  • 同样的意思,中文可能 2-3 个字,英文要 10 几个字母
  • 比如"设置"是 2 个字,"Settings"是 8 个字母

解决方法

  1. 预留足够空间

    • 按钮、标签不要给固定宽度,用自适应
    • 预估一下最长的语言需要多少空间
  2. 允许换行

    • 不要限制行数(除非设计要求)
    • .maxLines() 配合 .textOverflow()
  3. 用省略号

    • 实在放不下,末尾显示省略号
    • .textOverflow({ overflow: TextOverflow.Ellipsis })
  4. 小字体

    • 长文本可以适当减小字号(但不要太小,看不清)
  5. 调整布局

    • 横排变竖排
    • 两栏变一栏

💡 经验法则:设计界面的时候,给文字预留 30-50% 的额外空间。这样换成英文或其他语言,才不会溢出。


问题2:翻译不准确,意思不对

现象:翻译读起来很奇怪,或者意思完全不对。

常见原因

原因 说明 解决方法
上下文缺失 翻译者不知道用在什么地方 给翻译者提供截图和上下文说明
术语不统一 同一个词有好几种译法 建立术语表,统一核心词汇
机器翻译直接用 没经过人工校对 机器翻译 + 人工校对
文化差异 有些说法直译过去很奇怪 找母语者做本地化,而不是直译

「民族图鉴」的经验

  • 专门建一个术语表文档,核心词汇统一译法
  • 翻译完了,找英语好的人通读一遍
  • 有疑问的地方标注出来,不要瞎猜

问题3:日期时间显示错误

现象:日期显示成了"月/日/年",但用户习惯"年/月/日"。

解决方法

  • 用系统的 DateTimeFormat,不要自己拼字符串
  • 传对 locale 参数
  • 测试的时候换不同的语言地区,看看格式对不对

常见坑

  • 月份从 0 开始(0=1月,11=12月),传错了就差一个月
  • 时区不对,差了几个小时
  • 相对时间的单位用错(应该用 day 却用了 date)

问题4:RTL 布局错乱

现象:切换到阿拉伯语,布局全乱了,有些元素还是从左到右。

常见原因

  • 用了固定的 left/right,没有用 start/end
  • 自定义绘制的内容(Canvas)没有翻转
  • 有方向的图标(箭头、指示)没翻转

解决方法

  1. 全面检查代码,把 left/right 换成 start/end
  2. 有方向的图标,准备 RTL 版本或自动翻转
  3. 自定义绘制的内容,根据布局方向翻转坐标系
  4. 找一台 RTL 语言的设备或模拟器,实际测试

💡 RTL 测试建议:如果暂时不支持 RTL 语言,可以先不用测。但写代码的时候养成好习惯,以后省得改。


问题5:语言切换后有些页面没刷新

现象:切换语言后,当前页面更新了,但返回上一页,上一页还是旧语言。

原因

  • 上一页的字符串是在 aboutToAppear 里取的,存在 @State 变量里
  • 切回页面时,页面没有重新创建,所以没更新

解决方法

  • 尽量用 $r() 直接引用(自动更新)
  • 如果必须手动取,在 onPageShowaboutToAppear 里重新取
  • 或者用 AppStorage + @StorageLink 来响应语言变化
// 用 @StorageLink 监听语言变化
@StorageLink('currentAppLanguage') currentLanguage: AppLanguage = AppLanguage.ZH_CN;

// 用 computed 属性,每次读取都重新计算
get title(): string {
  return getContext(this).resourceManager.getStringSync($r('app.string.title'));
}
// 但这样不是响应式的,语言变了不会自动更新

📝 本章小结

核心知识点

本文系统讲解了国际化的完整知识体系,并以「民族图鉴」英文版实现为实战案例,带你从零完成多语言适配。

1. 为什么需要国际化

  • 出海是趋势,文化输出、用户增长、品牌价值
  • 国内也有多语言需求(少数民族、港澳台、在华外国人)
  • 国际化要趁早,越晚做成本越高

2. 国际化的范围

  • 语言文字:界面文本、图片文字、字体、排版、书写方向
  • 日期时间:格式、时区、相对时间、星期起始日
  • 数字货币:千分位、小数点、货币符号与位置
  • 布局设计:文字长度、RTL 布局、文化禁忌
  • 文化合规:习俗、法律、内容差异

3. 资源管理

  • 字符串资源:string.json,按语言分目录
  • 带参数的字符串:%s、%d、%1$s(位置参数)
  • 图片资源:有文字的图片需要多语言版本
  • 布局资源:RTL 适配,用 start/end 代替 left/right

4. 日期时间国际化

  • 不同地区日期格式差异大(年/月/日、月/日/年、日/月/年)
  • 用系统的 DateTimeFormat,不要自己拼字符串
  • 存储传输用 UTC,显示用本地时间
  • 相对时间用 RelativeTimeFormat

5. 数字与货币格式化

  • 千分位分隔符、小数点符号,地区差异大
  • 货币符号位置、货币格式,各不相同
  • 用 NumberFormat,不要自己格式化

6. 翻译管理

  • 流程:提取 → 翻译 → 校验 → 合入 → 测试
  • 质量保证:术语统一、上下文、人工校对、实际预览
  • 策略:界面文本快迭代,内容文本分阶段

7. 「民族图鉴」实战

  • 准备英文 string.json
  • 检查并替换硬编码字符串
  • 民族数据双语化(平行字段)
  • 语言切换功能实现
  • 注意语言切换后的刷新问题

最佳实践总结

从项目第一天就考虑国际化

不要等中文版做完了再加英文
    ↓
一开始就按国际化的方式写
    ↓
以后加语言成本很低

字符串全部资源化,不要硬编码

// ❌ 不好
Text('收藏')

// ✅ 好
Text($r('app.string.favorite'))

用系统的格式化 API,不要自己拼

// ❌ 不好
Text(`${year}${month}${day}`)

// ✅ 好
Text(DateUtils.formatDate(date))

预留文字空间,防止溢出

英文比中文长 30-50%
德文、俄文可能更长
    ↓
设计的时候就留够空间
不要给固定宽度

术语统一,建立术语表

核心词汇先确定统一译法
    ↓
所有翻译都遵循术语表
    ↓
用户看到的术语是一致的

机器翻译 + 人工校对,性价比最高

机器翻译出初稿(快、便宜)
    ↓
母语者校对质量(地道、准确)
    ↓
既快又好,成本可控

下一步预告

在下一篇文章中,我们将:

  • 🔒 了解为什么需要安全加固,以及应用安全的四个层次
  • 🧩 学习代码混淆、资源混淆、加壳加固
  • 🔐 掌握数据安全:加密存储、加密算法、密钥管理
  • 🌐 了解网络安全:HTTPS、证书校验、防抓包
  • 📋 学习隐私合规:权限最小化、用户授权、PIPL 合规
  • 🧪 了解安全检测:静态扫描、渗透测试、漏洞修复
  • 🚩 分析安全加固的常见问题与解决方案

🔗 相关链接


💡 结语:国际化不只是"把文字翻译成英文",它是一种"尊重用户"的态度——不管用户说什么语言、在哪个地区、用什么书写方式,都能获得舒适、自然的使用体验。做好国际化,你的应用才能真正走向世界。下一篇,我们来聊一个同样重要的话题——安全加固。

Logo

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

更多推荐