工程日記・第十八天:10種語言、224條航線、以及 isZh 大清洗——48小時全球化
兩天。153個文件變更。27,000+行代碼。我們把 VOLO 從一個附帶法語和西班牙語的雙語產品,變成了一個真正服務10種語言的全球化平臺——但這不只是'添加更多 JSON 文件'這麼簡單。我們必須消滅嵌入在81個頁面文件中的 isZh 反模式,重建 i18n 基礎設施,將224條航線翻譯成10種語言(2,240個城市名稱對),優化 Core Web Vitals,添加 Agent 模式國際化,修復每個彈窗和覆蓋層的無障礙問題,以及清理 Google Search Console 覆蓋率問題。這是 VOLO 歷史上最大規模重構的故事。
問題:爲什麼10種語言不是"加幾個 JSON 文件"那麼簡單
VOLO 最初以英文和中文兩種語言發佈,後來又輕量級地加上了法語和西班牙語。i18n 層使用了 next-intl,這是正確的工具選擇。但實現中有一個致命的捷徑嵌入在數十個頁面文件中:isZh 模式。
頁面沒有使用 useTranslations() 或 getTranslations() 來獲取本地化字符串,而是這樣做的:
const isZh = locale === 'zh';
// 散佈在每個渲染函數中:
<h2>{isZh ? '我們的機隊' : 'Our Fleet'}</h2>
<p>{isZh ? '探索200+飛機' : 'Explore 200+ aircraft'}</p>
這對2種語言有效。對10種語言則完全崩潰。這個反模式嵌入在代碼庫的81個文件中:機隊頁面、目的地頁面、航線頁面、洞察頁面、運營商頁面、機場頁面、博客頁面、案例研究頁面、預訂組件和營銷組件。
從4種語言擴展到10種語言,意味着我們必須先消滅 isZh——在所有地方,一次性完成。
第一步:基礎設施——10語言管道
在觸碰任何頁面文件之前,我們先打好了地基:
路由與配置
- i18n 配置:添加 ja、ko、ar、de、pt、ru 到語言數組。更新語言顯示名稱、文字方向(阿拉伯語 RTL)和日期格式偏好。
- 路由:更新所有10種語言的路徑模式。
- 中間件:爲6種新語言添加 Accept-Language 檢測,讓日本訪客訪問
/時自動跳轉到/ja。 - 語言切換器:重新設計以處理10個選項——國旗、母語腳本標籤、按地區分組。
- 內容類型:擴展
MultilingualText以支持跨航線、目的地、機隊、服務等的10語言字符串對象。
6個新翻譯文件
每個語言 JSON 有約1,131個鍵,涵蓋26+個命名空間。我們爲日語、韓語、阿拉伯語、德語、葡萄牙語和俄語創建了完整翻譯——不是機器翻譯的佔位符,而是尊重每種語言慣例的本地化字符串。
阿拉伯語需要特別注意:RTL 文字方向、正確的數字格式,以及沒有直接阿拉伯語對應詞的航空術語的文化適應。
提交:6eb909a — 61個文件,+5,504/-198行。當時 VOLO 歷史上最大的單次提交。
第二步:isZh 大清洗——81個文件,一個反模式
這是最乏味但最重要的步驟。每一個 isZh 模式都必須替換爲適當的 t('namespace.key') 調用。重構涉及:
| 類別 | 重構文件數 |
|---|---|
| 機隊頁面和組件 | 12個文件 |
| 洞察頁面 | 18個文件 |
| 航線頁面和組件 | 5個文件 |
| 目的地頁面和組件 | 5個文件 |
| 機場和 FBO 頁面 | 5個文件 |
| 博客和案例研究頁面 | 6個文件 |
| 運營商頁面 | 4個文件 |
| 預訂和營銷組件 | 8個文件 |
| 空腿航班頁面 | 3個文件 |
| 工具和體驗頁面 | 5個文件 |
| 翻譯文件(10種語言) | 10個文件 |
每次替換的模式都是相同的:
// 之前(isZh 反模式):
const isZh = locale === 'zh';
<h2>{isZh ? '詳細規格' : 'Detailed Specifications'}</h2>
// 之後(next-intl):
const t = await getTranslations('fleet');
<h2>{t('detailedSpecs')}</h2>
總計:81個文件變更,+5,130/-2,222行。我們添加了約300個新翻譯鍵來支持之前硬編碼的字符串。提交:daf0119。
第三步:224條航線 × 10種語言
航線的 i18n 有獨特挑戰。航線數據文件包含需要用每種語言的母語腳本渲染的城市名稱對:
- 英語:New York → London
- 中文:紐約 → 倫敦
- 日語:ニューヨーク → ロンドン
- 韓語:뉴욕 → 런던
- 阿拉伯語:نيويورك → لندن
- 俄語:Нью-Йорк → Лондон
這不僅是翻譯——是音譯。日語使用片假名,韓語使用韓文字母,俄語使用西裏爾字母,阿拉伯語使用阿拉伯文字。224條航線中的每條都有出發地和目的地城市,每個城市名需要10種語言變體。
我們更新了所有7個航線數據文件,爲每條航線包含帶本地化城市名的 MultilingualText 對象。即 224條航線 × 2個城市 × 10種語言 = 4,480個本地化城市名。
提交:01b8d4c — 7個文件,+5,757/-607行。
第四步:Core Web Vitals 優化
在 i18n 工作進行的同時,我們也解決了 Lighthouse 和真實用戶指標標記的性能問題:
- Hero LCP:爲 Hero 主內容添加
fetchPriority="high"。React 19 原生支持fetchPriority——不需要@ts-expect-error。 - 延遲加載:首屏以下的非關鍵組件(GlobalMapSection、EmptyLegsTeaser、案例研究卡片)現在在初次繪製後加載。
- 自適應預加載器:VOLO 預加載器現在檢測連接速度,在慢連接上跳過動畫。
- 服務端地圖數據:GlobalMapSection 之前在客戶端獲取航線座標。我們將數據計算移到服務端組件,消除了佈局偏移。
在同一個過程中,我們將航線目錄從約100條擴展到224條,包含完整的雙語描述(中英文)、真實 ICAO 代碼和市場化定價。
提交:cf8256d — 11個文件,+8,258/-68行。
第五步:Agent 模式 i18n + 無障礙打磨
面向 AI Agent 的 UI(e2b.dev 風格設計)之前完全是英文的。有了10種語言支持後,我們國際化了每個 Agent 組件,添加了95+個新 i18n 鍵。
無障礙修復
- QuickQuoteModal:添加了
role="dialog"、aria-modal="true"、焦點陷阱和 Escape 鍵處理。 - ExitIntentPopup:相同的模態框無障礙模式,加上
aria-live="polite"。 - AirportAutocomplete:添加了
role="listbox"、aria-activedescendant、方向鍵導航和屏幕閱讀器結果數量播報。 - Header:修復了移動菜單、浮動 CTA 和彈窗之間的 z-index 堆疊衝突。
提交:3f57ec7 — 28個文件,+1,451/-265行。
第六步:Google Search Console 清理
- robots.txt:移除了對 OpenGraph 和 Twitter 圖片路由的過度封鎖。
- 聯繫頁 canonical:爲 GSC 標記爲重複的聯繫頁添加了專用 canonical URL。
- 雙重 locale 重定向:爲
/en/en/...類 URL 添加了 Next.js 重定向規則。
核心數據
| 指標 | 之前 | 之後 |
|---|---|---|
| 支持語言 | 4 (en, zh, fr, es) | 10 (+ ja, ko, ar, de, pt, ru) |
| 每語言翻譯鍵 | ~739 | ~1,131 |
| 總翻譯鍵 | ~2,956 | ~11,310 |
| 航線數 | ~100 | 224 |
| 本地化城市名對 | ~400 | 4,480 |
| 頁面 × 語言 | 86 × 4 = 344 | 86 × 10 = 860 |
| isZh 出現次數 | 200+ | 0 |
| 總變更文件 | — | 153 |
| 新增行數 | — | 27,184 |
| 刪除行數 | — | 2,705 |
提交記錄
兩天內的8次提交:
6eb909a— i18n: 從4種擴展到10種語言(61個文件,+5,504/-198)a2e076c— i18n: 完成 ja、ko、de、pt、ru 翻譯03c8829— i18n: 完成阿拉伯語全部30個命名空間翻譯cf8256d— perf: Core Web Vitals 優化 + 航線擴展到224條(+8,258/-68)01b8d4c— i18n: 爲224條航線添加8種語言城市名翻譯(+5,757/-607)daf0119— i18n: 全面重構——移除 isZh 反模式(81個文件,+5,130/-2,222)3d5c681— fix: 解決 Google Search Console 覆蓋率驗證問題3f57ec7— ui/ux: 全面打磨——i18n、無障礙、視覺改進(28個文件,+1,451/-265)
經驗教訓
- 儘早消滅反模式。isZh 快捷方式在只有2種語言時省了10分鐘,在需要10種語言時花了我們幾個小時。每個硬編碼的三元表達式都是一種帶複利的技術債。
- 城市名比 UI 字符串難。翻譯按鈕標籤很直接。把"聖保羅"音譯成日語片假名(サンパウロ)需要翻譯 API 有時會出錯的文化知識。
- RTL 不只是
dir="rtl"。阿拉伯語支持需要審查網站上每個 flex 容器、padding/margin 方向和文本對齊。 - React 19 + next-intl v4 是好的技術棧。服務端組件使用
getTranslations()意味着靜態翻譯字符串的 JavaScript 包大小爲零。
下一步
- 翻譯質量審計——讓母語使用者審查翻譯
- 每語言獨立 SEO 元描述
- RTL 佈局測試——對阿拉伯語進行全面視覺 QA
- Hreflang 驗證——確保10種語言替代鏈接正確
- 內容本地化——博客文章、案例研究、洞察報告多語言版本
準備好飛行了嗎?幾秒鐘獲取個性化包機報價。
訂閱資訊
空腿航班優惠、新航線與航空洞察,直達您的郵箱。