国际化(i18n)和本地化(l10n)实战

国际化(i18n)和本地化(l10n)实战

如果你的网站需要支持多语言,就需要做国际化和本地化。这两个概念经常混用,但含义不同:

  • i18n(Internationalization):使产品能适应不同语言和地区
  • l10n(Localization):为特定语言和地区做适配

这篇文章以 React 项目为例,聊聊多语言支持的实际实现。

项目结构

src/
  i18n/
    index.ts           # i18n 配置
    locales/
      zh-CN.json       # 简体中文
      en-US.json       # 英文
      ja-JP.json       # 日文

方案选型

react-i18next

最成熟的 React i18n 方案:

pnpm add react-i18next i18next i18next-browser-languagedetector
// src/i18n/index.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';

import zhCN from './locales/zh-CN.json';
import enUS from './locales/en-US.json';

i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources: {
      'zh-CN': { translation: zhCN },
      'en-US': { translation: enUS },
    },
    fallbackLng: 'zh-CN',
    interpolation: {
      escapeValue: false, // React 已经处理了 XSS
    },
    detection: {
      order: ['localStorage', 'navigator', 'htmlTag'],
    },
  });

使用方式

import { useTranslation } from 'react-i18next';

function LoginPage() {
  const { t } = useTranslation();

  return (
    <div>
      <h1>{t('login.title')}</h1>
      <input placeholder={t('login.username')} />
      <input placeholder={t('login.password')} />
      <button>{t('login.submit')}</button>
    </div>
  );
}

翻译文件

// zh-CN.json
{
  "login": {
    "title": "登录",
    "username": "用户名",
    "password": "密码",
    "submit": "登录",
    "forgot_password": "忘记密码?"
  }
}

// en-US.json
{
  "login": {
    "title": "Sign In",
    "username": "Username",
    "password": "Password",
    "submit": "Sign In",
    "forgot_password": "Forgot password?"
  }
}

带变量的翻译

{
  "post": {
    "comment_count": "{{count}} 条评论",
    "published_at": "发布于 {{date}}"
  }
}
t('post.comment_count', { count: 42 }) // "42 条评论"
t('post.published_at', { date: '2025-01-15' }) // "发布于 2025-01-15"

复数处理

不同语言的复数规则不同(中文不分单复数,英语分,俄语有三种形式):

{
  "post": {
    "comment_count_one": "{{count}} comment",
    "comment_count_other": "{{count}} comments"
  }
}

i18next 会根据当前语言自动选择正确的复数形式。

切换语言

import { useTranslation } from 'react-i18next';

function LanguageSwitcher() {
  const { i18n } = useTranslation();

  return (
    <select
      value={i18n.language}
      onChange={(e) => i18n.changeLanguage(e.target.value)}
    >
      <option value="zh-CN">中文</option>
      <option value="en-US">English</option>
      <option value="ja-JP">日本語</option>
    </select>
  );
}

Next.js 的 i18n

如果用 Next.js,有内置的 i18n 方案:

// next.config.js
module.exports = {
  i18n: {
    locales: ['zh-CN', 'en-US', 'ja-JP'],
    defaultLocale: 'zh-CN',
    localeDetection: true,
  },
};
// pages/blog/[id].tsx
import { useRouter } from 'next/router';

export default function BlogPost() {
  const router = useRouter();
  const { locale } = router;

  return <div>当前语言: {locale}</div>;
}

App Router 下用 next-intl

// app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';

export default async function LocaleLayout({
  children,
  params: { locale },
}) {
  const messages = await getMessages();

  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}

本地化不只是翻译

除了文本翻译,本地化还包括:

日期和时间

new Intl.DateTimeFormat('zh-CN', {
  year: 'numeric',
  month: 'long',
  day: 'numeric',
}).format(new Date('2025-01-15'));
// "2025年1月15日"

new Intl.DateTimeFormat('en-US', {
  year: 'numeric',
  month: 'long',
  day: 'numeric',
}).format(new Date('2025-01-15'));
// "January 15, 2025"

数字和货币

new Intl.NumberFormat('zh-CN', {
  style: 'currency',
  currency: 'CNY',
}).format(1234.56);
// "¥1,234.56"

new Intl.NumberFormat('ja-JP', {
  style: 'currency',
  currency: 'JPY',
}).format(1234.56);
// "¥1,235"

相对时间

new Intl.RelativeTimeFormat('zh-CN', { numeric: 'auto' }).format(-1, 'day');
// "昨天"

镜像布局

阿拉伯语和希伯来语是从右到左(RTL)书写的:

[dir="rtl"] .sidebar {
  margin-left: 0;
  margin-right: 1rem;
}

用 CSS 逻辑属性可以自动处理:

.sidebar {
  margin-inline-start: 1rem; /* LTR: margin-left, RTL: margin-right */
}

不要过度国际化

如果你的网站只面向中文用户,不需要做国际化。国际化增加了维护成本——每新增一个功能,翻译文件就要同步更新。

建议:

  • 确定目标用户和语言
  • 先做好一套语言的质量
  • 需要的时候再扩展其他语言
  • 用翻译管理平台(如 Crowdin、Lokalise)而不是手动维护 JSON 文件

国际化不是技术难点,难点在于翻译质量和维护成本。技术上 react-i18next 已经很成熟了,开箱即用。

ReactTypeScriptNext.js
返回首页