likes
comments
collection
share

【前端出海】多语言篇(二):实现一个多语言自动翻译脚本

作者站长头像
站长
· 阅读数 3

【前端出海】多语言篇(二):实现一个多语言自动翻译脚本

众所周知,国内互联网行业已经进入存量市场,竞争激烈、利润微薄。一些公司为了摆脱困境,将目标转向了海外,海外市场相比国内,竞争小、利润高,潜力大。未来数年,出海会成为越来越多公司的重要战略。

先来看看自动翻译的实际效果:

【前端出海】多语言篇(二):实现一个多语言自动翻译脚本

从上面动图可以看到,我们只用编写zh-cn.json文件中的语言配置,其他国家语言就会自动翻译生成。那这是如何实现的呢?其实仅仅通过执行一个js翻译脚本就会自动将中文翻译成他国语言。

下面我将介绍如何一步步编写这个脚本。

准备工作

翻译脚本采用的是阿里云SDK提供的机器翻译功能,所以拥有阿里云账号并且开通机器翻译功能是先决条件。

如果你没有阿里云账号,可以去 阿里云首页 注册一个,再通过支付宝账号进行实名认证。然后在 阿里云控制台首页 登录阿里云账号,在个人中心-开通助手里面勾选机器翻译,点击一键开通。

【前端出海】多语言篇(二):实现一个多语言自动翻译脚本

下一步就是申请AccessToken,同样是在 阿里云控制台首页 点击AccessKeys管理。

【前端出海】多语言篇(二):实现一个多语言自动翻译脚本

接着在AccessKey管理页面点击创建AccessKey,然后将创建的AccessKey ID和AccessKey Secret保存到本地。

【前端出海】多语言篇(二):实现一个多语言自动翻译脚本

核心代码

下面我将介绍自动翻译脚本的核心代码,整个代码其实是基于阿里云的机器翻译SDK改造的。

阿里云的机器翻译提供了多种开发语言的SDK,包括Java、Python、.NET等常用语言,我们采用node.js运行脚本,使用TypeScript版本比较合适。

打开 TypeScript SDK示例文档 页面,可以看到需要下载的依赖包以及SDK示例代码。

【前端出海】多语言篇(二):实现一个多语言自动翻译脚本

然后点击SDK示例代码的链接,打开 机器翻译_API调试 页面,就能看到完整的示例代码以及可以根据修改代码和参数进行调试。

【前端出海】多语言篇(二):实现一个多语言自动翻译脚本

按照示例代码在项目中下载依赖

npm install --save @alicloud/alimt20181012 @alicloud/openapi-client @alicloud/tea-util

注:如果你需要使用TypeScript,还需要下载这个依赖@alicloud/tea-typescript

新建一个文件aliyun-translator.js,放在 /scripts 目录下,然后复制整块代码,稍作改动,整个翻译脚本的核心功能就完成了,代码如下:

const {default: alimt20181012, ...$alimt20181012} = require('@alicloud/alimt20181012');
const $OpenApi = require('@alicloud/openapi-client');
const {default: Util, ...$Util} = require('@alicloud/tea-util');

function createClient(accessKeyId, accessKeySecret) {
    let config = new $OpenApi.Config({
        // 您的 AccessKey ID
        accessKeyId: accessKeyId,
        // 您的 AccessKey Secret
        accessKeySecret: accessKeySecret,
    });
    // 访问的域名
    config.endpoint = `mt.cn-hangzhou.aliyuncs.com`;
    return new alimt20181012(config);
}

async function main(sourceLanguage, targetLanguage, sourceText) {
    // 传入 AccessKey ID 和 AccessKey Secret,示例代码,敏感数据打码
    let client = createClient("******Dy9gLx2Lzwqs******", "******OcI0H88SC64ce4DizT******");
    let translateGeneralRequest = new $alimt20181012.TranslateGeneralRequest({
        formatType: "text",
        sourceLanguage: sourceLanguage,
        targetLanguage: targetLanguage,
        sourceText: sourceText,
        scene: "general",
    });
    let runtime = new $Util.RuntimeOptions();
    try {
        // 复制代码运行请自行打印 API 的返回值
        const result = await client.translateGeneralWithOptions(translateGeneralRequest, runtime);
        return result.body.data?.translated;
    } catch (error) {
        if (error.code === 'ECONNRESET') {
            return main(sourceLanguage, targetLanguage, sourceText);
        }
        throw error;
    }
}

module.exports = main;

目录结构如下:

  • scripts
    • aliyun-translator.js
    • build.js
    • start.js
    • test.js
  • src
  • package.json

其实整块代码最核心的就是TranslateGeneralRequest方法的调用

let translateGeneralRequest = new $alimt20181012.TranslateGeneralRequest({
    formatType: "text",
    sourceLanguage: sourceLanguage,
    targetLanguage: targetLanguage,
    sourceText: sourceText,
    scene: "general"
});

我们来看看阿里云开发文档上,对这个方法的配置项传参说明

