根据产品校验过的 excel , 更新项目国际化
需求
- 产品将校对后的 excel 发回。将 excel 和项目国际化文件对比,相同的 key,使用 excel 的值覆盖,输出新的国际化配置文件。
思路分析
- 产品修改后返回 Excel 文件 ↓
- 读取 Excel,解析修改后的文案 ↓
- 对比原始 JSON,按 key 合并更新 ↓
- 写回新的国际化 JSON 文件
完整代码
js
/**
* 根据产品校验过的 excel , 更新项目国际化配置文件
*
* 1. 校队好的 excel 文件,列顺序必须要和以下配置 languageColumns 顺序对应:Key, zh-TW, en_US, ...
* 2. /locales 项目所有国际化配置 .json 文件
*/
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import XLSX from 'xlsx'
// =======================
// 🔧 配置区(可外部抽离为 config.json 或 .env)
// =======================
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const config = {
// Excel 文件路径
excelFile: path.resolve(__dirname, '2025-09-09_ME学堂国际化.xlsx'),
// 国际化文件目录
localesDir: path.resolve(__dirname, 'locales'),
// Excel 第一行为表头时,按顺序定义语言字段名(对应 JSON 文件名)
// Excel 列顺序:Key, zh-TW, ...
languageColumns: [
'zh-TW', // Excel 第2列
'en_US', // 第3列
'th_TH', // 第4列
'pt_PT', // 第5列
'ko_KR', // 第6列
'tr_TR', // 第7列
'vi_VN', // 第8列
'es_ES' // 第9列
],
// 可选:自定义 JSON 文件名映射(如果文件命名不规范可调整)
// 默认: langCode => langCode.replace('-', '_') + '.json'
// 例如: 'zh-TW' -> 'zh_TW.json',如果你的文件是 zh-TW.json,则无需映射
fileMapping: {
'zh-TW': 'zh-TW.json',
en_US: 'en_US.json',
th_TH: 'th_TH.json',
pt_PT: 'pt_PT.json',
ko_KR: 'ko_KR.json',
tr_TR: 'tr_TR.json',
vi_VN: 'vi_VN.json',
es_ES: 'es_ES.json'
}
}
// =======================
// 读取 Excel 数据
// =======================
async function readExcelData(filePath) {
try {
const workbook = XLSX.readFile(filePath)
const sheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[sheetName]
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: '' })
const rows = data.length > 1 ? data.slice(1) : []
console.log(`✅ 成功读取 Excel,共 ${rows.length} 行数据`)
return rows
} catch (err) {
console.error('❌ 读取 Excel 文件失败:', err.message)
process.exit(1)
}
}
// =======================
// 构建翻译映射表
// =======================
function buildTranslationMap(excelData, languageColumns) {
const map = {}
excelData.forEach(row => {
const key = row[0] // 第一列是 key
if (!key) return
const translations = {}
languageColumns.forEach((langCode, index) => {
const value = row[index + 1] // Excel 第2列开始对应 languageColumns
translations[langCode] = String(value || key) // 空值 fallback 到 key
})
map[key] = translations
})
return map
}
// =======================
// 获取 JSON 文件路径
// =======================
function getJsonFilePath(langCode, baseDir, fileMapping) {
// 优先使用自定义映射
if (fileMapping[langCode]) {
return path.join(baseDir, fileMapping[langCode])
}
// 默认规则:zh-TW -> zh_TW.json
const filename = langCode.replace('-', '_') + '.json'
return path.join(baseDir, filename)
}
// =======================
// 更新单个 JSON 文件
// =======================
async function updateJsonFile(filePath, langCode, translationMap) {
try {
const rawData = await fs.promises.readFile(filePath, 'utf8')
const data = JSON.parse(rawData)
let updated = false
for (const key in data) {
if (translationMap[key] && translationMap[key][langCode] !== undefined) {
const newValue = translationMap[key][langCode]
if (data[key] !== newValue) {
data[key] = newValue
updated = true
}
}
}
if (updated) {
await fs.promises.writeFile(
filePath,
JSON.stringify(data, null, 2),
'utf8'
)
console.log(`✅ 已更新: ${filePath}`)
} else {
console.log(`🔸 无需更新: ${filePath}`)
}
} catch (err) {
if (err.code === 'ENOENT') {
console.warn(`🟡 文件不存在,跳过: ${filePath}`)
} else {
console.error(`❌ 更新失败: ${filePath}`, err.message)
}
}
}
// =======================
// 主流程
// =======================
async function main() {
console.log('🔄 开始更新国际化文件...')
// 1. 读取 Excel
const excelData = await readExcelData(config.excelFile)
// 2. 构建翻译映射
const translationMap = buildTranslationMap(excelData, config.languageColumns)
// 3. 批量更新每个语言文件
for (const langCode of config.languageColumns) {
const filePath = getJsonFilePath(
langCode,
config.localesDir,
config.fileMapping
)
await updateJsonFile(filePath, langCode, translationMap)
}
console.log('🎉 国际化文件更新完成!')
}
// 执行
main().catch(console.error)