多语言脚本工具-解放双手

170 阅读5分钟

前情:由于我们公司是做海外市场的,所以避免不了多语言,之前一直都是拿到运营的整理好的多语言,然后复制到对应的语言文件,但是想了想,不能做个粘贴复制的废物,于是写下了这个脚本工具(解释在代码里面),由浅入深, 但是这个功能针对的格式有限

基于Vue3+TS(不过框架语言影响不大哈,其他也都可以用)+Vue-i18n

1. Excel转json (无脑简单版)

我们要做的事,就是把excel里面的数据读取出来,变成一个对象

假设sample.xlsx的数据是这样:

企业微信截图_16643454566217.png

在使用vue-i18n我们要得到的应该是,生成对应的语言文件,然后显示对应的语言

企业微信截图_16643457957844.png

先上代码: package.json

 "scripts": {
    "jsonToExcel": "node src/i18n/jsonToExcel.js --bp",
    "testToExcel": "node src/i18n/testToExcel.js --bp"
  },

npm run xxx 就行啦~

企业微信截图_1664350971120.png

const xlsx = require('node-xlsx') // 读取excel
const fs = require('fs')
const prettier = require('prettier') // json格式化
const colors = require('colors') // 颜色

// 定义最多所存在得语言
const allLanguages = ['zh-Hans', 'zh-Hant', 'en', 'ms'] // 假设最多有4种,简中/繁中/英文/马来

// 1.拿到excel数据
const workSheetsFromBuffer = xlsx.parse(fs.readFileSync(`${__dirname}/../language/sample.xlsx`));

console.log(colors.yellow(`mention1:由sample.xlsx生成多语言ts文件`))
workSheetsFromBuffer.forEach(item => {
  // 2.可能有多个sheet表,选择有数据的那个
  if(item.data.length) {
    // 3. 获取excel中得语言
    const currentLanguage = []
    item.data[0].forEach(item => {
      if (allLanguages.includes(item)) {
        currentLanguage.push(item)
      }  
    })
    // 4. 获取各种语言所有得key
    let eachLangIndex = 0
    // 5. 去掉第一项(这里比较死, 必须按照key -> 语言得顺序)
    const restData = item.data.slice(1)
    currentLanguage.forEach(lang => {
      eachLangIndex = item.data[0].indexOf(lang)
      // 6. 获取到当前得语言,当前的index值, 以便知道获取那一栏的数据
      createTs(lang, eachLangIndex, restData)
    })
  }
})


// 7.根据key创建出对应得ts文件
function createTs(tsFileName, eachLangIndex, data) {
  const obj = {}
  data.forEach(item => {
    obj[item[0]] = item[eachLangIndex]
  })
  const filePath = `${__dirname}/../language/${tsFileName}.ts`
  
  // 8.插入数据到文件里面
  const content = `
    const message = 
      ${JSON.stringify(obj,null,"\t")}
    export default message;
  `
  fs.exists(filePath, (exists) => {
    if (!exists) {
      fs.writeFile(filePath, beautifyJs(content), { 'flag': 'a' }, function(err) {
        if (err) {
          throw err;
        }
        // 写入成功后读取测试
        fs.readFile(filePath, 'utf-8', function(err, data) {
          if (err) {
            throw err;
          }
          console.log(colors.yellow(`mention3:${filePath}文件生成啦`))
        });
      });
    } else {
      console.log(colors.yellow(`mention2:${filePath}已经有相同的文件拉`))
    }   
  })
}

// 格式化代码
function beautifyJs(txt) {
  return prettier.format(txt, {
    parser: "babel-ts",
    printWidth: 80,
    semi: true,
    tabWidth: 2, // 缩进字节数
    useTabs: false, // 缩进不使用tab,使用空格
    singleQuote: false,
    bracketSpacing: true,
    trailingComma: "es5",
  });
}
1.1 首先安装一些包
npm i node-xlsx prettier colors --save

node-xlsx 可以读取xlsx里面的内容,prettier用于将xlsx里面数据插入到ts中,colors在终端可以打印出一些提示等