【前端出海】多语言篇(二):实现一个多语言自动翻译脚本 文档里也描述了返回参数,也是我们整个核心代码的返回参数

【前端出海】多语言篇(二):实现一个多语言自动翻译脚本 示例如下,Translated就是我们需要的翻译结果

{
  "Code": 200,
  "Message": "success",
  "RequestId": "86D18195-D89C-4C8C-9DC4-5FCE789CE6D5",
  "Data": {
    "Translated": "Hello",
    "WordCount": "10",
    "DetectedLanguage": "zh"
  }
}

自动翻译的脚本

核心代码写好后,我们还需要编写一个用node.js运行的脚本,在aliyun-translator.js文件同级目录下,新建一个translate-to-other.js文件,整体代码如下:

const fs = require('fs');
const aliyunTranslator = require('./aliyun-translator');

// 需要使用阿里云翻译的语言
const langMap = [
    ['de', 'de'], // 德语
    ['en', 'en-us'], // 英语
    ['es', 'es'], // 西班牙语
    ['fr', 'fr'],  // 法语
    ['ja', 'ja'],  // 日语
    ['ru', 'ru'], // 俄语
];

const forEachObject = function(obj, cb) {
    for (let i in obj) {
        const v = obj[i];
        if (typeof v === 'object') {
            forEachObject(v, (keys, v) => cb([i, ...keys], v));
        } else {
            cb([i], v);
        }
    }
}
const objectGet = function(obj, keys) {
    const [firstKey, ...otherKeys] = keys;
    if (typeof obj[firstKey] === 'object' && otherKeys.length) {
        return objectGet(obj[firstKey], keys.slice(1));
    }
    return obj[firstKey];
}
const objectSet = function(obj, keys, value) {
    const [firstKey, ...otherKeys] = keys;
    if (typeof obj[firstKey] === 'object' && otherKeys.length) {
        objectSet(obj[firstKey], otherKeys, value);
    } else if (obj[firstKey] === undefined && otherKeys.length) {
        obj[firstKey] = {};
        objectSet(obj[firstKey], otherKeys, value);
    } else if (otherKeys.length === 0) {
        obj[firstKey] = value;
    }
}

let i = 0;
// 翻译脚本主方法
doTrans('../src/i18n', './src/i18n');

function doTrans(relativeDir, localBaseDir) {
    // 读取中文基准文件
    const baseZhCN = require(`${relativeDir}/zh-cn.json`);
    // 然后遍历langMap数组找到需要翻译的目标语言文件
    langMap.forEach(([lang, fileName]) => {
        const promiseArray = [];
        let target;
        try{
            target = require(`${relativeDir}/${fileName}.json`);
        }catch(e){
            try{
                target = require(`${relativeDir}/${fileName.toLowerCase()}.json`)
            }catch(e){
                process.stdout.write(`找不到文件${relativeDir}/${fileName}.json,跳过\n`);
                return;
            }
        }
        forEachObject(baseZhCN, (keys, v) => {
            if (objectGet(target, keys) === undefined) {
                // 没有对应翻译结果就调用阿里云的翻译功能
                promiseArray.push(
                    new Promise((resolve, reject) => {
                        setTimeout(() => {
                            aliyunTranslator('zh', lang, v).then((r) => {
                                objectSet(target, keys, r);
                                process.stdout.write('中间结果 ' + v + '\t' + r + '\n');
                                process.stdout.write(
                                    keys.join('.') + '\t' + v + '\t' + r + '\n'
                                );
                                resolve(target);
                            }).catch((error) => {
                                reject(error);
                            });
                        }, i * 200);
                        i++;
                    })
                );
            }
        });

        process.stdout.write(`开始翻译 ${localBaseDir}/${fileName}.json`);
        process.stdout.write('\n');
        Promise.all(promiseArray).then((results) => {
            const r = results[results.length - 1];
            if (r) {
                const file = `${localBaseDir}/${fileName}.json`;
                fs.writeFile(file, JSON.stringify(r, null, 2), (error) => {
                    if (!error) {
                        process.stdout.write(`写文件${file}成功`);
                    } else {
                        process.stderr.write(error.message);
                    }
                    process.stdout.write('\n');
                });
            }
            process.stdout.write('翻译结束 ' + fileName + '\n');
            process.stdout.write('-------------------------------------------\n');
        })
    });
}

这段代码很长,但并不复杂,我来对代码一一拆解。

首先整个脚本是从doTrans这个方法开始运行的。

doTrans('../src/i18n', './src/i18n');

它有两个参数:第一个是i18n文件夹相对当前文件的相对路径,第二个参数是i18n文件夹相对于整个项目的路径。

这个函数通过第一个参数读取中文基准文件zh-cn.json,然后遍历langMap数组找到需要翻译的目标语言的json文件。

