首页 > 编程语言 >Node翻译i18n多语言文件,1分钟生成100种语言包

Node翻译i18n多语言文件,1分钟生成100种语言包

时间:2023-05-26 22:56:09浏览次数:59  
标签:Node 翻译 const chunk 语言包 key i18n obj 文案

前言

在需要国际化的项目中经常会遇到写完代码后需要将文案翻译到其他很多国家语言,人工翻译再复制文案到对应 json 或 js / ts 文件,这样将会浪费大量时间且非常枯燥,所以很有必要开发一款 node 插件,将这些重复乏味的工作交给机器完成。话不多说,先展示成品再讲原理

插件链接

https://github.com/hymhub/language-translate
language-translate 是一款基于 Google 翻译在线转换 ts/js/json 多语言文件并批量生成或插入指定文件的插件,支持增量更新,可使用 bash 翻译单个文件,也能集成在项目中持续批量翻译,支持单文件转单文件,单文件转多文件,多文件转多文件,多文件转单文件

效果演示

正常翻译效果:

压力测试(1分钟内生成100种语言包):

原理

插件原理比较简单,核心是使用 node 读取文案文件,再利用 google 翻译 API 翻译文案后最终生成或写入结果,其中需要注意的是翻译前文案合并和翻译后文案拆分细节,此操作是翻译速度提升的关键

下面写一个简易版方便理解原理

安装依赖

首先安装以下 2 个依赖:

  1. @vitalets/google-translate-api:用于翻译文案
  2. tunnel:用于网络代理(大陆无法直接使用 Google API)

注意版本,es6 和 conmonjs 模块化下载的版本不同,方便演示,以 conmonjs 为例

npm i @vitalets/[email protected]
npm i [email protected]

编写翻译脚本

const fs = require('fs')
const tunnel = require('tunnel')
const google = require('@vitalets/google-translate-api')

const googleTranslator = (text) => google(
  text,
  { from: 'en', to: 'zh-CN' },
  {
    agent: tunnel.httpsOverHttp({
      proxy: {
        host: '127.0.0.1',// 代理 ip
        port: 7890, // 代理 port
        headers: {
          'User-Agent': 'Node'
        }
      }
    })
  }
)
// 读取 json 文案文件
const sourceJson = require('./en.json')
// 定义翻译方法
const translateRun = async (inputJson) => {
  const sourceKeyValues = Object.entries(inputJson)
  const resultJson = {}
  for (let i = 0; i < sourceKeyValues.length; i++) {
    const [key, value] = sourceKeyValues[i]
    const { text } = await googleTranslator(value)
    resultJson[key] = text
  }
  return resultJson
}
// 将翻译结果写入硬盘
translateRun(sourceJson).then(resultJson => {
  fs.writeFileSync('./zh.json', JSON.stringify(resultJson))
})

将文案放入 en.json 例如:

{
  "hello": "hello"
}

执行此脚本就会发现目录下生成了 zh.json 的翻译结果:

{"hello":"你好"}

但是现在还不能递归翻译 json 内容,并且每一个 key 都调用了一次接口

先完成递归功能,递归翻译的实现方案有多种,考虑到后期文案合并/拆分减少 API 调用频率,这里采用 json 扁平化的方法

编写扁平化和反扁平化方法

const flattenObject = (obj, prefix = '') => {
  let result = {}
  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      const nestedKey = prefix.length > 0 ? `${prefix}/${key}` : key
      if (typeof obj[key] === 'object' && obj[key] !== null) {
        const nestedObj = flattenObject(obj[key], nestedKey)
        result = { ...result, ...nestedObj }
      } else {
        result[nestedKey] = obj[key]
      }
    }
  }
  return result
}

const unflattenObject = (obj) => {
  const result = {}
  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      const nestedKeys = key.split('/')
      let nestedObj = result
      for (let i = 0; i < nestedKeys.length; i++) {
        const nestedKey = nestedKeys[i]
        if (!Object.prototype.hasOwnProperty.call(nestedObj, nestedKey)) {
          nestedObj[nestedKey] = {}
        }
        if (i === nestedKeys.length - 1) {
          nestedObj[nestedKey] = obj[key]
        }
        nestedObj = nestedObj[nestedKey]
      }
    }
  }
  return result
}

扁平化方法的如何实现递归翻译?例如从传入:

const inputJson = {
  "hello": "hello",
  "colors": {
    "red": "red"
  }
}
flattenObject(inputJson) // { hello: 'hello', 'colors/red': 'red' }
// 此时进行翻译,例如结果是 { hello: '你好', 'colors/red': '红色的' }
// 再进行反扁平化
unflattenObject(resultJson) // {"hello":"你好","colors":{"red":"红色的"}}

搞懂扁平化原理后接着改造 translateRun 方法:

const translateRun = async (inputJson) => {
  inputJson = flattenObject(inputJson)
  const sourceKeyValues = Object.entries(inputJson)
  const resultJson = {}
  for (let i = 0; i < sourceKeyValues.length; i++) {
    const [key, value] = sourceKeyValues[i]
    const { text } = await googleTranslator(value)
    resultJson[key] = text
  }
  return unflattenObject(resultJson)
}

现在已经能进行递归翻译了,接下来进行翻译前文案合并和翻译后文案拆分,目的是为了减少 API 调用频率,也是大大提高翻译速度的核心

翻译提速

继续改造 translateRun 方法:

const translateRun = async (inputJson) => {
  inputJson = flattenObject(inputJson)
  let chunkValuesLength = 0
  let chunk = []
  const chunks = []
  const sourceKeyValues = Object.entries(inputJson)
  sourceKeyValues.forEach(([key, value]) => {
    // Google 翻译单次最大字符长度 5000 字, 5 为占位分隔符长度
    if (chunkValuesLength + value.length + 5 >= 5000) {
      chunks.push(chunk)
      chunkValuesLength = 0
      chunk = []
    } else {
      chunk.push({ key, value })
      chunkValuesLength += (value.length + 5)
    }
  })
  if (chunk.length > 0) {// 遍历完后检查不满 5000 字符的遗留
    chunks.push(chunk)
    chunkValuesLength = 0
    chunk = []
  }
  const resultJson = {}
  for (let i = 0; i < chunks.length; i++) {
    const chunk = chunks[i]
    const mergeText = chunk.map(v => v.value).join('\n###\n')// 合并文案
    const { text } = await googleTranslator(mergeText)
    const resultValues = text.split(/\n *# *# *# *\n/).map((v) => v.trim())// 拆分文案
    if (chunk.length !== resultValues.length) {
      throw new Error('翻译前文案碎片长度和翻译后的不一致')
    }
    chunk.forEach(({ key }, index) => {
      resultJson[key] = resultValues[index]
    })
  }
  return unflattenObject(resultJson)
}

现在放入大量文案在 en.json 文件,执行翻译脚本,假如文案有 1000 个 key 原本需要调用 1000 次接口,现在不到 10 次甚至不到 5 次即可翻译完成。

完整 demo:

const fs = require('fs')
const tunnel = require('tunnel')
const google = require('@vitalets/google-translate-api')

const flattenObject = (obj, prefix = '') => {
  let result = {}
  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      const nestedKey = prefix.length > 0 ? `${prefix}/${key}` : key
      if (typeof obj[key] === 'object' && obj[key] !== null) {
        const nestedObj = flattenObject(obj[key], nestedKey)
        result = { ...result, ...nestedObj }
      } else {
        result[nestedKey] = obj[key]
      }
    }
  }
  return result
}

const unflattenObject = (obj) => {
  const result = {}
  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      const nestedKeys = key.split('/')
      let nestedObj = result
      for (let i = 0; i < nestedKeys.length; i++) {
        const nestedKey = nestedKeys[i]
        if (!Object.prototype.hasOwnProperty.call(nestedObj, nestedKey)) {
          nestedObj[nestedKey] = {}
        }
        if (i === nestedKeys.length - 1) {
          nestedObj[nestedKey] = obj[key]
        }
        nestedObj = nestedObj[nestedKey]
      }
    }
  }
  return result
}

