# 前端国际化:为全球用户打造无缝体验
在当今全球化的数字时代,前端开发不再局限于单一地区或语言的用户。为了满足不同国家和地区用户的需求,前端国际化成为了至关重要的一环。本文将深入探讨前端国际化的重要性、实现方法以及最佳实践。
# 一、为什么前端国际化至关重要?
- 扩大用户群体,随着互联网的普及,产品的潜在用户可能来自世界各地。通过国际化,能够让更多的人访问和使用你的应用,从而扩大用户群体。
- 提升用户体验,为用户提供他们熟悉的语言和文化习惯,可以极大地提升用户体验。用户能够更轻松地理解和操作应用,减少学习成本和困惑。
- 增强品牌影响力,一个支持多种语言的应用展示了品牌的全球化视野和对不同用户的关注。这有助于增强品牌的影响力和声誉,提高品牌在全球市场的竞争力。
# 二、背景知识
# 一、国际化(Internationalization,通常缩写为 i18n)
国际化是指设计和开发软件使其能够适应不同的地区和语言环境,而无需进行大量的重新设计或重新编码。它的目标是使软件具有足够的灵活性,以便在不同的文化和语言背景下都能正常运行。 包括:日期,数字,文本方向,翻译等
# 二、本地化(Localization,通常缩写为 L10n)
本地化是在国际化的基础上,针对特定的地区或语言进行具体的调整和定制。它包括翻译软件中的文本内容、调整界面布局以适应不同的语言长度、使用当地习惯的图标和图像等。 包括:语言本地习惯,文化适应,法律法规,货币单位等
# 三、前端国际化的常见问题
- 语言:这是基本的,也是工作量占比比较大的部分。
- 排版:不同语言字体、书写方向、单位空间信息密度都不相同。
- 文本编码:得益于 Unicode 的普及,这个问题越来越少遇见了,否则不同语言可能有各自独立的文本编码格式。
- 日期与时间:时区、显示格式、历法、节假日。
- 数字:分隔符、读法。
- 货币:货币符号,汇率。
- 电话或手机号码:不同国家地区格式不同。
- 银行卡或信用卡:不同银行格式不同。
- 邮编:不同国家地区格式不同,不过现在比较少使用了。
- 度量单位:比如美国温度常用华氏温度,长度使用英寸、英里等,而非国际单位。
- 法律:各个国家和地区可能对于数据安全性和隐私性法律法规不同,比如欧盟对隐私的 GDPR 要求、中国大陆对于软件服务数据存放的要求、域名备案要求等。
- 文化:信仰、保守与开放、颜色、意识形态等等都可能不同。
# 四、工具和库
框架 | 说明 |
---|---|
react-intl | 用于React |
vue-i18n | 用于Vue |
angular-i18n | 用于Angular |
i18next | 通用,无关框架 |
react-intl-universal | 阿里的国际化,无关框架 |
FormatJS |
# 五、最佳实践
# 1. 数量和名词应该合并在一个key中
1个人 %1$s个人
%1$s people
# 2. 表述应当完整,不要随意截断
此活动时间是2022-2023年
此活动时间是%1$s-%2$s年
# 3. 多个占位符需要有所去烦恼
此活动时间是2022-2023年
此活动时间是%1$s-%2$s年
# 4. 创建合理的日期月份星期的key
此活动开始时间是星期三
此活动开始时间是%1$s
需要接入本地化
# 5. 本地化-姓名
获取对应locale的firstName和lastName的顺序,然后组合成fullName
# 6. 本地化-数字
// JSON
{
"intlNumber": "Some {{val, number}}",
"intlNumberWithOptions": "Some {{val, number(minimumFractionDigits: 2)}}"
}
i18next.t('intlNumber', { val: 1000 });
// --> Some 1,000
i18next.t('intlNumber', { val: 1000.1, minimumFractionDigits: 3 });
// --> Some 1,000.100
i18next.t('intlNumber', { val: 1000.1, formatParams: { val: { minimumFractionDigits: 3 } } });
// --> Some 1,000.100
i18next.t('intlNumberWithOptions', { val: 2000 });
// --> Some 2,000.00
i18next.t('intlNumberWithOptions', { val: 2000, minimumFractionDigits: 3 });
// --> Some 2,000.000
# 7. 本地化-货币
// JSON
{
"intlCurrencyWithOptionsSimplified": "The value is {{val, currency(USD)}}",
"intlCurrencyWithOptions": "The value is {{val, currency(currency: USD)}}",
"twoIntlCurrencyWithUniqueFormatOptions": "The value is {{localValue, currency}} or {{altValue, currency}}",
}
i18next.t('intlCurrencyWithOptionsSimplified', { val: 2000 });
// --> The value is $2,000.00
i18next.t('intlCurrencyWithOptions', { val: 2300 });
// --> The value is $2,300.00
i18next.t('twoIntlCurrencyWithUniqueFormatOptions',
{
localValue: 12345.67,
altValue: 16543.21,
formatParams: {
localValue: { currency: 'USD', locale: 'en-US' },
altValue: { currency: 'CAD', locale: 'fr-CA' },
},
},);
// --> The value is $12,345.67 or 16 543,21 $ CA
# 8. 本地化-日期
根据locale本地化日期和时间
// JSON
{
"intlDateTime": "On the {{val, datetime}}",
}
i18next.t('intlDateTime', { val: new Date(Date.UTC(2012, 11, 20, 3, 0, 0)) });
// --> On the 12/20/2012
i18next.t('intlDateTime',
{
val: new Date(Date.UTC(2012, 11, 20, 3, 0, 0)),
formatParams: {
val: { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' },
},
});
// --> On the Thursday, December 20, 2012
# 9. 本地化-度量衡
根据locale本地化度量衡
# 10. 本地化-单复数
根据locale和数字获取单复数的展示
{
"key_zero": "zero",
"key_one": "singular",
"key_two": "two",
"key_few": "few",
"key_many": "many",
"key_other": "other"
}
i18next.t('key', {count: 0}); // -> "zero"
i18next.t('key', {count: 1}); // -> "singular"
i18next.t('key', {count: 2}); // -> "two"
i18next.t('key', {count: 3}); // -> "few"
i18next.t('key', {count: 4}); // -> "few"
i18next.t('key', {count: 5}); // -> "few"
i18next.t('key', {count: 11}); // -> "many"
i18next.t('key', {count: 99}); // -> "many"
i18next.t('key', {count: 100}); // -> "other"
# 11. 本地化-符号
中国-郑州 因为不同地方的习惯原因 中间的“-” 也是需要翻译的 正确做法: %1$s-%1$s
具体的本地化格式 翻译团队来决定
# 六、架构设计
如果是小项目,小团队,考虑到时间人力成本,可以把翻译数据放在项目文件中,或者是存储为一个json文件或js文件注入到window中
具体流程:
- 先把文案翻译变成每一个语言的json文件
- 通过url的locale参数或者域名(cookie)获取对应的语言
- 前端读取json文件,然后把翻译通过script脚本注入到window全局中
- 服务端直接读取json文件来做翻译
如果是大项目,跨团队合作,需要大量的沟通成本,就需要做一个翻译中台,中台统一提供 翻译平台,接入sdk,本地化等,分离出语言相关的内容
- 翻译团队在翻译平台翻译好项目的文案,发布
- 通过url的locale参数或者域名(cookie)获取对应的语言
- 前端通过sdk进行获取翻译后的值
- 服务端通过sdk进行获取翻译后的值
# 七、路由(如何确定语言)
一般有两种形式的路由
- 域名路由
- 子路由
域名路由
- www.zhq123.com
- hk.zhq123.com
- jp.zhq123.com
子路由
- zhq123.com/www/
- zhq123.com/hk/
- zhq123.com/jp/
根据域名或者子路由来确定语言
但是一般也要携带参数locale=zh-HK
locale(语言区域设置)的格式通常是 “语言代码 - 地区代码”
具体见附录
路由 | 语言 |
---|---|
hk.zhq123.com | zh-HK |
hk.zhq123.com?locale=zh-HK | zh-HK |
hk.zhq123.com?locale=jp-JP | 重定向至hk.zhq123.com?locale=zh-HK |
如果locale 和 域名匹配不上,就取域名的默认语言
这部分逻辑应该在服务端中间件来做
# 八、代码实现步骤
i18next
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script
src="https://cdnjs.cloudflare.com/ajax/libs/i18next/23.15.1/i18next.min.js"></script>
</head>
<body>
<div id="output"></div>
</body>
<script>
/**
* 获取当前语言
* html下发的时候就要把locale下发在cookie中
*
* 服务端下发html的时候写入locale的cookie
* */
function getLocale() {
const cookie = document.cookie;
const cookieArr = cookie.split(';');
for (let i = 0; i < cookieArr.length; i++) {
const item = cookieArr[i];
const itemArr = item.split('=');
if (itemArr[0].trim() === 'locale') {
return itemArr[1];
}
}
return 'en-US';
}
window.dataJson = {
"en-US": {
translation: {
"welcome": "Hello World!"
}
},
"zh-HK": {
translation: {
"welcome": "你好,世界!"
}
}
}
i18next.init({
lng: 'en', // if you're using a language detector, do not define the lng option
debug: true,
resources: window.dataJson
});
const locale = getLocale();
i18next.changeLanguage(locale);
document.getElementById('output').innerHTML = i18next.t('welcome');
</script>
</html>
# 附录
下面是部分语言
zh-CN:中文简体,中国大陆地区。
zh-TW:中文繁体,中国台湾地区。
zh-HK:中文繁体,中国香港特别行政区。
zh-MO:中文繁体,中国澳门特别行政区。
zh-SG:中文简体,新加坡(新加坡也使用简体中文,但在一些词汇和习惯表达上可能与中国大陆略有不同)。
en-US:英语,美国。
en-GB:英语,英国。
fr-FR:法语,法国。
de-DE:德语,德国。
es-ES:西班牙语,西班牙。
it-IT:意大利语,意大利。
pt-PT:葡萄牙语,葡萄牙。
pt-BR:葡萄牙语,巴西。
ru-RU:俄语,俄罗斯。
ja-JP:日语,日本。
ko-KR:韩语,韩国。
hi-IN:印地语,印度。
ar-SA:阿拉伯语,沙特阿拉伯。