国际化 - 知蛛网1. Intl API(内置国际化对象)
日期时间格式化
// 创建日期
const date = new Date("2024-01-15T14:30:00");
// 基本格式化
console.log(new Intl.DateTimeFormat("en-US").format(date));
// "1/15/2024"
console.log(new Intl.DateTimeFormat("zh-CN").format(date));
// "2024/1/15"
console.log(new Intl.DateTimeFormat("de-DE").format(date));
// "15.1.2024"
// 详细格式化
const options = {
year: "numeric",
month: "long",
day: "numeric",
weekday: "long",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
timeZoneName: "short",
};
console.log(new Intl.DateTimeFormat("zh-CN", options).format(date));
// "2024年1月15日星期一 14:30:00 GMT+8"
console.log(new Intl.DateTimeFormat("en-US", options).format(date));
// "Monday, January 15, 2024, 02:30:00 PM GMT+8"
// 相对时间格式化
const rtf = new Intl.RelativeTimeFormat("zh-CN", {
numeric: "auto", // 'always' 或 'auto'
});
console.log(rtf.format(-1, "day")); // "昨天"
console.log(rtf.format(2, "day")); // "后天"
console.log(rtf.format(5, "hour")); // "5小时后"
console.log(rtf.format(-3, "month")); // "3个月前"
// 英文相对时间
const rtfEN = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
console.log(rtfEN.format(-1, "day")); // "yesterday"
console.log(rtfEN.format(2, "day")); // "in 2 days"
数字格式化
const number = 1234567.89;
// 基本数字格式化
console.log(new Intl.NumberFormat("en-US").format(number));
// "1,234,567.89"
console.log(new Intl.NumberFormat("zh-CN").format(number));
// "1,234,567.89"
console.log(new Intl.NumberFormat("de-DE").format(number));
// "1.234.567,89"
console.log(new Intl.NumberFormat("ar-EG").format(number));
// "١٬٢٣٤٬٥٦٧٫٨٩"
// 货币格式化
const currencyOptions = {
style: "currency",
currency: "USD",
currencyDisplay: "symbol", // 'symbol', 'code', 'name'
};
console.log(new Intl.NumberFormat("en-US", currencyOptions).format(1234.56));
// "$1,234.56"
console.log(
new Intl.NumberFormat("zh-CN", {
style: "currency",
currency: "CNY",
}).format(1234.56)
);
// "¥1,234.56"
console.log(
new Intl.NumberFormat("ja-JP", {
style: "currency",
currency: "JPY",
}).format(1234)
);
// "¥1,234"
// 百分比格式化
const percentOptions = { style: "percent", maximumFractionDigits: 2 };
console.log(new Intl.NumberFormat("en-US", percentOptions).format(0.4567));
// "45.67%"
// 单位格式化
const unitOptions = {
style: "unit",
unit: "kilometer-per-hour",
unitDisplay: "short",
};
console.log(new Intl.NumberFormat("en-US", unitOptions).format(100));
// "100 km/h"
console.log(new Intl.NumberFormat("zh-CN", unitOptions).format(100));
// "100 千米/小时"
列表格式化
const list = ["苹果", "香蕉", "橙子"];
const listFormatter = new Intl.ListFormat("zh-CN", {
style: "long", // 'long', 'short', 'narrow'
type: "conjunction", // 'conjunction', 'disjunction', 'unit'
});
console.log(listFormatter.format(list));
// "苹果、香蕉和橙子"
const listFormatterEN = new Intl.ListFormat("en-US", {
style: "long",
type: "conjunction",
});
console.log(listFormatterEN.format(["apple", "banana", "orange"]));
// "apple, banana, and orange"
// 或关系列表
const orFormatter = new Intl.ListFormat("zh-CN", {
style: "short",
type: "disjunction",
});
console.log(orFormatter.format(list));
// "苹果、香蕉或橙子"
多语言排序(Collator)
const words = ["äpple", "zebra", "apple", "éclair", "banana"];
// 英文排序
const collatorEN = new Intl.Collator("en");
console.log(words.sort(collatorEN.compare));
// ["apple", "banana", "éclair", "zebra", "äpple"]
// 德文排序
const collatorDE = new Intl.Collator("de");
console.log(words.sort(collatorDE.compare));
// ["apple", "äpple", "banana", "éclair", "zebra"]
// 中文拼音排序
const chineseWords = ["张三", "李四", "王五", "赵六"];
const collatorCN = new Intl.Collator("zh-CN");
console.log(chineseWords.sort(collatorCN.compare));
// ["李四", "王五", "张三", "赵六"]
// 敏感度选项
const sensitiveCollator = new Intl.Collator("en", {
sensitivity: "base", // 'base', 'accent', 'case', 'variant'
ignorePunctuation: true,
numeric: true,
});
2. i18next 库(功能强大的国际化解决方案)
安装和基础配置
npm install i18next
# 或使用 CDN
<script src="https://unpkg.com/i18next/dist/umd/i18next.min.js"></script>
基础使用
import i18n from "i18next";
// 初始化
i18n.init({
lng: "zh-CN", // 默认语言
fallbackLng: "en", // 回退语言
debug: process.env.NODE_ENV === "development",
resources: {
"zh-CN": {
translation: {
welcome: "欢迎 {{name}}!",
description: "这是描述文本",
button: {
save: "保存",
cancel: "取消",
delete: "删除",
},
product: {
one: "{{count}} 个产品",
other: "{{count}} 个产品",
},
},
},
en: {
translation: {
welcome: "Welcome {{name}}!",
description: "This is description",
button: {
save: "Save",
cancel: "Cancel",
delete: "Delete",
},
product: {
one: "{{count}} product",
other: "{{count}} products",
},
},
},
ja: {
translation: {
welcome: "ようこそ {{name}}!",
description: "これは説明です",
button: {
save: "保存",
cancel: "キャンセル",
delete: "削除",
},
},
},
},
});
// 使用
console.log(i18n.t("welcome", { name: "张三" }));
console.log(i18n.t("button.save"));
console.log(i18n.t("product", { count: 1 }));
console.log(i18n.t("product", { count: 5 }));
// 切换语言
i18n.changeLanguage("en").then(() => {
console.log(i18n.t("welcome", { name: "John" }));
});
// 获取当前语言
console.log(i18n.language);
console.log(i18n.languages); // 支持的语言数组
加载外部翻译文件
// 按需加载翻译文件
i18n.use(Backend) // 加载后端插件
.init({
lng: "zh-CN",
fallbackLng: "en",
backend: {
// 从服务器加载翻译文件
loadPath: "/locales/{{lng}}/{{ns}}.json",
},
ns: ["common", "validation", "error"], // 命名空间
defaultNS: "common",
});
// 手动添加资源
i18n.addResourceBundle("zh-CN", "validation", {
required: "此项必填",
email: "请输入有效的邮箱地址",
minLength: "长度不能少于 {{min}} 个字符",
});
// 使用命名空间
console.log(i18n.t("validation:required"));
3. React 国际化(react-i18next)
安装和配置
npm install react-i18next i18next
React 组件中使用
// i18n.js 配置
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
const resources = {
en: {
translation: {
title: "Welcome to React",
description: "Edit <1>src/App.js</1> and save to reload.",
learn: "Learn React",
user: {
welcome: "Welcome, {{name}}!",
messages: "You have {{count}} new message",
messages_plural: "You have {{count}} new messages",
},
},
},
zh: {
translation: {
title: "欢迎使用 React",
description: "编辑 <1>src/App.js</1> 并保存以重新加载。",
learn: "学习 React",
user: {
welcome: "欢迎,{{name}}!",
messages: "您有 {{count}} 条新消息",
messages_plural: "您有 {{count}} 条新消息",
},
},
},
};
i18n.use(initReactI18next).init({
resources,
lng: "zh",
interpolation: {
escapeValue: false, // React 已经转义了
},
});
export default i18n;
在组件中使用
// App.jsx
import React from "react";
import { useTranslation, Trans } from "react-i18next";
import "./i18n"; // 导入配置
function App() {
const { t, i18n } = useTranslation();
const changeLanguage = lng => {
i18n.changeLanguage(lng);
};
const user = { name: "张三", messageCount: 3 };
return (
<div className="App">
<div>
<button onClick={() => changeLanguage("zh")}>中文</button>
<button onClick={() => changeLanguage("en")}>English</button>
</div>
<h1>{t("title")}</h1>
<p>
<Trans i18nKey="description">
编辑 <code>src/App.js</code> 并保存以重新加载。
</Trans>
</p>
<p>{t("user.welcome", { name: user.name })}</p>
<p>{t("user.messages", { count: user.messageCount })}</p>
<a href="https://reactjs.org">{t("learn")}</a>
</div>
);
}
export default App;
4. Vue 国际化(vue-i18n)
安装和配置
在 Vue 3 中使用
// main.js
import { createApp } from "vue";
import { createI18n } from "vue-i18n";
import App from "./App.vue";
// 定义消息
const messages = {
en: {
message: {
hello: "Hello world",
welcome: "Welcome, {name}!",
product: {
apple: "apple | apples",
apple_plural: "apples",
},
},
},
zh: {
message: {
hello: "你好世界",
welcome: "欢迎,{name}!",
product: {
apple: "苹果",
apple_plural: "苹果",
},
},
},
ja: {
message: {
hello: "こんにちは世界",
welcome: "ようこそ、{name}!",
},
},
};
// 创建 i18n 实例
const i18n = createI18n({
locale: "zh", // 默认语言
fallbackLocale: "en", // 回退语言
messages,
legacy: false, // 使用组合式 API
});
const app = createApp(App);
app.use(i18n);
app.mount("#app");
在组件中使用
<template>
<div>
<!-- 语言切换 -->
<button @click="changeLocale('zh')">中文</button>
<button @click="changeLocale('en')">English</button>
<button @click="changeLocale('ja')">日本語</button>
<!-- 基本使用 -->
<p>{{ $t("message.hello") }}</p>
<!-- 带参数 -->
<p>{{ $t("message.welcome", { name: "张三" }) }}</p>
<!-- 复数 -->
<p>{{ $tc("message.product.apple", 1) }}</p>
<p>{{ $tc("message.product.apple", 3) }}</p>
<!-- 使用 v-t 指令 -->
<p v-t="'message.hello'"></p>
<p v-t="{ path: 'message.welcome', args: { name: '李四' } }"></p>
</div>
</template>
<script setup>
import { useI18n } from "vue-i18n";
const { t, locale } = useI18n();
const changeLocale = lang => {
locale.value = lang;
};
</script>
5. 日期时间库国际化(date-fns)
// 安装:npm install date-fns date-fns-tz
import { format, formatDistance, formatRelative } from "date-fns";
import { zhCN, enUS, ja } from "date-fns/locale";
import { format as formatTZ, utcToZonedTime } from "date-fns-tz";
const date = new Date("2024-01-15T14:30:00Z");
// 格式化日期
console.log(format(date, "yyyy-MM-dd HH:mm:ss", { locale: zhCN }));
// "2024-01-15 22:30:00" (北京时间)
console.log(format(date, "PPPPpp", { locale: enUS }));
// "Monday, January 15th, 2024 at 2:30:00 PM"
// 相对时间
console.log(formatDistance(date, new Date(), { locale: zhCN, addSuffix: true }));
// "约 后" 或 "约 前"
console.log(formatRelative(date, new Date(), { locale: enUS }));
// "today at 2:30 PM"
// 时区处理
const timeZone = "Asia/Shanghai";
const zonedDate = utcToZonedTime(date, timeZone);
console.log(formatTZ(zonedDate, "yyyy-MM-dd HH:mm:ss zzz", { timeZone }));
// "2024-01-15 22:30:00 CST"
// 支持的语言
const locales = {
"zh-CN": zhCN,
"en-US": enUS,
ja: ja,
de: require("date-fns/locale/de"),
fr: require("date-fns/locale/fr"),
};
function formatLocalized(date, formatStr, localeCode = "zh-CN") {
return format(date, formatStr, { locale: locales[localeCode] });
}
console.log(formatLocalized(date, "PPPP", "ja"));
// "2024年1月15日月曜日"
6. 货币格式化库(currency.js)
// 安装:npm install currency.js
import currency from "currency.js";
// 基本使用
const price = currency(1234.56, {
symbol: "¥",
precision: 2,
separator: ",",
decimal: ".",
pattern: "# !",
});
console.log(price.format()); // "¥1,234.56"
// 多货币配置
const currencies = {
USD: {
symbol: "$",
precision: 2,
separator: ",",
decimal: ".",
pattern: "! #",
},
CNY: {
symbol: "¥",
precision: 2,
separator: ",",
decimal: ".",
pattern: "! #",
},
JPY: {
symbol: "¥",
precision: 0,
separator: ",",
decimal: ".",
pattern: "! #",
},
EUR: {
symbol: "€",
precision: 2,
separator: ".",
decimal: ",",
pattern: "! #",
},
};
function formatCurrency(amount, currencyCode = "CNY") {
const config = currencies[currencyCode];
return currency(amount, config).format();
}
console.log(formatCurrency(1234.56, "USD")); // "$1,234.56"
console.log(formatCurrency(1234.56, "EUR")); // "€1.234,56"
console.log(formatCurrency(1234, "JPY")); // "¥1,234"
// 货币计算
const price1 = currency(100, { symbol: "$" });
const price2 = currency(50, { symbol: "$" });
console.log(price1.add(price2).format()); // "$150.00"
console.log(price1.multiply(1.1).format()); // "$110.00"
console.log(price1.subtract(20).format()); // "$80.00"
7. 自定义国际化方案
轻量级 i18n 实现
// i18n.js
class I18n {
constructor(options = {}) {
this.locale = options.locale || "zh-CN";
this.fallbackLocale = options.fallbackLocale || "en";
this.messages = options.messages || {};
this.cache = new Map();
}
// 设置语言
setLocale(locale) {
if (this.messages[locale]) {
this.locale = locale;
this.cache.clear(); // 清除缓存
this.onLanguageChange?.(locale);
}
}
// 获取翻译
t(key, params = {}) {
const cacheKey = `${this.locale}:${key}:${JSON.stringify(params)}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
let message = this.getMessage(key, this.locale);
if (!message && this.locale !== this.fallbackLocale) {
message = this.getMessage(key, this.fallbackLocale);
}
if (!message) {
console.warn(`Translation missing for key: ${key}`);
return key;
}
// 替换参数
const result = this.replaceParams(message, params);
// 缓存结果
this.cache.set(cacheKey, result);
return result;
}
// 获取消息
getMessage(key, locale) {
const keys = key.split(".");
let message = this.messages[locale];
for (const k of keys) {
if (message && message[k] !== undefined) {
message = message[k];
} else {
return null;
}
}
return message;
}
// 替换参数
replaceParams(message, params) {
return message.replace(/\{\{(\w+)\}\}/g, (match, key) => {
return params[key] !== undefined ? params[key] : match;
});
}
// 复数处理
tc(key, count, params = {}) {
const pluralKey = this.getPluralKey(key, count);
return this.t(pluralKey, { count, ...params });
}
getPluralKey(key, count) {
// 简单复数规则:英语
if (this.locale.startsWith("en")) {
return count === 1 ? `${key}_one` : `${key}_other`;
}
// 中文没有复数形式
if (this.locale.startsWith("zh")) {
return key;
}
return key;
}
// 添加资源
addResource(locale, namespace, resources) {
if (!this.messages[locale]) {
this.messages[locale] = {};
}
this.messages[locale][namespace] = resources;
}
}
// 使用示例
const i18n = new I18n({
locale: "zh-CN",
messages: {
"zh-CN": {
common: {
welcome: "欢迎, {{name}}!",
product: "产品",
product_plural: "产品",
item: "{{count}} 个物品",
},
},
en: {
common: {
welcome: "Welcome, {{name}}!",
product_one: "product",
product_other: "products",
item: "{{count}} item",
item_plural: "{{count}} items",
},
},
},
});
console.log(i18n.t("common.welcome", { name: "张三" }));
console.log(i18n.tc("common.product", 5));
console.log(i18n.tc("common.item", 1));
console.log(i18n.tc("common.item", 3));
8. 国际化最佳实践
1. 组织翻译文件
src/
├── locales/
│ ├── en/
│ │ ├── common.json
│ │ ├── validation.json
│ │ ├── error.json
│ │ └── product.json
│ ├── zh-CN/
│ │ ├── common.json
│ │ ├── validation.json
│ │ ├── error.json
│ │ └── product.json
│ └── index.js
2. 使用命名空间
// en/common.json
{
"buttons": {
"save": "Save",
"cancel": "Cancel",
"delete": "Delete"
},
"messages": {
"welcome": "Welcome back, {{user}}!",
"loading": "Loading...",
"error": "An error occurred"
}
}
// en/validation.json
{
"required": "This field is required",
"email": "Please enter a valid email address",
"minLength": "Must be at least {{count}} characters",
"maxLength": "Must be at most {{count}} characters"
}
3. 处理复数形式
// 不同的复数规则
const pluralRules = {
// 英语:1 和其他
en: count => (count === 1 ? "one" : "other"),
// 中文:没有复数形式
zh: () => "other",
// 阿拉伯语:6种复数形式
ar: count => {
if (count === 0) return "zero";
if (count === 1) return "one";
if (count === 2) return "two";
if (count % 100 >= 3 && count % 100 <= 10) return "few";
if (count % 100 >= 11 && count % 100 <= 99) return "many";
return "other";
},
// 俄语:3种复数形式
ru: count => {
const mod10 = count % 10;
const mod100 = count % 100;
if (mod10 === 1 && mod100 !== 11) return "one";
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return "few";
return "many";
},
};
4. RTL(从右到左)支持
/* CSS RTL 支持 */
[dir="rtl"] {
text-align: right;
}
[dir="rtl"] .container {
padding-right: 1rem;
padding-left: 0;
}
[dir="rtl"] .float-left {
float: right;
}
[dir="rtl"] .margin-left {
margin-right: 1rem;
margin-left: 0;
}
// 检测 RTL 语言
const rtlLanguages = ["ar", "he", "fa", "ur"];
function getTextDirection(locale) {
return rtlLanguages.includes(locale.split("-")[0]) ? "rtl" : "ltr";
}
// 设置文档方向
document.documentElement.dir = getTextDirection(i18n.language);
5. SEO 和可访问性
<!-- 设置语言属性 -->
<html lang="zh-CN" dir="ltr">
<!-- 或 -->
<html lang="ar" dir="rtl">
<!-- 多语言链接 -->
<link rel="alternate" hreflang="en" href="https://example.com/en" />
<link rel="alternate" hreflang="zh-CN" href="https://example.com/zh-CN" />
<link rel="alternate" hreflang="x-default" href="https://example.com/" />
<!-- 语言切换按钮 -->
<button aria-label="Switch to English" lang="en" onclick="changeLanguage('en')">
English
</button>
</html>
</html>
6. 性能优化
// 懒加载翻译文件
async function loadLocale(locale) {
try {
const response = await fetch(`/locales/${locale}.json`);
const messages = await response.json();
i18n.addResourceBundle(locale, "translation", messages);
return true;
} catch (error) {
console.error(`Failed to load locale: ${locale}`, error);
return false;
}
}
// 预加载常用语言
const preloadLocales = ["en", "zh-CN", "ja"];
preloadLocales.forEach(locale => {
loadLocale(locale);
});
9. 测试国际化
// 测试翻译覆盖
function validateTranslations(baseLocale, targetLocale) {
const baseKeys = getTranslationKeys(baseLocale);
const targetKeys = getTranslationKeys(targetLocale);
const missingKeys = baseKeys.filter(key => !targetKeys.includes(key));
const extraKeys = targetKeys.filter(key => !baseKeys.includes(key));
if (missingKeys.length > 0) {
console.warn(`Missing translations in ${targetLocale}:`, missingKeys);
}
if (extraKeys.length > 0) {
console.warn(`Extra translations in ${targetLocale}:`, extraKeys);
}
return { missingKeys, extraKeys };
}
function getTranslationKeys(locale) {
// 提取所有翻译键
const extractKeys = (obj, prefix = "") => {
let keys = [];
for (const key in obj) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof obj[key] === "string") {
keys.push(fullKey);
} else if (typeof obj[key] === "object") {
keys = keys.concat(extractKeys(obj[key], fullKey));
}
}
return keys;
};
return extractKeys(i18n.messages[locale]);
}