avatar

mdo

Hello

  • 首页
  • 知识库
  • 归档
  • 标签
  • 关于
主页 忍了一年多,我终于对i18n下手了过去一年,我主要参与国际机票业务的开发工作,因此每天都要和多语言(i18n)打交道
文章

忍了一年多,我终于对i18n下手了过去一年,我主要参与国际机票业务的开发工作,因此每天都要和多语言(i18n)打交道

发表于 12天前 更新于 12天前
作者 mdo
16~21 分钟 阅读

前言

大家好,我是奈德丽。

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

痛点背景

先说说我们在机票项目中遇到的困境。

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

  • 一个 Excel 文件,H5 和 PC 各自占一个 sheet 页;
  • 每次更新语言,需要先导出 Excel,然后手动跑脚本生成语言文件,再拷贝到项目中。

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

  1. Key 命名混乱

    • 有的首字母大写,有的小驼峰、大驼峰混用;
    • 没有统一规则,难以模块化管理。
  2. 不支持模块化

    • 目前已有数千条 key;
    • 查找、修改、维护都非常痛苦。
  3. 更新流程繁琐

    • 需要手动进入脚本目录,用 node 跑脚本;
    • 生成后再手动复制到项目中。

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

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

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

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

新项目,新机会

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

目标很简单:

  1. 统一 key 规则,支持模块化,模块与内容间用.隔开,内容之间用下划线隔开;
  2. 自动化生成多语言 JSON 文件,集成到项目内,不再需要查找转化脚本的位置;
  3. 一条命令搞定更新,不需要手动拷贝。

于是,我在项目中新增了一个 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',
}


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 命名管理 | 无统一规则 | 模块化、规范化 |

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

总结

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

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

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

知识库
国际机票 天都 多语
许可协议:  CC BY 4.0
分享

相关文章

10月 7, 2026

比尔盖茨的智慧:想多赚钱,就每天循环做这3件事

我们总有一种非常固执的错觉,认为那些站在财富金字塔顶端的人,必然拥有某种超越常人的特异功能。我们在脑海中给比尔盖茨这样的大佬描绘了一幅极其悲壮的奋斗画像。大家总觉得,他之所以能富可敌国,肯定是因为他每天只睡三个小时,同时对着八个电脑屏幕疯狂敲击键盘,每一秒钟都在做出价值几亿美金的生死抉择。 为了模仿

9月 28, 2026

程序员越想创业,越不要急着动手

一个能源方面的前辈找到我,希望通过我把一些人工的工作 AI 自动化。 能源方面我不懂,找 Gemini 聊完发现这个是可以复制的,非常兴奋。我跟老婆说,这个项目做好以后可以做成平台,推广到其他公司,你就等着做总裁夫人吧! 她听完以后跟我说,这个项目还是太定制化,和我之前做的一个项目很像。 那个项目一

9月 25, 2026

忍了一年多,我终于对i18n下手了过去一年,我主要参与国际机票业务的开发工作,因此每天都要和多语言(i18n)打交道

前言 大家好,我是奈德丽。 过去一年,我主要参与国际机票业务的开发工作,因此每天都要和多语言(i18n)打交道。熟悉我的朋友都知道,我这个人比较“惜力”(并不是,实际上只是忍不下去了),对于重复笨拙的工作非常抵触,于是,我开始思考如何优化团队的多语言管理模式。 痛点背景 先说说我们在机票项目中遇到的

下一篇

一款免费易用且打造的全功能媒体播放器

上一篇

中秋给在外游子的一封信

最近更新

  • 比尔盖茨的智慧:想多赚钱,就每天循环做这3件事
  • iOS 侧载(Sideloading)工具
  • 程序员越想创业,越不要急着动手
  • 注册 Chat Participant
  • 中秋给在外游子的一封信

热门标签

API CodeGeex Coding Cursor DeepSeek Docker Gitkraken Harness Laravel Management

目录

©2026 mdo. 保留部分权利。