前端國際化 i18n 架構實戰:next-intl、react-i18next 與多語言 SEO 全覆蓋

前端开发

國際化不只是翻譯文字

i18n 涉及翻譯、排版、日期/數字格式、貨幣、複數、RTL、SEO——遠比"換個語言包"複雜。

維度 範例
翻譯 "Hello" → "你好" → "こんにちは"
日期格式 2026/06/02 vs 02/06/2026 vs 06.02.2026
數字格式 1,234.56 vs 1.234,56 vs 1 234,56
貨幣 ¥1,234 vs $1,234 vs €1.234
複數 1 item vs 2 items(中文無複數變化)
排版方向 LTR(中文/英文)vs RTL(阿拉伯語)
URL 策略 /en/about vs /about?lang=en

一、翻譯檔案組織

按命名空間拆分

messages/
├── zh-CN/
│   ├── common.json       # 通用:按鈕、導航、錯誤
│   ├── home.json         # 首頁
│   ├── auth.json         # 登入/註冊
│   ├── dashboard.json    # 儀表板
│   └── errors.json       # 錯誤訊息
├── en/
│   ├── common.json
│   ├── home.json
│   └── ...
└── ja/
    ├── common.json
    └── ...

JSON 結構

{
  "common": {
    "button": {
      "submit": "提交",
      "cancel": "取消",
      "confirm": "確認"
    },
    "nav": {
      "home": "首頁",
      "about": "關於",
      "contact": "聯絡我們"
    }
  },
  "home": {
    "title": "免費線上工具箱",
    "subtitle": "200+ 工具,瀏覽器本地處理,隱私安全",
    "cta": "開始使用"
  }
}

巢狀鍵的扁平化

// 巢狀寫法
{ "home": { "hero": { "title": "工具箱" } } }

// 等價扁平寫法
{ "home.hero.title": "工具箱" }

二、next-intl 實戰

設定

// src/i18n/routing.ts
import { defineRouting } from 'next-intl/routing';

export const routing = defineRouting({
  locales: ['zh-CN', 'zh-TW', 'en', 'ja'],
  defaultLocale: 'zh-CN',
  localePrefix: 'always', // /zh-CN/about
});

伺服器端使用

// src/app/[locale]/page.tsx
import { useTranslations } from 'next-intl';

export default function HomePage() {
  const t = useTranslations('home');
  return (
    <div>
      <h1>{t('title')}</h1>
      <p>{t('subtitle')}</p>
    </div>
  );
}

客戶端使用

'use client';
import { useTranslations } from 'next-intl';

function LanguageSwitcher() {
  const router = useRouter();
  const pathname = usePathname();
  const locale = useLocale();

  function switchLocale(newLocale: string) {
    router.replace(pathname.replace(`/${locale}`, `/${newLocale}`));
  }

  return (
    <select value={locale} onChange={(e) => switchLocale(e.target.value)}>
      <option value="zh-CN">简体中文</option>
      <option value="zh-TW">繁體中文</option>
      <option value="en">English</option>
      <option value="ja">日本語</option>
    </select>
  );
}

三、ICU 訊息格式

變數插值

{
  "greeting": "你好,{name}!",
  "fileCount": "已上傳 {count} 個檔案"
}
t('greeting', { name: '張三' })       // 你好,張三!
t('fileCount', { count: 5 })          // 已上傳 5 個檔案

複數處理

{
  "items": "{count, plural, =0 {沒有項目} =1 {1 個項目} other {# 個項目}}"
}
count 中文 英文
0 沒有項目 0 items
1 1 個項目 1 item
5 5 個項目 5 items

選擇格式(性別等)

{
  "invitation": "{gender, select, male {邀請他} female {邀請她} other {邀請他們}}"
}

富文本

{
  "terms": "點選 <link>此連結</link> 檢視條款"
}
<p>{t.rich('terms', {
  link: (children) => <a href="/terms">{children}</a>
})}</p>

四、日期與數字格式化

日期格式

import { useFormatter } from 'next-intl';

function DateFormatter({ date }: { date: Date }) {
  const format = useFormatter();

  return (
    <div>
      {/* 中文:2026年6月2日 */}
      <p>{format.dateTime(date, { dateStyle: 'long' })}</p>

      {/* 英文:June 2, 2026 */}
      {/* 日文:2026年6月2日 */}

      {/* 自訂格式 */}
      <p>{format.dateTime(date, {
        year: 'numeric',
        month: '2-digit',
        day: '2-digit',
      })}</p>
      {/* zh-CN: 2026/06/02 */}
      {/* en: 06/02/2026 */}
      {/* ja: 2026/06/02 */}
    </div>
  );
}

數字與貨幣格式