...
// 需要使用阿里云翻译的语言
const langMap = [
    ['de', 'de'], // 德语
    ['en', 'en-us'], // 英语
    ['es', 'es'], // 西班牙语
    ['fr', 'fr'],  // 法语
    ['ja', 'ja'],  // 日语
    ['ru', 'ru'], // 俄语
];
...
function doTrans(relativeDir, localBaseDir) {
    // 读取中文基准文件
    const baseZhCN = require(`${relativeDir}/zh-cn.json`);
    // 然后遍历langMap数组找到需要翻译的目标语言文件
    langMap.forEach(([lang, fileName]) => {
        const promiseArray = [];
        let target;
        try{
            target = require(`${relativeDir}/${fileName}.json`);
        }catch(e){
            try{
                target = require(`${relativeDir}/${fileName.toLowerCase()}.json`)
            }catch(e){
                process.stdout.write(`找不到文件${relativeDir}/${fileName}.json,跳过\n`);
                return;
            }
        }
        ...
    });
}

然后用forEachObject方法递归遍历基准文件zh-cn.json中所有的key,如果目标语言文件中没有对应的翻译结果,则调用上文提到的aliyunTranslator方法,请求阿里云的翻译SDK,生成对应语言的翻译结果。

...
const forEachObject = function(obj, cb) {
    for (let i in obj) {
        const v = obj[i];
        if (typeof v === 'object') {
            forEachObject(v, (keys, v) => cb([i, ...keys], v));
        } else {
            cb([i], v);
        }
    }
}
...

function doTrans(relativeDir, localBaseDir) {
    // 读取中文基准文件
    const baseZhCN = require(`${relativeDir}/zh-cn.json`);
    // 然后遍历langMap数组找到需要翻译的目标语言文件
    langMap.forEach(([lang, fileName]) => {
        ...
        forEachObject(baseZhCN, (keys, v) => {
            if (objectGet(target, keys) === undefined) {
                // 没有对应翻译结果就调用阿里云的翻译功能
                promiseArray.push(
                    new Promise((resolve, reject) => {
                        setTimeout(() => {
                            aliyunTranslator('zh', lang, v).then((r) => {
                                objectSet(target, keys, r);
                                process.stdout.write('中间结果 ' + v + '\t' + r + '\n');
                                process.stdout.write(
                                    keys.join('.') + '\t' + v + '\t' + r + '\n'
                                );
                                resolve(target);
                            }).catch((error) => {
                                reject(error);
                            });
                        }, i * 200);
                        i++;
                    })
                );
            }
        });
        ...
    });
}

最后使用fs的writeFile方法,将所有翻译结果写入目标文件,然后要用到doTrans方法的第二个参数localBaseDir

const fs = require('fs');
...
function doTrans(relativeDir, localBaseDir) {
    const baseZhCN = require(`${relativeDir}/zh-cn.json`);
    langMap.forEach(([lang, fileName]) => {
       ...
        process.stdout.write(`开始翻译 ${localBaseDir}/${fileName}.json`);
        process.stdout.write('\n');
        Promise.all(promiseArray).then((results) => {
            const r = results[results.length - 1];
            if (r) {
                const file = `${localBaseDir}/${fileName}.json`;
                fs.writeFile(file, JSON.stringify(r, null, 2), (error) => {
                    if (!error) {
                        process.stdout.write(`写文件${file}成功`);
                    } else {
                        process.stderr.write(error.message);
                    }
                    process.stdout.write('\n');
                });
            }
            process.stdout.write('翻译结束 ' + fileName + '\n');
            process.stdout.write('-------------------------------------------\n');
        })
    });
}

注:localBaseDir这个参数一定要传入i18n文件夹相对于整个项目的路径,不然文件内容会写入不成功。

脚本执行命令

在项目package.json文件中,添加一个脚本执行命令trans-from-zh

"scripts": {
    "start": "node scripts/start.js",
    "build": "node scripts/build.js",
    "test": "node scripts/test.js",
    "trans-from-zh": "node scripts/translate-to-other.js"
},

这样运行如下命令时,就会用node执行translate-to-other.js

yarn trans-from-zh

最终效果

最后再来看一遍自动翻译的实际效果

注意事项

1、每个需要翻译的json文件,要手动输入一对大括号,不然翻译结果会写入失败。

【前端出海】多语言篇(二):实现一个多语言自动翻译脚本

2、如果需要翻译的中文有成千上万条,一次性翻译多国语言很容易导致翻译结果写入不成功,如果遇到这种情况,可以每次跑脚本时只翻译一种语言。

translate-to-other.js文件中,每次只启用一种语言进行翻译。

const fs = require('fs');
const aliyunTranslator = require('./aliyun-translator');

// 每次跑脚本只启用一种语言,跑一次换一种语言
const langMap = [
    ['de', 'de'], // 德语
    // ['en', 'en-us'], // 英语
    // ['es', 'es'], // 西班牙语
    // ['fr', 'fr'],  // 法语
    // ['ja', 'ja'],  // 日语
    // ['ru', 'ru'], // 俄语
];

结语

本文讲解了如何实现一个多语言翻译脚本,将中文自动翻译成指定的多个语言。

在下一篇中,我将介绍多语言的规范、常见问题以及一些注意事项,干货满满,敬请期待。