1.2 读取excel中的数据

参考:node-xlsx, 文档里面都有

我们需要读取的就是sample.xlsx这个文件里面的数据,可以用

const workSheetsFromBuffer = xlsx.parse(fs.readFileSync(`${__dirname}/../language/sample.xlsx`))

进行读取,workSheetsFromBuffer读取出来的结果是什么呢?

企业微信截图_16643472309471.png

也就是说拿到的是一个excel中所有的sheet里面的数据,一般来说我们也只有一个sheet里面有数据(这里比较死哈),所以我做了处理拿到有数据的那个表里面的data数据

1.3 处理成json数据

也许项目中有6种语言,但是针对的不同的活动,可能有的活动只需要做4种语言,因为活动也是针对国家来的,所以要遍历出目前excel中有多少中语言,allLanguages一定是包含了currentLanguage 上面的打印workSheetsFromBuffer可以看到,如果要拿到对应的语言,你得找到语言所在的index的值, ,通过

eachLangIndex = item.data[0].indexOf(lang)

能够拿到每个语言所在的index的值,这样下面的key对应的语言都能拿到

const restData = item.data.slice(1)

为什么这里我需要slice(1),是因为第一项,对我来创建这个ts(json数据)已经没有意义了,我唯一要做的就是把下面的数据,第一项的key和后面的value(借助eachLangIndex)对应起来,所以直接处理下面的,我选择手动删掉他 企业微信截图_16643491854916.png

1.4写入ts文件
fs.writeFile(filePath, beautifyJs(content), { 'flag': 'a' }, function(err) {
    if (err) {
      throw err;
    }
    // 写入成功后读取测试
    fs.readFile(filePath, 'utf-8', function(err, data) {
      if (err) {
        throw err;
      }
      console.log(colors.yellow(`mention3:ts文件生成啦`))
    });
});

以上我仅仅实现了最简单的value为字符串的情况, 对于value为数组和对象等第三个大标题去实现

2. json转excel

我们要做的事,就是把对象用在excel中表示 因为vue3直接引入ts用require或者import不太行,所以这里我直接用json数据,这样就可以require拉

假设sample.json的数据是这样,这是我们常用的格式, 如果超出这个格式,宝,你的层级是不是太多了,或者自己处理吧,小橘子已经不行了

{
  "title": "抢红包活动",
  "gameRule": ["超过18岁", "活动结束数据清零"],
  "personalInfo": {
    "age": 20,
    "name": "小橘子"
  },
  "gameList": [
    {
      "gameTitle": "贪吃蛇",
      "gameDesc": "儿时经典"
    },
    {
      "gameTitle": "超级玛丽",
      "gameDesc": "街机游戏"
    }
  ]
}

下面是我们的目标

image.png

上代码吧

// npm run testToExcel
const fs = require('fs')
const xlsx = require('node-xlsx').default;
// 1.获取json的数据key以及结构
const sampleJson = require('../language/sample')

// 2.假设最多有4种,简中/繁中/英文/马来
const allLanguages = ['zh-Hans', 'zh-Hant', 'en', 'ms'] 

const save = `${__dirname}/../language/sampleJsonToExcel.xlsx`

