多語言網站開發:輕鬆支援5種以上語言
第三種語言上線,路由配置就崩潰了。內容編輯打開CMS卻看不出哪些內容已翻譯、哪些仍是草稿、哪些在德文版本上線但日文版本還沒有。套件大小膨脹到800KB,因為每個語言區域都會在每一頁加載。hreflang標籤?沒人記得設定它,直到週四才在向客戶展示測試環境時發現漏了。如果你要為五種或更多語言建置網站,這些架構決策必須在你寫第一行路由代碼前就鎖定——而不是等翻譯人員寄來第一份文件時才臨時修補。以下是我們實踐過、確實能經得起真實翻譯團隊考驗的路由策略、CMS結構和優化方案。
我們已經為金融科技、醫療和電商客戶交付過支援8到14種語言的多語言網站。在做過足夠次數後我學到:一個能處理2種語言的網站和一個能處理12種語言的網站之間的差異,並不在於複雜度,而在於你是否有正確的抽象化設計。本指南涵蓋從URL策略、i18n路由到CMS建模、翻譯工作流程和效能優化的所有內容。
目錄
- 為什麼大多數多語言實現在規模化時會失敗
- URL策略:子域名vs子目錄vs國家代碼頂級域
- 多語言網站的框架選擇
- i18n路由架構
- 無頭CMS的多語言內容建模
- 翻譯工作流程自動化
- 多語言網站的SEO
- 跨語言區域的效能優化
- 從右到左(RTL)語言支援
- 多語言網站的測試和QA
- 常見問題
為什麼大多數多語言實現在規模化時會失敗
每次都是同樣的故事。團隊用英文建置一個網站,有人要求加上西班牙文,他們就塞進一個翻譯庫、硬編碼一些語言區域切換邏輯,然後上線。接著有人要求加法文。然後德文。然後日文。到第五種語言時,他們已經淹沒在以下問題中:
- 路由混亂:語言區域前綴一旦引入動態路由就會崩潰
- 內容漂移:翻譯文件落後原始語言數週——有時候如果老實說的話,落後數個月
- 套件膨脹:無論使用者實際需要哪個語言區域,所有翻譯字符串都會被加載
- SEO盲點:缺少或破損的hreflang標註、重複內容懲罰徹底摧毀排名
- 版面崩壞:德文文本比英文長40%、日文完全需要不同的字體堆棧
根本原因?團隊把多語言當作一項功能。這不是功能。當你支援5種或更多語言時,本地化會影響路由、資料建模、構建管道、CDN配置和部署策略。你不能在週五下午npm install某個套件就說完成了。這是基礎性的——否則就是一團亂。
URL策略:子域名vs子目錄vs國家代碼頂級域
你的URL結構是多語言SEO中單一最重要的決策。而且上線後幾乎不可能改變而不會摧毀排名。有三種真正的選項:
| 策略 | 範例 | SEO權威性 | 實現複雜度 | 成本 |
|---|---|---|---|---|
| 子目錄 | example.com/fr/about | 整合(單一域名) | 低 | 低 |
| 子域名 | fr.example.com/about | 分散(視為獨立網站) | 中 | 低 |
| 國家代碼頂級域 | example.fr/about | 獨立(每國分別) | 高 | 高($10-50/域名/年 × n) |
| 查詢參數 | example.com/about?lang=fr | 差(不建議) | 低 | 低 |
我們對5種或更多語言的建議:子目錄。 理由如下:
- 域名權威性整合:所有反向連結都能造福每個語言版本。使用子域名時,8種語言基本上就是在為8個獨立網站構建權威性。這很殘酷——而且完全不必要。
- 簡化基礎架構:單一部署、一個SSL憑證、一個CDN配置。搞定。
- 更簡單的分析:單一GA4屬性搭配語言區域維度,而不是跨域追蹤的惡夢。如果你曾經浪費過週四下午除錯跨域GA設定,你確切知道我在說什麼。
- 降低成本:不需要為每個語言區域註冊域名。
例外情況是什麼?當你需要按國家有真正不同的內容時——不只是語言。德國的德文網站vs瑞士的德文網站,價格、法律條款和產品可用性都不同?這才是真正的區分。國家代碼頂級域或子域名搭配國家特定內容模型在這種情況下才真正有意義。
# 建議的子目錄結構
example.com/ → 英文(預設)
example.com/fr/ → 法文
example.com/de/ → 德文
example.com/ja/ → 日文
example.com/ar/ → 阿拉伯文
example.com/pt-br/ → 巴西葡萄牙文
注意用pt-br而不只是pt。當支援5種或更多語言時,你一定會遇到語言vs語言區域的區分。巴西葡萄牙文和歐洲葡萄牙文差異大到使用者會注意——而且相信我,他們確實會告訴你這件事。從一開始就用BCP 47標籤規劃language-region代碼。稍後改造這個東西的痛苦程度無法充分表達,除非你親身經歷過。
多語言網站的框架選擇
並非所有框架都能平等地處理i18n。以下是主要玩家在2026年針對5種或更多語言支援的情況:
| 框架 | 內建i18n路由 | 靜態+動態 | 按語言區域進行套件分割 | RTL支援 | 最適用於 |
|---|---|---|---|---|---|
| Next.js 15 | ✅(App Router) | ✅ | ✅(透過配置) | 手動 | 全棧應用、動態內容 |
| Astro 5 | ✅(手動+Starlight) | ✅ | ✅(自動按頁) | 手動 | 內容豐富的行銷網站 |
| Nuxt 3 | ✅(@nuxtjs/i18n) | ✅ | ✅ | 手動 | Vue生態系專案 |
| Remix / React Router 7 | ❌(手動) | ✅ | 手動 | 手動 | 複雜的互動應用 |
| SvelteKit | ❌(手動) | ✅ | 手動 | 手動 | 效能關鍵應用 |
Next.js 15多語言架構
Next.js目前擁有最成熟的i18n故事,主要得益於App Router。[locale]動態區段模式提供了乾淨的路由而無需中介軟體技巧:
// app/[locale]/layout.tsx
import { notFound } from 'next/navigation';
const locales = ['en', 'fr', 'de', 'ja', 'ar', 'pt-br', 'es', 'ko'];
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export default function LocaleLayout({
children,
params: { locale },
}: {
children: React.ReactNode;
params: { locale: string };
}) {
if (!locales.includes(locale)) notFound();
return (
<html lang={locale} dir={locale === 'ar' ? 'rtl' : 'ltr'}>
<body>{children}</body>
</html>
);
}
針對翻譯字符串管理,next-intl基本上已成為標準。它支援ICU MessageFormat、伺服器元件,以及——這是重點——按語言區域進行套件分割,讓你的日文使用者不會下載德文翻譯。這重要性遠超過大多數人的想像。
// i18n/request.ts
import { getRequestConfig } from 'next-intl/server';
export default getRequestConfig(async ({ locale }) => ({
messages: (await import(`../messages/${locale}.json`)).default,
}));
在我們的Next.js開發能力頁面深入了解這個架構。
Astro用於內容豐富的多語言網站
Astro的內容集合非常適合多語言行銷網站和文檔。每項內容都按語言區域組織,完全不需JavaScript開銷:
src/content/
blog/
en/
getting-started.md
pricing-guide.md
fr/
getting-started.md
pricing-guide.md
de/
getting-started.md
Astro 5的內容層API讓按語言區域查詢內容和為所有語言生成靜態頁面變得超簡單。對於200頁網站的8種語言版本,Astro在30秒內生成1,600個靜態HTML頁面——每個完全優化且零執行時JavaScript,除非你明確添加互動性。想想這點。這簡直太瘋狂了。
在我們的Astro開發實踐了解更多。
i18n路由架構
基於中介軟體的語言區域檢測
為了最佳UX,你想在首次訪問時檢測使用者偏好的語言並重定向。在Next.js中介軟體:
// middleware.ts
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
locales: ['en', 'fr', 'de', 'ja', 'ar', 'pt-br', 'es', 'ko'],
defaultLocale: 'en',
localeDetection: true, // 使用Accept-Language標頭
localePrefix: 'as-needed', // 預設語言區域無/en/前綴
});
export const config = {
matcher: ['/((?!api|_next|_vercel|.*\\..*).*)'],
};
檢測優先順序應該這樣進行:
- 明確的URL語言區域(
/fr/about)——總是優先,無例外 - Cookie(
NEXT_LOCALE)——尊重使用者之前的選擇 - Accept-Language標頭——瀏覽器偏好
- GeoIP——謹慎使用;很多僑民和旅客會用不同於他們位置的語言瀏覽
- 預設語言區域——備選
不重新加載整個頁面的語言切換
我們經常看到的一個錯誤:將語言切換實現為完整導航。當有人在/en/about時從英文切到法文,他們應該登陸/fr/about——而不是/fr/。沒有人想被丟回首頁。你需要跨語言區域的路徑映射:
// components/LocaleSwitcher.tsx
'use client';
import { usePathname, useRouter } from 'next/navigation';
export function LocaleSwitcher({ currentLocale, locales }) {
const pathname = usePathname();
const router = useRouter();
const switchLocale = (newLocale: string) => {
// 用新語言區域替換目前的語言區域區段
const newPath = pathname.replace(`/${currentLocale}`, `/${newLocale}`);
router.push(newPath);
};
return (
<select
value={currentLocale}
onChange={(e) => switchLocale(e.target.value)}
>
{locales.map((locale) => (
<option key={locale} value={locale}>
{new Intl.DisplayNames([locale], { type: 'language' }).of(locale)}
</option>
))}
</select>
);
}
快速提示:使用Intl.DisplayNames以他們自己的文字顯示語言名稱(Français、Deutsch、日本語)而不是英文。小細節。使用者肯定會注意到。
無頭CMS的多語言內容建模
無頭CMS對5種或更多語言來說是不可或缺的。WordPress搭配WPML在三個語言區域後變成維護惡夢——我們看過它發生太多次了。以下是主要無頭平台在這方面的表現:
| CMS | 本地化模型 | 翻譯工作流程 | API查詢模式 | 定價影響 |
|---|---|---|---|---|
| Contentful | 欄位層級語言區域 | 內建+外部整合 | ?locale=fr |
每個語言區域都計入項目限制 |
| Sanity | 文件層級(建議) | 外掛式(Sanity Translate) | GROQ按語言篩選 | 對文件層級定價無影響 |
| Storyblok | 欄位層級搭配資料夾 | 內建翻譯UI | 維度API | 包含在所有方案中 |
| Hygraph | 欄位層級語言區域 | 基於階段的工作流程 | GraphQL中的locales: [fr] |
語言區域計入方案限制 |
| Payload CMS | 欄位層級或集合層級 | 自訂工作流程 | 按語言區域欄位篩選 | 自託管,無按語言區域計費 |
欄位層級vs文件層級本地化
這是多語言網站中最重要的CMS架構決策。大多數機構搞錯了。
欄位層級本地化(Contentful、Storyblok):內容項目中的每個欄位都持有每個語言區域的值。單一部落格文章項目包含英文標題、法文標題、德文標題等等——全部擠在一個地方。
文件層級本地化(Sanity的建議模式):每個語言區域有自己的文件,透過共享參考ID連結。
對於5種或更多語言,我們強烈建議文件層級本地化用於長篇內容,欄位層級本地化用於結構化資料——產品名稱、中繼資料、UI標籤。理由:
- 使用欄位層級本地化跨8種語言,編輯部落格文章時意味著要滾過7種其他語言的內容才能找到你需要的欄位。內容編輯討厭這個。像是真的、發自內心地討厭。
- 文件層級讓編輯UI保持乾淨——你的法文編輯只看到法文內容
- 翻譯狀態追蹤按文件變得簡單得多(草稿、審核中、每個語言區域已發佈)
- 需要時內容可以按語言區域有所不同——不同的英雄圖像、不同的行動號召用於不同市場
在Sanity中,看起來像這樣:
// schemas/blogPost.ts
export default defineType({
name: 'blogPost',
type: 'document',
fields: [
defineField({
name: 'language',
type: 'string',
options: {
list: [
{ title: 'English', value: 'en' },
{ title: 'French', value: 'fr' },
{ title: 'German', value: 'de' },
// ...
],
},
}),
defineField({
name: 'translationGroup',
type: 'string', // 此文章所有翻譯間的共享UUID
hidden: true,
}),
defineField({ name: 'title', type: 'string' }),
defineField({ name: 'body', type: 'portableText' }),
],
});
在我們的CMS開發頁面了解更多我們如何構建無頭CMS專案。
翻譯工作流程自動化
手動翻譯無法規模化到超過3種語言。句號。在8種語言下,單一部落格文章會產生7個翻譯工作——如果你的內容團隊每週發佈4篇文章,那就是每週28篇翻譯。數字變得很難看。
機器翻譯作為初稿
2026年真正能堅持的方法:用AI/機器翻譯做初稿,然後讓人工翻譯者潤色。DeepL Pro($25/月)和Google Cloud Translation V3為歐洲語言提供85-92%的準確度,雖然CJK的準確度明顯下降。
// scripts/auto-translate.ts
import * as deepl from 'deepl-node';
const translator = new deepl.Translator(process.env.DEEPL_API_KEY);
async function translateContent(
text: string,
sourceLang: deepl.SourceLanguageCode,
targetLang: deepl.TargetLanguageCode
): Promise<string> {
const result = await translator.translateText(text, sourceLang, targetLang, {
preserveFormatting: true,
formality: 'more', // 商務適當的語氣
tagHandling: 'html', // 保留HTML/markdown結構
});
return result.text;
}
翻譯管理系統(TMS)
對於企業級工作流程,你會想要一個專用TMS:
- Phrase(前稱Memsource):從$25/月起,整合大多數無頭CMS
- Crowdin:從$40/月起,卓越的開發者體驗搭配GitHub/GitLab同步
- Lokalise:從$120/月起,最佳的Figma整合用於設計到翻譯工作流程
- Transifex:從$150/月起,強大的API優先方法
以下是我們對大多數客戶的著陸工作流程:
- 內容作者在原始語言(通常是英文)發佈
- Webhook觸發TMS中的翻譯工作建立
- 機器翻譯產生初稿
- 人工翻譯者審核並批准
- 已批准的翻譯透過API推回CMS
- Webhook觸發受影響頁面的重建/重驗證
這是很多活動部分——我不會假裝不是。但一旦接線完成,內容團隊基本上察覺不到下面的機制。他們只是寫和發佈。
多語言網站的SEO
Hreflang實現
Hreflang標籤告訴搜尋引擎哪個語言版本應在哪個市場提供。搞錯這個,Google就會向法國使用者顯示德文頁面。我們曾和客戶有過這個對話。不好玩。
每個頁面都需要hreflang標註指向所有語言變體:
<!-- 在/fr/about上 -->
<link rel="alternate" hreflang="en" href="https://example.com/about" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr/about" />
<link rel="alternate" hreflang="de" href="https://example.com/de/about" />
<link rel="alternate" hreflang="ja" href="https://example.com/ja/about" />
<link rel="alternate" hreflang="ar" href="https://example.com/ar/about" />
<link rel="alternate" hreflang="x-default" href="https://example.com/about" />
x-default標籤很關鍵——它告訴搜尋引擎當沒有語言區域相符時顯示哪個版本。不要跳過它。
規模上自動化是強制的。 200頁×8種語言,你在管理1,600頁,每個都需要9個hreflang標籤(8種語言+x-default)。那是14,400個hreflang標註。你不會手動做這個。以程式方式生成它們:
// lib/generateHreflang.ts
export function generateHreflangTags(
path: string,
currentLocale: string,
locales: string[],
baseUrl: string
) {
return locales.map((locale) => ({
rel: 'alternate',
hreflang: locale,
href: `${baseUrl}${locale === 'en' ? '' : `/${locale}`}${path}`,
})).concat({
rel: 'alternate',
hreflang: 'x-default',
href: `${baseUrl}${path}`,
});
}
多語言站點地圖
對於5種或更多語言的網站,使用站點地圖索引檔案指向按語言區域的站點地圖:
<!-- sitemap-index.xml -->
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap><loc>https://example.com/sitemap-en.xml</loc></sitemap>
<sitemap><loc>https://example.com/sitemap-fr.xml</loc></sitemap>
<sitemap><loc>https://example.com/sitemap-de.xml</loc></sitemap>
<!-- ... -->
</sitemapindex>
每個語言區域站點地圖應包含xhtml:link元素用於hreflang交叉參考。Google的John Mueller已確認這是最可靠的hreflang實現方法。就這樣做。
跨語言區域的效能優化
翻譯套件分割
不要把所有語言區域字符串運給每個使用者。有2,000個翻譯鍵每個語言區域的典型8語言網站產生~400KB未壓縮JSON。只加載活躍語言區域需要的:
// 動態加載翻譯
const messages = await import(`@/messages/${locale}.json`);
搭配Next.js 15和next-intl,這自動發生於伺服器元件——翻譯字符串在伺服器端渲染且永遠不會作為JavaScript運給客戶端。問題解決。
CJK語言的字體加載
中文、日文和韓文字體巨大。Noto Sans JP完整字符覆蓋是5.7MB。如果你不小心的話那會完全毀掉你的Core Web Vitals。以下是有效的方法:
- 使用
unicode-range子集化:只加載各頁使用的字符 - Google Fonts搭配
display=swap:CJK自動子集化 - 可變字體(如可用):單一檔案,多種粗細
/* 只為日文語言區域加載日文字體 */
@font-face {
font-family: 'NotoSansJP';
src: url('/fonts/NotoSansJP-subset.woff2') format('woff2');
unicode-range: U+3000-9FFF, U+F900-FAFF; /* CJK子集 */
font-display: swap;
}
CDN和邊緣快取
配置你的CDN以按語言區域快取。在Vercel上,這自動發生[locale]區段。在Cloudflare上:
Cache-Key: ${URI}-${Accept-Language}
Vary: Accept-Language
但要小心Vary: Accept-Language——它能以醜陋的方式分割你的快取。更好的做法是使用明確的語言區域URL路徑(子目錄)所以每個語言區域得到自己的乾淨快取項目而無需基於標頭的變化。還是另一個子目錄勝出的理由。
從右到左(RTL)語言支援
如果你的5種或更多語言中有任何一個包含阿拉伯文、希伯來文、波斯文或烏爾都文,RTL支援不是可選的。它影響一切:
- 文件方向:
<html dir="rtl"> - CSS版面:Flexbox和Grid自動處理方向。
margin-left不會——改用邏輯屬性。 - 圖示:方向性圖示(箭頭、導航chevrons)需要鏡像
/* 使用CSS邏輯屬性——對LTR和RTL都有效 */
.card {
margin-inline-start: 1rem; /* 替代margin-left */
padding-inline-end: 2rem; /* 替代padding-right */
border-inline-start: 3px solid blue; /* 替代border-left */
}
Tailwind CSS 3.4+開箱即用支援RTL變體:
<div class="ml-4 rtl:mr-4 rtl:ml-0">
<!-- 或更好的做法,使用邏輯實用程式 -->
<div class="ms-4"> <!-- margin-inline-start -->
用偽本地化測試RTL版面,在實際阿拉伯文翻譯到達前。pseudolocalize之類的工具能鏡像你的英文文字以早期暴露版面問題——遠在它成為客戶QA期間尷尬對話前。問我怎麼知道。
多語言網站的測試和QA
自動化測試策略
// e2e/multilingual.spec.ts (Playwright)
import { test, expect } from '@playwright/test';
const locales = ['en', 'fr', 'de', 'ja', 'ar', 'pt-br', 'es', 'ko'];
for (const locale of locales) {
test(`首頁在${locale}正確加載`, async ({ page }) => {
await page.goto(`/${locale}`);
// 驗證HTML lang屬性
const lang = await page.getAttribute('html', 'lang');
expect(lang).toBe(locale);
// 驗證hreflang標籤對所有語言區域存在
for (const l of locales) {
const hreflang = page.locator(`link[hreflang="${l}"]`);
await expect(hreflang).toHaveCount(1);
}
// 驗證x-default存在
await expect(page.locator('link[hreflang="x-default"]')).toHaveCount(1);
// 驗證無未翻譯字符串(英文出現在非EN頁面)
if (locale !== 'en') {
const h1 = await page.textContent('h1');
expect(h1).not.toBe('Welcome'); // 英文備選偵測
}
});
}
視覺回歸測試
德文文本平均比英文長30-40%。日文可能較短但需要不同的行高。使用Percy或Chromatic跨語言區域捕捉版面崩壞——為每個支援的語言在桌面和行動中斷點設定快照。
對多語言測試基礎架構的投資在第二次內容更新後就能回本,該更新會無聲地破壞三個語言區域。總會有第二次內容更新。總會。
聽著,如果這一切聽起來像很多協調——確實是。但這是我們定期進行的工程工作。聯繫我們討論你的多語言專案,或查看我們的定價以取得估計。
常見問題
構建具有5種或更多語言的多語言網站要花多少成本? 針對無頭設定(Next.js或Astro+無頭CMS),根據頁面計數和複雜度,預期初始構建$30,000-$80,000。除此之外,預算每月$500-$2,000的翻譯管理工具,以及專業人工翻譯的持續成本為每字$0.08-$0.20。機器翻譯搭配人工審核能將按字成本降低40-60%。
我應該使用翻譯外掛或構建自訂i18n? 對於3種或更少語言的WordPress網站,WPML($79/年)或Polylang之類的外掛就不錯了。但超過5種語言,基於外掛的翻譯在單體CMS的開銷變得難以管理。無頭CMS搭配專用TMS整合是可擴展路徑——CMS處理內容建模,TMS處理工作流程,你的前端框架處理路由和渲染。關注點乾淨分離。
最佳的多語言網站無頭CMS是什麼? 完全取決於你在優化什麼。Storyblok具有最拋光的內建多語言編輯體驗搭配視覺編輯器和欄位層級本地化。Sanity透過文件層級本地化和自訂工作流程給你最大靈活性——當你的內容模型變複雜時很理想。Contentful是最安全的企業選擇搭配強大的TMS整合,但要注意定價——每個語言區域計入項目限制。沒有普遍答案。
如何為多語言網站處理SEO?
三項不可協商的要求:正確的hreflang標籤在每個頁面上指向所有語言變體,按語言區域的XML站點地圖搭配交叉參考,以及指向你的規範/預設語言版本的x-default hreflang。使用子目錄URL結構(/fr/、/de/)用於整合域名權威性。在Google Search Console和Bing Webmaster Tools中提交按語言區域的站點地圖。並在前三個月每週監控按語言區域的索引——你會及早捕捉問題而不是在有機流量下滑時發現。
我可以使用Google翻譯或AI來翻譯我的網站嗎? 沒有人工審核,不能作為生產翻譯。Google Cloud Translation V3和DeepL對歐洲語言對達到85-92%準確度,對CJK語言降至70-80%。確實有效的工作流程:機器翻譯用於初稿,人工翻譯者審核和改正,然後發佈。這個混合方法將翻譯成本降低40-60%同時維持品質。永遠不要在沒有專家人工審核的情況下自動翻譯法律、醫療或財務內容。就是別。
如何處理不同語言的URL slug?
翻譯的URL slug(/fr/a-propos而不是/fr/about)改善SEO和使用者體驗但增加真正複雜度。你需要CMS中的slug映射表和路由期間的雙向查詢。對於5種或更多語言,我們建議對頂級頁面和關鍵登陸頁面使用翻譯slug,但保持部落格文章slug為原始語言或使用音譯版本。維護數百個跨十幾個語言區域的翻譯URL是一個快速增長的負擔。
支援許多語言的效能影響是什麼? 有適當架構的話?接近零。靜態網站生成搭配Astro或Next.js為獨立HTML頁面預渲染每個語言區域——伺服器和CDN提供法文頁面的速度就像英文一樣快。主要效能風險是一次加載所有語言區域翻譯套件(透過按語言區域代碼分割解決)、CJK字體加載(透過子集化解決)以及CDN層的快取分割(透過基於URL的語言區域路由而不是基於標頭的解決)。
添加新語言到現有多語言網站需要多長時間? 有適當架構到位的話,添加第9種語言到8語言網站需要1-2天的工程工作:將語言區域添加到路由配置,在CMS中建立語言區域/維度,為新語言配置TMS,並更新hreflang生成。瓶頸總是內容翻譯,不是工程。50頁網站有200個翻譯鍵大約需要2-3週用於專業翻譯和審核。