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

Posted 是明啊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Node翻译i18n多语言文件,1分钟生成100种语言包相关的知识,希望对你有一定的参考价值。

前言

在需要国际化的项目中经常会遇到写完代码后需要将文案翻译到其他很多国家语言,人工翻译再复制文案到对应 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/google-translate-api@8.0.0
npm i tunnel@0.0.6

编写翻译脚本

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))
)

结语

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

Qt系列文章之三十九(Qt多语言国际化程序实例设计实现)

关注我的公众号,可免费看全本Qt系列文章~

概述

  有些软件需要开发多语言界面版本,如中文版、英文版、日文版、繁体版等等,并且在软件里可以方便地切换界面语言。Qt为多语言界面提供了很好的支持,使用Qt的一些规则和工具,可以很方便地为应用程序开发提供多语言界面支持。

  用Qt开发多语言界面应用程序,主要包括以下几个步骤:

  1. 在程序设计阶段,程序代码中每一个用户可见的字符串都用 tr() 函数封装, 以便Qt提取界面字符串用于生成翻译资源文件。用UI设计器可视化设计窗体时统一用一 种语言,如汉语。

以上是关于Node翻译i18n多语言文件,1分钟生成100种语言包的主要内容,如果未能解决你的问题,请参考以下文章

低代码平台多语言国际化(i18n)技术方案

ExtJS - 带有 i18n 文本的多语言

ionic2 基于ngx-translate实现多语言切换,翻译

React-intl 多语言应用程序:更改语言和翻译存储

多语言国际化

Vue 项目中实现多语言国际化 ( i18n )