大家好,我是奈德麗。
過去一年,我主要參與國際機票業務的開發工作,因此每天都要和多語言(i18n)打交道。熟悉我的朋友都知道,我這個人比較「惜力」(並不是,實際上只是忍不下去了),對於重複笨拙的工作非常抵觸,於是,我開始思考如何優化團隊的多語言管理模式。
先說說我們在機票專案中遇到的困境。
目前機票專案分為 H5 和 PC 兩端,團隊在維護多語言時主要透過線上 Excel進行管理:
聽起來還算湊合,但隨著專案規模的擴大,問題逐漸顯現:
Key 命名混亂
不支援模組化
更新流程繁瑣
node
跑腳本;下面是一個實際的 Excel 片段,可以感受一下當時的混亂程度:
用原node腳本生成的語言檔如圖
在這樣的場景下,每次迭代多語言檔更新都像噩夢一樣。
尤其是我們很多翻譯是透過AI 機翻生成,後續頻繁修改的成本極高。
然而,機票專案的程式碼量太大、歷史包袱太重,短期內幾乎不可能徹底改造。
機票專案雖然不能動,但在我們啟動飯店業務新專案時,我決定不能再重蹈覆轍。
因此,在飯店專案中,我從零搭建了這套更高效的 i18n 管理方案。
目標很簡單:
於是,我在專案中新增了一個 scripts
目錄,並編寫了一個 excel-to-json.js
腳本。
在 package.json
中添加如下命令:
{
"scripts": {
"i18n:excel-to-json": "node scripts/excel-to-json.js"
}
}
以後,只需要運行下面一行命令,就能完成所有工作:
pnpm i18n:excel-to-json
再也不用手動尋找腳本路徑,也不用手動複製粘貼,效率直接起飛 🚀。
核心邏輯就是:
從 Excel 讀取內容 → 轉換為 JSON → 輸出到專案 i18n 目錄。
完整程式碼如下:
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import XLSX from 'xlsx'
/**
* 語言映射表:Excel 表頭 -> 標準語言碼
*/
const languageMap = {
'English': 'en',
'簡中': 'zh-CN',
'Chinese (Traditional)': 'zh-TW',
'Korean': 'ko',
'Spanish': 'es',
'German Edited': 'de',
'Italian': 'it',
'Norwegian': 'no',
'French': 'fr',
'Arabic': 'ar',
'Thailandese': 'th',
'Malay': 'ms',
}
// 讀取 Excel 檔案
function readExcel(filePath) {
if (!fs.existsSync(filePath)) {
throw new Error(`❌ Excel 檔案未找到: ${filePath}`)
}
const workbook = XLSX.readFile(filePath)
const sheet = workbook.Sheets[workbook.SheetNames[0]]
return XLSX.utils.sheet_to_json(sheet)
}
/**
* 清空輸出目錄
*/
function clearOutputDir(dirPath) {
if (fs.existsSync(dirPath)) {
fs.readdirSync(dirPath).forEach(file => fs.unlinkSync(path.join(dirPath, file)))
console.log(`🧹 已清空目錄: ${dirPath}`)
} else {
fs.mkdirSync(dirPath, { recursive: true })
console.log(`📂 創建目錄: ${dirPath}`)
}
}
/**
* 生成 JSON 檔
*/
function generateLocales(rows, outputDir) {
const locales = {}
rows.forEach(row => {
const key = row.Key
if (!key) return
// 遍歷語言列
Object.entries(languageMap).forEach(([columnName, langCode]) => {
if (!locales[langCode]) locales[langCode] = {}
const value = row[columnName] || ''
const keys = key.split('.')
let current = locales[langCode]
keys.forEach((k, idx) => {
if (idx === keys.length - 1) {
current[k] = value
} else {
current[k] = current[k] || {}
current = current[k]
}
})
})
})
// 輸出檔
Object.entries(locales).forEach(([lang, data]) => {
const filePath = path.join(outputDir, `${lang}.json`)
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8')
console.log(`✅ 生成檔: ${filePath}`)
})
}
/**
* 檢測缺失翻譯
*/
function detectMissingTranslations(rows) {
const missing = []
rows.forEach(row => {
const key = row.Key
if (!key) return
Object.entries(languageMap).forEach(([columnName, langCode]) => {
const value = row[columnName]
if (!value?.trim()) {
missing.push({ key, lang: langCode })
}
})
})
return missing
}
function logMissingTranslations(missingList) {
if (missingList.length === 0) {
console.log('\n🎉 所有 key 的翻譯完整!')
return
}
console.warn('\n⚠️ 以下 key 缺少翻譯:')
missingList.forEach(item => {
console.warn(` - key: "${item.key}" 缺少語言: ${item.lang}`)
})
}
function main() {
const desktopPath = path.join(os.homedir(), 'Desktop', 'hotel多語言.xlsx')
const outputDir = path.resolve('src/i18n/locales')
const rows = readExcel(desktopPath)
clearOutputDir(outputDir)
generateLocales(rows, outputDir)
logMissingTranslations(detectMissingTranslations(rows))
}
main()
這是線上語言原文檔
這是生成後的多語言檔和內容
現在的工作流大幅簡化:
操作 | 舊流程 | 新流程 |
---|---|---|
運行腳本 | 手動找腳本路徑 | pnpm i18n:excel-to-json |
檔生成位置 | 生成後手動拷貝 | 自動輸出到專案 |
檢測缺失翻譯 | 無 | 自動提示 |
key 命名管理 | 無統一規則 | 模組化、規範化 |
這套機制目前在飯店專案中運行良好,團隊反饋也很積極。
這次改造讓我最大的感觸是:
舊專案難以推翻重來,但新專案一定要趁早做好架構設計。
透過這次優化,我們不僅解決了多語言維護的痛點,還提升了團隊整體開發效率。
而這套方案在未來如果機票專案有機會重構,也可以直接平滑遷移過去。