稀土掘金技术社区 前天 09:55
优化多语言管理:从 Excel 到自动化 JSON 生成
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文作者在参与国际机票业务开发时,因忍受不了繁琐的多语言管理流程,为新启动的酒店项目设计了一套全新的 i18n 管理方案。该方案解决了机票项目在 Key 命名混乱、不支持模块化、更新流程繁琐等痛点。新方案通过自动化脚本,将 Excel 文件直接转换为项目所需的 JSON 语言文件,并集成了到项目内,实现“一条命令搞定”的更新流程,极大地提升了开发效率。作者分享了脚本的核心逻辑和完整代码,并展示了改造前后的工作流对比,强调了新项目早期进行架构设计的重要性。

💡 **痛点与背景分析**:在国际机票业务中,多语言管理主要依赖 Excel,面临 Key 命名混乱、缺乏模块化、更新流程繁琐等问题。数千条 Key 的维护工作量巨大,尤其是在 AI 辅助翻译后,频繁修改成本高昂。由于机票项目历史包袱重,短期内难以彻底改造。

🚀 **新方案的设计目标与实现**:针对酒店新项目,作者搭建了一套高效的 i18n 管理方案,核心目标是统一 Key 规则(模块.内容_内容)、实现多语言 JSON 文件的自动化生成并集成到项目内,以及通过一条命令完成更新,彻底告别手动查找脚本和复制粘贴。通过编写 `excel-to-json.js` 脚本并配置 `package.json`,实现了 `pnpm i18n:excel-to-json` 一键操作。

⚙️ **核心脚本逻辑与成果展示**:脚本核心流程为“读取 Excel 内容 → 转换为 JSON → 输出到项目 i18n 目录”,提供了完整的 Node.js 代码实现。改造后的工作流大幅简化,操作效率显著提升,实现了自动输出、自动提示缺失翻译、统一 Key 命名和模块化管理,团队反馈积极,为未来机票项目重构提供了平滑迁移的方案。

原创 奈德丽 2025-10-15 08:31 重庆

点击关注公众号,技术干货及时达。

前言大家好,我是奈德丽。

过去一年,我主要参与「国际机票业务」的开发工作,因此每天都要和多语言(i18n)打交道。熟悉我的朋友都知道,我这个人比较“惜力”(并不是,实际上只是忍不下去了),对于「重复笨拙的工作非常抵触」,于是,我开始思考如何优化团队的多语言管理模式。

痛点背景先说说我们在「机票项目」中遇到的困境。

目前机票项目分为 H5 和 PC 两端,团队在维护多语言时主要通过「在线 Excel」进行管理:

一个 Excel 文件,H5 和 PC 各自占一个 sheet 页;

每次更新语言,需要先导出 Excel,然后手动跑脚本生成语言文件,再拷贝到项目中。

听起来还算凑合,但随着项目规模的扩大,问题逐渐显现:

「Key 命名混乱」

有的首字母大写,有的小驼峰、大驼峰混用;

没有统一规则,难以模块化管理。

「不支持模块化」

目前已有「数千条 key」

查找、修改、维护都非常痛苦。

「更新流程繁琐」

需要手动进入脚本目录,用 node 跑脚本;

生成后再「手动复制」到项目中。

下面是一个实际的 Excel 片段,可以感受一下当时的混乱程度:

用原node脚本生成的语言文件如图

在这样的场景下,「每次迭代多语言文件更新都像噩梦一样」尤其是我们很多翻译是通过「AI 机翻」生成,后续频繁修改的成本极高。

然而,机票项目的代码量太大、历史包袱太重,短期内「几乎不可能彻底改造」

新项目,新机会机票项目虽然不能动,但在我们启动「酒店业务新项目」时,我决定不能再重蹈覆辙。因此,在酒店项目中,我从零搭建了这套更高效的 i18n 管理方案。

目标很简单:

「统一 key 规则」,支持模块化,模块与内容间用.隔开,内容之间用下划线隔开;

「自动化生成」多语言 JSON 文件,集成到项目内,不再需要查找转化脚本的位置;

「一条命令搞定」更新,不需要手动拷贝。

于是,我在项目中新增了一个 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, { recursivetrue })
    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, null2), '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 命名管理

无统一规则

模块化、规范化

这套机制目前在「酒店项目」中运行良好,团队反馈也很积极。

总结这次改造让我最大的感触是:

「旧项目难以推翻重来,但新项目一定要趁早做好架构设计。」

通过这次优化,我们不仅解决了多语言维护的痛点,还提升了团队整体开发效率。而这套方案在未来如果机票项目有机会重构,也可以直接平滑迁移过去。

""~

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

多语言管理 i18n 自动化 开发效率 Excel JSON Node.js 脚本 Multilingual Management i18n Automation Development Efficiency Excel JSON Node.js Scripting
相关文章