const googleTranslator = (text) => google(
  text,
  { from: 'en', to: 'zh-CN' },
  {
    agent: tunnel.httpsOverHttp({
      proxy: {
        host: '127.0.0.1',// 代理 ip
        port: 7890, // 代理 port
        headers: {
          'User-Agent': 'Node'
        }
      }
    })
  }
)
// 读取 json 文案文件
const sourceJson = require('./en.json')
// 定义翻译方法
const translateRun = async (inputJson) => {
  inputJson = flattenObject(inputJson)
  let chunkValuesLength = 0
  let chunk = []
  const chunks = []
  const sourceKeyValues = Object.entries(inputJson)
  sourceKeyValues.forEach(([key, value]) => {
    // Google 翻译单次最大字符长度 5000 字, 5 为占位分隔符长度
    if (chunkValuesLength + value.length + 5 >= 5000) {
      chunks.push(chunk)
      chunkValuesLength = 0
      chunk = []
    } else {
      chunk.push({ key, value })
      chunkValuesLength += (value.length + 5)
    }
  })
  if (chunk.length > 0) {// 遍历完后检查不满 5000 字符的遗留
    chunks.push(chunk)
    chunkValuesLength = 0
    chunk = []
  }
  const resultJson = {}
  for (let i = 0; i < chunks.length; i++) {
    const chunk = chunks[i]
    const mergeText = chunk.map(v => v.value).join('\n###\n')// 合并文案
    const { text } = await googleTranslator(mergeText)
    const resultValues = text.split(/\n *# *# *# *\n/).map((v) => v.trim())// 拆分文案
    if (chunk.length !== resultValues.length) {
      throw new Error('翻译前文案碎片长度和翻译后的不一致')
    }
    chunk.forEach(({ key }, index) => {
      resultJson[key] = resultValues[index]
    })
  }
  return unflattenObject(resultJson)
}
// 将翻译结果写入硬盘
translateRun(sourceJson).then(resultJson => {
  fs.writeFileSync('./zh.json', JSON.stringify(resultJson))
})

结语

有了核心思路,其他的就是细节完善以及不断排坑了,例如合并拆分文案的特殊字符在不同语言会有异常,所以需要测试出不同语言所支持的特殊字符拆分方法,在翻译时根据不同语言使用不同的特殊字符进行分割,以及断点再续(文案太长,翻译中断导致浪费已翻译的文案)、增量更新等等,希望这篇文章对你有所帮助哦~

标签:Node,翻译,const,chunk,语言包,key,i18n,obj,文案
From: https://www.cnblogs.com/hymenhan/p/17436013.html

相关文章

  • C#与Node JS互相实现DES加密解密
    具体的加密算法可以可自行查询其区别,这里只是抛砖引玉,大部分加密方法基本都能通过改变传入参数来实现。C#相关类文档: System.Security.Cryptography命名空间|MicrosoftLearnNodeJS相关文档:Crypto|Node.jsv16.20.0Documentation(nodejs.org) C#加密函数:1using......
  • 解决npm npm does not support Node.js
    原因:node.js和npm版本不对应参考官网版本对应(https://nodejs.org/zh-cn/download/releases/),下载对应的node.js版本和更新npm版本npmupdate常用命令使用 npm-check检查更新npminstall-gnpm-checknpm-check2.npm-upgrade更新......
  • Nodejs 应用编译构建提速建议
    编译构建的整体过程拉取编译镜像拉取缓存镜像拉取项目源码挂载缓存目录执行编译命令(用户自定义)持久化缓存上传编译镜像为什么在本地构建就快,但编译机上很慢在编辑机上每次的构建环境都是全新的,完成一次构建比本地需要多一些步骤:现成的全局包缓......
  • 使用Node搭建一个本地的WebSocket服务
    首先创建一个目录,cd到目录下,npminit-y一路回车,安装一个插件npmiwebsocket建一个server.js文件constWebSocketServer=require('websocket').serverconsthttp=require('http')constport=8000lettime=0//创建服务器constserver=http.createServe......
  • Nvm 安装node报错: The system cannot find the path specified.
    解决思路:1.确保你安装nvm之前node.js已经删除干净了。这一步如果不会请移步:https://blog.csdn.net/m0_51945510/article/details/127710792这个是要删除的。 2.确保你点击的安装路径中,没有空格和中文,并且确定存在这个目录(安装时,不会帮你新建文件夹)。  上面两张图只......
  • 部署node项目外网访问失败
    原因是没有正确的开启防火墙端口。查看防火墙是否在运行firewall-cmd--state查看都有哪些端口添加到例外firewall-cmd--permanent--list-port正确命令是:firewall-cmd--zone=public--add-port=8888/tcp--permanent 永久开启防火墙8888端口,再执行 firewall-cmd--reload......
  • 01-Node.js介绍
    01.Node.js是什么?pNode.js是一个基于V8JavaScript引擎的JavaScript运行时环境。也就是说:Node.js基于V8引擎来执行JavaScript的代码。V8引擎可以嵌入到任何C++应用程序中,无论是Chrome还是Node.js,事实上都是嵌入了V8引擎来执行JavaScript代码的。但需要注意的是:两者都不仅......
  • 02-Node.js的包管理工具
    00.代码共享方案模块化的编程思想,支持将代码划分成一个个小的、独立的结构。我们可以通过模块化的方式来封装自己的代码,将之封装成一个工具;这个工具我们可以让同事通过导入的方式来使用,甚至也可以分享给世界各地的程序员来使用;假如,我们要将某个工具分享给世界上所有的程序员......
  • 【Node】node.js安装与配置(详细步骤)
    node.js安装与配置(详细步骤)一、安装Node.js1.1下载1.2安装1.3环境变量二、验证是否安装成功三、修改模块下载位置3.1查看npm默认存放位置3.2在nodejs安装目录下,创建“node_global”和“node_cache”两个文件夹3.3修改默认文件夹3.4测试默认位置是否更改成功四、设置......
  • python:Error: EPERM: operation not permitted, mkdir 'F:\Program Files\nodejs\n
     可以发现文件没有权限npmERR!Error:EPERM:operationnotpermitted,mkdir'F:\ProgramFiles\nodejs\node_global\node_modules'将nodejs的文件权限改为完全控制之后操作即可 ......