前端國際化 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
翻譯工作流程
- 開發者只修改
zh-CN檔案 - CI 校驗其他語言鍵的完整性
- 新鍵自動標記為待翻譯
- 翻譯平台(Crowdin/Transifex)同步
- 翻譯完成後自動 PR 回合
本站提供瀏覽器本地工具,免註冊即可試用 →
#i18n#国际化#next-intl#react-i18next#SEO#多语言