function NumberFormatter() {
  const format = useFormatter();

  return (
    <div>
      {/* 數字 */}
      {format.number(1234.56)}          {/* zh-CN: 1,234.56 | de: 1.234,56 */}

      {/* 貨幣 */}
      {format.number(1234, { style: 'currency', currency: 'CNY' })}
      {/* zh-CN: ¥1,234.00 | en-US: CN¥1,234.00 */}

      {/* 百分比 */}
      {format.number(0.856, { style: 'percent' })}
      {/* zh-CN: 86% | en: 86% */}

      {/* 單位 */}
      {format.number(1024, { style: 'unit', unit: 'megabyte' })}
      {/* zh-CN: 1,024 MB | en: 1,024 MB */}
    </div>
  );
}

五、RTL 支援

檢測與設定

import { useLocale } from 'next-intl';

const RTL_LOCALES = ['ar', 'he', 'fa', 'ur'];

function AppLayout({ children }) {
  const locale = useLocale();
  const dir = RTL_LOCALES.includes(locale) ? 'rtl' : 'ltr';

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

CSS 邏輯屬性

/* 使用邏輯屬性,自動適配 RTL */
.card {
  margin-inline-start: 1rem;    /* LTR: margin-left | RTL: margin-right */
  padding-inline-end: 0.5rem;   /* LTR: padding-right | RTL: padding-left */
  border-inline-start: 2px solid blue;
  text-align: start;            /* LTR: left | RTL: right */
}

六、多語言 SEO

hreflang 標籤

<link rel="alternate" hreflang="zh-CN" href="https://example.com/zh-CN/about" />
<link rel="alternate" hreflang="zh-TW" href="https://example.com/zh-TW/about" />
<link rel="alternate" hreflang="en" href="https://example.com/en/about" />
<link rel="alternate" hreflang="ja" href="https://example.com/ja/about" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/about" />

Next.js metadata 生成

// src/app/[locale]/about/layout.tsx
export async function generateMetadata({ params: { locale } }) {
  const t = await getTranslations({ locale, namespace: 'about' });

  return {
    title: t('metaTitle'),
    description: t('metaDescription'),
    alternates: {
      canonical: `https://example.com/${locale}/about`,
      languages: {
        'zh-CN': 'https://example.com/zh-CN/about',
        'zh-TW': 'https://example.com/zh-TW/about',
        'en': 'https://example.com/en/about',
        'ja': 'https://example.com/ja/about',
        'x-default': 'https://example.com/en/about',
      },
    },
  };
}

Sitemap 多語言

// src/app/sitemap.ts
export default function sitemap() {
  const locales = ['zh-CN', 'zh-TW', 'en', 'ja'];
  const pages = ['/', '/about', '/tools', '/blog'];

  return locales.flatMap(locale =>
    pages.map(page => ({
      url: `https://example.com/${locale}${page}`,
      lastModified: new Date(),
      changeFrequency: 'weekly',
      priority: page === '/' ? 1 : 0.8,
    }))
  );
}

七、URL 策略對比

策略 範例 SEO 技術複雜度
子路徑 /zh-TW/about
子域名 tw.example.com/about
參數 /about?lang=zh-TW
獨立域名 example.tw/about

推薦:子路徑策略,SEO 友好且實作簡單。


八、翻譯品質保障

i18n 校驗指令碼

// scripts/i18n-check.js
const fs = require('fs');
const path = require('path');

const locales = ['zh-CN', 'en', 'ja'];
const baseLocale = 'zh-CN';

function flattenKeys(obj, prefix = '') {
  return Object.entries(obj).flatMap(([key, value]) =>
    typeof value === 'object'
      ? flattenKeys(value, prefix ? `${prefix}.${key}` : key)
      : [prefix ? `${prefix}.${key}` : key]
  );
}

const baseKeys = flattenKeys(JSON.parse(fs.readFileSync(`messages/${baseLocale}/common.json`, 'utf-8')));

for (const locale of locales.filter(l => l !== baseLocale)) {
  const localeKeys = flattenKeys(JSON.parse(fs.readFileSync(`messages/${locale}/common.json`, 'utf-8')));
  const missing = baseKeys.filter(k => !localeKeys.includes(k));
  const extra = localeKeys.filter(k => !baseKeys.includes(k));

  if (missing.length) console.log(`[${locale}] 缺少鍵:`, missing);
  if (extra.length) console.log(`[${locale}] 多餘鍵:`, extra);
}

CI 整合

- name: i18n check
  run: node scripts/i18n-check.js

翻譯工作流程

  1. 開發者只修改 zh-CN 檔案
  2. CI 校驗其他語言鍵的完整性
  3. 新鍵自動標記為待翻譯
  4. 翻譯平台(Crowdin/Transifex)同步
  5. 翻譯完成後自動 PR 回合

本站提供瀏覽器本地工具,免註冊即可試用 →

#i18n#国际化#next-intl#react-i18next#SEO#多语言