menu
首页文章相册工具关于
search
...

你可能遇到的国际化问题

2025/04/08
eye-

布局问题

  • 当你的用户使用阿拉伯语时,文档会从右到左显示。所以你需要调整文档的布局。
html {
  direction: rtl;
}

如果你使用绝对定位属性,让元素在左侧,那么当你改变文档方向时,元素仍然停留在左侧。你需要将元素的位置调整到右侧。

/* 之前,方向:从左到右 */
.element {
  position: absolute;
  left: 0;
}

/* 之后,方向:从右到左 */
.element {
  position: absolute;
  right: 0;
}
  • 不同语言的字符长度不同,所以你需要调整元素的宽度和高度。
const en = "What's the weather like today?"
const zh = "今天天气怎么样?"
const ar = "ما هو الطقس اليوم ؟"
const ja = "今日の天気はどうですか?"
const ko = "오늘 날씨는 어때요?"
const es = "¿Qué tiempo hace hoy?"
const fr = "Quel temps fait-il aujourd'hui?"
const de = "Wie ist das Wetter heute?"
const it = "Che tempo fa oggi?"
const pt = "Qual a temperatura hoje?"
const nl = "Wat is de weer vandaag?"
const pl = "Jakie jest pogoda dzisiaj?"
const ro = "Ce vreme este azi?"
const tr = "Bugün hava nasıl?"
const vi = "Hôm nay thời tiết thế nào?"
  • 不同国家有不同的价格格式

例如,在巴西(pt-BR),价格使用逗号作为小数分隔符而不是句号/点:

// 美国/英国格式
$10.50 (使用小数点)

// 巴西格式
R$ 10,50 (使用逗号作为小数分隔符)
  • 显示折扣

如果要显示促销价格,比如英文中的 35% off,在中文里面是 6.5 折。

discount: t("{percent} off", {
  percent: 35,
  num: (100 - 35) / 10, // 6.5
}),

然后再对应的翻译文件里增加:

// en.json
"{percent} off": "{percent} off"

// zh_CN.json
"{percent} off": "{num} 折"
  • 排序

如果你要对一个包含中文/英文/日文等多语言的数组排序,不应该直接使用 Array.sort(),它只按 UTF-16 编码排序,结果会很混乱。

例如:

["Apple", "Google", "facebook", "阿里", "腾讯", "字节", "ソニー", "トヨタ", "任天堂"].sort()
// ["Apple", "Google", "facebook", "ソニー", "トヨタ", "任天堂", "字節", "腾讯", "阿里"]

所以需要使用 localeCompare:

function createStringSorter({ caseInsensitive = true, order = ["en", "zh", "ja"] } = {}) {
  const collator = new Intl.Collator("zh", {
    sensitivity: caseInsensitive ? "base" : "variant",
  });

  const getGroup = (str) => {
    if (/^[\u3040-\u309f\u30a0-\u30ff]/.test(str)) return "ja";
    if (/^[\u4e00-\u9fff]/.test(str)) return "zh";
    return "en";
  };

  return (a, b) => {
    const groupA = getGroup(a);
    const groupB = getGroup(b);

    if (groupA !== groupB) {
      return order.indexOf(groupA) - order.indexOf(groupB);
    }

    return collator.compare(a, b);
  };
}


const items = ["Apple", "Google", "facebook", "阿里", "腾讯", "字节", "ソニー", "トヨタ", "任天堂"];

// default: en → zh → ja
items.sort(createStringSorter());
// ["Apple", "facebook", "Google", "阿里", "任天堂", "腾讯", "字节", "ソニー", "トヨタ"]

// ja → zh → en
items.sort(createStringSorter({ order: ["ja", "zh", "en"] }));

文本转换

不同语言的大小写转换规则不同。比如在土耳其语中,i 的大写是 İ(带点),而不是 I。

"i".toUpperCase() // "I" ✗
"i".toLocaleUpperCase("tr") // "İ" ✓

所以需要转换大小写时,应使用 toLocaleUpperCase / toLocaleLowerCase 并传入正确的 locale:

<Badge text={t("Beta").toLocaleUpperCase(i18n.language.replace("_", "-"))} />

注意:toLocaleUpperCase 要求 BCP 47 格式的语言标签(用连字符,如 zh-CN),但 i18next 使用下划线格式(如 zh_CN)。传入错误格式会导致页面崩溃:


"beta".toLocaleUpperCase("zh_CN") // ❌ RangeError: Incorrect locale information provided
"beta".toLocaleUpperCase("zh_TW") // ❌ RangeError: Incorrect locale information provided
"beta".toLocaleUpperCase("zh-CN") // ✅ "BETA"
"beta".toLocaleUpperCase("zh-TW") // ✅ "BETA"

传入前务必转换格式:i18n.language.replace("_", "-")。

或者


i18next.init({
  lng: "zh-CN",
  supportedLngs: ["en", "zh-CN", "zh-TW", "ja", "ko", "tr"],
});

复数规则

不同语言有不同的复数规则。例如:

  • 英语有两种形式:单数(1 minute)和复数(2 minutes)
  • 中文只有一种形式:1 分钟, 2 分钟
  • 俄语有三种形式:1 минута, 2 минуты, 5 минут
  • 阿拉伯语有六种形式:0 دقيقة, 1 دقيقة, 2 دقيقة, 3-10 دقائق, 11-99 دقيقة, 100+ دقيقة

你可以使用像 i18next 这样的库来处理复数规则:

// 英语
i18next.t("key", { count: 1 }) // "1 minute"
i18next.t("key", { count: 2 }) // "2 minutes"

// 中文
i18next.t("key", { count: 1 }) // "1分钟"
i18next.t("key", { count: 2 }) // "2分钟"

// 俄语
i18next.t("key", { count: 1 }) // "1 минута"
i18next.t("key", { count: 2 }) // "2 минуты"
i18next.t("key", { count: 5 }) // "5 минут"

i18next 中的 key 是你的翻译字符串的唯一标识符。它就像一个变量名,映射到不同语言的不同翻译。它的工作原理如下:

// 翻译文件
// en.json
{
  "time.minutes": {
    "one": "{{count}} minute",
    "other": "{{count}} minutes"
  }
}

// zh.json
{
  "time.minutes": "{{count}}分钟"
}

// 使用
i18next.t('time.minutes', { count: 1 }); // 英语中显示 "1 minute",中文中显示 "1分钟"
i18next.t('time.minutes', { count: 2 }); // 英语中显示 "2 minutes",中文中显示 "2分钟"

key 可以是:

  • 简单字符串:"welcome"
  • 嵌套路径:"common.buttons.submit"
  • 带有复数形式的 key:"time.minutes"

像 one 和 other 这样的复数形式是 i18next 用来处理不同复数规则的特殊 key:

  • one:用于单数形式(当 count 为 1 时)
  • other:用于复数形式(当 count 为 0 或 > 1 时)

有些语言有更多的复数形式。例如,在俄语中:

// ru.json
{
  "time.minutes": {
    "one": "{{count}} минута",    // 1
    "few": "{{count}} минуты",    // 2-4
    "many": "{{count}} минут",    // 5-20
    "other": "{{count}} минуты"   // 21, 22, 25-30, etc.
  }
}

复数规则基于 CLDR(通用区域设置数据存储库) 规范,该规范定义了不同语言的复数规则。i18next 根据计数和当前语言的规则自动选择正确的形式。

如果你的翻译文件不支持嵌套结构,你有一个简单的解决方案:

minute > 1 ? t("{num} minutes) : t("{num} minute)

但这种方法有几个问题:

  1. 它不适用于具有两个以上复数形式的语言(如俄语、阿拉伯语等)
  2. 它在许多语言中不能正确处理零
  3. 它不可维护 - 你需要在每个需要复数形式的地方编写这个逻辑
  4. 它不遵循 CLDR 标准,这意味着你的翻译可能是错误的
  5. 它使翻译人员更难使用你的翻译文件

例如,在俄语中:

// 错误的方法
minute > 1 ? t("{num} минут") : t("{num} минута")

// 这对于以下情况是错误的:
// 2-4 分钟(应该是 "минуты")
// 5-20 分钟(应该是 "минут")
// 21, 22, 25-30 分钟(应该是 "минуты")

这就是为什么最好使用 i18next 的内置复数处理:

政策问题

  • 出于法律原因,在土耳其不要显示删除线价格

由于土耳其政府的规定,我们不能在产品页面上显示删除线价格。 例如,如果你有一个原价为 100 TL、折扣价为 80 TL 的产品,你不能将其显示为 100 TL → 80 TL。 你需要将其显示为 80 TL。

<!-- 这在土耳其是不允许的 -->
<span class="current-price">80</span>
<del>
  <span class="original-price">100</span>
  <span class="currency">₺</span>
</del>
实现文件上传组件Next.js 拦截路由
目录
  • 布局问题
  • 文本转换
  • 复数规则
  • 政策问题