const initArray = ['key', 'objSonKey', 'arrGrandSonKey', ...allLanguages]
const activeArray = []
// 
for(key in sampleJson) {
  // 只进行两层得判断,判断三种情况: 字符串/number, 数组, 对象
  if (typeof(sampleJson[key]) === 'object') {
    // 如果是数组
    const isArray = sampleJson[key].constructor === Array
    // 如果是对象(非数组)
    const isObject = sampleJson[key].constructor !== Array
    // 1.是数组
    if (isArray) {
      // 1.1 数组的子项是基础类型的值
      if(typeof(sampleJson[key][0]) !== 'object') {
        sampleJson[key].forEach(element => {
          activeArray.push([key])
          // activeArray.push([key, null, null, element])
        });
      }
      // 1.2 数组的子项为对象(对象的子项为基础类型的值)
      if(typeof(sampleJson[key][0]) === 'object'  && sampleJson[key][0].constructor !== Array) {
        sampleJson[key].forEach(element => {
          for(grandSonKey in sampleJson[key][0]) {
            activeArray.push([key, null, grandSonKey])
            // activeArray.push([key, null, grandSonKey, element[grandSonKey]])
          }
        });
      }
    }
    // 2.是对象(对象的子项为基础类型的值)
    if(isObject) {
      for(var sonKey in sampleJson[key]) {
        activeArray.push([key, sonKey])
        // activeArray.push([key, sonKey, null, sampleJson[key][sonKey]])
      }
    }
  } else {
    activeArray.push([key])
    // activeArray.push([key, null, null, sampleJson[key]])
  }
}
const data = [initArray].concat(activeArray)
var buffer = xlsx.build([ {name: "sheet1", data: data} ]); // Returns a buffer
fs.writeFileSync(save, buffer);
2.1 首先安装一些包
npm i node-xlsx prettier colors --save

node-xlsx 可以用来创建xlsx, 主要用到xlsx.build(),colors在终端可以打印出一些提示等

2.2 明确json的value各种情况在excel中如何体现

只进行三层得判断,判断三种情况: 字符串, 数组, 对象

{
  "title": "抢红包活动",
  "gameRule": ["超过18岁", "活动结束数据清零"],
  "personalInfo": {
    "age": 20,
    "name": "小橘子"
  },
  "gameList": [
    {
      "gameTitle": "贪吃蛇",
      "gameDesc": "儿时经典"
    },
    {
      "gameTitle": "超级玛丽",
      "gameDesc": "街机游戏"
    }
  ]
}
 if (typeof(sampleJson[key]) === 'object') {
    // 如果是数组
    const isArray = sampleJson[key].constructor === Array
    // 如果是对象(非数组)
    const isObject = sampleJson[key].constructor !== Array
    // 1.是数组
    if (isArray) {
      // 1.1 数组的子项是基础类型的值
      if(typeof(sampleJson[key][0]) !== 'object') {
        sampleJson[key].forEach(element => {
          activeArray.push([key])
          // activeArray.push([key, null, null, element])
        });
      }
      // 1.2 数组的子项为对象(对象的子项为基础类型的值)
      if(typeof(sampleJson[key][0]) === 'object'  && sampleJson[key][0].constructor !== Array) {
        sampleJson[key].forEach(element => {
          for(grandSonKey in sampleJson[key][0]) {
            activeArray.push([key, null, grandSonKey])
            // activeArray.push([key, null, grandSonKey, element[grandSonKey]])
          }
        });
      }
    }
    // 2.是对象(对象的子项为基础类型的值)
    if(isObject) {
      for(var sonKey in sampleJson[key]) {
        activeArray.push([key, sonKey])
        // activeArray.push([key, sonKey, null, sampleJson[key][sonKey]])
      }
    }
  } else {
    activeArray.push([key])
    // activeArray.push([key, null, null, sampleJson[key]])
  }

1.如果value为数组,并且子项就是基础类型,那么直接在对应语言项进行写入值

2.如果value为数组,并且子项就是对象(对象的子项为基础类型的值),那么对象的key应该写在groundSonKey(因为已经是第三层的key)

3.如果value为对象(对象的子项为基础类型的值),那么key写在第二层sonKey

4.如果value是子项,直接写入值

这里发现,为啥第一个和第二个直接写入值呢,到时候excel转json怎么弄?这里我是采用length是不是大于1来判断,如果length为1,你还用啥数组呢?

2.3 写入
var buffer = xlsx.build([ {name: "sheet1", data: data} ]); 
fs.writeFileSync(save, buffer);
2.4 value如何excel中体现出来?

请把这几行放出来,并且把上面的行注释

activeArray.push([key, null, null, element])
activeArray.push([key, null, grandSonKey, element[grandSonKey]])
activeArray.push([key, sonKey, null, sampleJson[key][sonKey]])
activeArray.push([key, null, null, sampleJson[key]])

企业微信截图_16643550457958.png

json转excel只是为了生成最基础的excel的模板,所以这个value,应该是在excel中去粘贴实现.当然也许也用不上,直接在excel里面去修改key,objSonKey,arrGroundKey也是一样的,看什么样的多语言吧,这里最重要的是为复杂一点的excel文件的反推成json做铺垫吧

3. Excel转json (最终版本)

目标就是把下面的excel转成json

企业微信截图_1664356286993.png

企业微信截图_16643576792699.png

上代码吧

const xlsx = require('node-xlsx') // 读取excel
const fs = require('fs')
const prettier = require('prettier') // json格式化

// 定义最多所存在得语言
const allLanguages = ['zh-Hans', 'zh-Hant', 'en', 'ms'] // 假设最多有4种,简中/繁中/英文/马来

// 1.拿到excel数据
const workSheetsFromBuffer = xlsx.parse(fs.readFileSync(`${__dirname}/../language/sample.xlsx`));
console.log(colors.yellow(`mention1:由sample.xlsx生成多语言ts文件`))

workSheetsFromBuffer.forEach(item => {
  // 2.可能有多个sheet表,选择有数据的那个
  if(item.data.length) {
    // 3. 获取excel中得语言
    const currentLanguage = []
    item.data[0].forEach(item => {
      if (allLanguages.includes(item)) {
        currentLanguage.push(item)
      }  
    })
    // 4. 获取各种语言所有得key
    let eachLangIndex = 0
    // 5. 去掉第一项(这里比较死, 必须按照key -> 语言得顺序)
    const restData = item.data.slice(1)
    currentLanguage.forEach(lang => {
      eachLangIndex = item.data[0].indexOf(lang)
      // 6. 获取到当前得语言,当前的index值, 以便知道获取那一栏的数据
      createTs(lang, eachLangIndex, restData)
    })
  }
})

function handleData(arr, eachLangIndex) {
  // 8.1 先将excel的数据,变成key与children带父子级的关系
  let object = {}
  arr.forEach((item) => {
    let key = item[0]
    if (!object[key]) { 
      object[key] = { 
        key,  
        children: []    
      }   
    }
    object[key].children.push(item) 
  })
  const initArr = Object.values(object)

  let resObj = {}
  initArr.forEach(item => {
    const { children } = item
    const firstChildren = children[0]
    // 8.2对sonkey,grandSonKey项以及length进行判断,来确定是什么类型
    // 8.2.1如果第二项(sonkey),第三项(grandSonKey)不存在,并且children只有一项,那么表示是字符串
    if (!firstChildren[1] && !firstChildren[2] && item.children.length === 1) {
      resObj[item.key] = beautifyText(firstChildren[eachLangIndex])
    }
    // 8.2.2 如果第二项(sonkey),第三项(grandSonKey)不存在,并且children多项,那么表示就是一个数组(子项为基础项)
    if (!firstChildren[1] && !firstChildren[2] && item.children.length > 1) {
      const arr = []
      children.forEach(cSon => {
        arr.push(beautifyText(cSon[eachLangIndex]))
      })
      resObj[item.key] = arr
    }
    // 8.2.3 如果第二项(sonkey)存在,第三项(grandSonKey)不存在,那么表示是一个对象
    if (firstChildren[1]) {
      const obj = {}
      children.forEach(cSon => {
        obj[cSon[1]] = beautifyText(cSon[eachLangIndex]) 
      })
      resObj[item.key] = obj
    }
    // 8.2.4 如果第三项(grandSonKey)存在,那么表示是一个数组(子项为对象)
    if (firstChildren[2]) {
      const items = []
      let keyNumbers = 0
      const arr = []
      children.forEach(cSon => {
        items.push(cSon[2])
      })
      keyNumbers = [...new Set(items)].length
      let obj = {}
      children.forEach((cSon, index) => {
        obj[[cSon[2]]] = beautifyText(cSon[eachLangIndex])
        if(index%keyNumbers) {
          arr.push(obj)
          obj = {}
        }
      })
      resObj[item.key] = arr
    }
  })
  return resObj
}

// 7.根据key创建出对应得ts文件
function createTs(tsFileName, eachLangIndex, data) {
  // 8.处理数据
  const obj = handleData(data, eachLangIndex)
  const filePath = `${__dirname}/../language/${tsFileName}.ts`
  
  // 9.插入数据到文件里面
  const content = `
    const message = 
      ${JSON.stringify(obj,null,"\t")}
    export default message;
  `
  fs.exists(filePath, (exists) => {
    if (!exists) {
      fs.writeFile(filePath, beautifyJs(content), { 'flag': 'a' }, function(err) {
        if (err) {
          throw err;
        }
        // 写入成功后读取测试
        fs.readFile(filePath, 'utf-8', function(err, data) {
          if (err) {
            throw err;
          }
          console.log(colors.yellow(`mention3:${filePath}文件生成啦`))
        });
      });
    } else {
      console.log(colors.yellow(`mention2:${filePath}已经有相同的文件拉`))
    } 
  })
}

// 格式化代码
function beautifyJs(txt) {
  return prettier.format(txt, {
    parser: "babel-ts",
    printWidth: 80,
    semi: true,
    tabWidth: 2, // 缩进字节数
    useTabs: false, // 缩进不使用tab,使用空格
    singleQuote: false,
    bracketSpacing: true,
    trailingComma: "es5",
  });
}

// 去掉首尾的换行和空格
function beautifyText(str) {
  return String(str).replace(/^\s+|\s+$/g,'')
}

3.1 代码解析

Excel转json (无脑简单版)相比,我只加上了一个beautifyText()以及handleData方法

beautifyText主要是excel里面得复制也许前后会有空格或者换行,可以去掉 handleData主要是excel中返回得数组,处理成json 因为excel获取有很多项,所以先进行key一致得一个合并

  let object = {}
  arr.forEach((item) => {
    let key = item[0]
    if (!object[key]) { 
      object[key] = { 
        key,  
        children: []    
      }   
    }
    object[key].children.push(item) 
  })
  const initArr = Object.values(object)

主要是将

const arr =  [
    [ 'title',       null, null, '抢红包活动'],
    [ 'gameRule',    null, null, '超过18岁' ],
    [ 'gameRule',    null, null, '活动结束数据清零' ],
    [ 'personalInfo', 'age', null, 20 ],
    [ 'personalInfo', 'name', null, '小橘子' ],
    [ 'gameList',    null, 'gameTitle', '贪吃蛇' ],
    [ 'gameList',    null, 'gameDesc', '儿时经典' ],
    [ 'gameList',    null, 'gameTitle', '超级玛丽' ],
    [ 'gameList',    null, 'gameDesc', '街机游戏' ],
  ] 

转化为

企业微信截图_16643597778137.png

然后进行每个key对应得数组不同类型得一个处理,当然也许这里有更好得方式! 其中要注意得是8.2.4 如果第三项(grandSonKey)存在,那么表示是一个数组(子项为对象)的处理,说明在写excel的时候,数组里面的对象的key和value,应该按照固定的个数去写,如果乱了,会出问题的

4.效果

创建language/message.js

import zhHans from "./zh-Hans";
import en from "./en";

// 语言包
const messages = {
  "zh-Hans": zhHans,
  en,
};

export default messages;

创建language/index.ts

import { createI18n } from "vue-i18n";
import messages from "./message";


const i18n = createI18n({
  legacy: false,
  locale: 'en',
  fallbackLocale: "en",
  globalInjection: true,
  messages,
});

export default i18n;

main.js

import i18n from "./language";
createApp(App).use(i18n).mount('#app');

index.vue

<template>
  test1
  {{$t('personalInfo.name')}}
</template>

企业微信截图_16643651264930.png

当然language底下得message.js和index.ts 都可以直接生成, 不用手动加入,大家自己去写吧,嘻嘻,后续有空我补上,不过这个不难的,重点是多语言这块,奥里给!感谢同事的excel的格式思路