diff --git a/i18n/configuration.js b/i18n/configuration.js new file mode 100644 index 0000000..395c4db --- /dev/null +++ b/i18n/configuration.js @@ -0,0 +1,37 @@ +const buildDebug = require("debug"); +const path = require("path"); + +const debug = buildDebug("files:configuration"); + +function loadConfig (filepath) { + try { + const conf = readConfig(filepath) + return conf + } catch (e) { + debug('error', e) + return null + } +} + +function readConfig(filepath) { + let options; + try { + const configModule = require(filepath); + options = configModule && configModule.__esModule + ? configModule.default || undefined + : configModule; + } catch (err) { + throw err; + } finally { + } + return { + filepath, + dirname: path.dirname(filepath), + options, + } +} + +module.exports = { + loadConfig, + readConfig, +} \ No newline at end of file diff --git a/i18n/index.js b/i18n/index.js new file mode 100644 index 0000000..893f1df --- /dev/null +++ b/i18n/index.js @@ -0,0 +1,236 @@ +'use strict' +const program = require('commander'); +const jsonfile = require('jsonfile') +const utils = require('./utils') +const trans = require('./translation') // trans +const { loadConfig } = require('./configuration') +const vfs = require('vinyl-fs') +const map = require('map-stream') +const path = require('path') +const fs = require('fs'); +const uniq = require('lodash.uniq') + +function commaSeparatedList(value, split = ',') { + return value.split(split).filter(item => item); +} + +program + .version('1.0.0') + .option('--cwd ', '工作目录') + .option('--root-dir ', '国际文本所在的根目录') + .option('--i18n-file-rules ', '匹配含有国际化文本的文件规则', commaSeparatedList) + .option('--i18n-text-rules ', '国际化文本的正则表达式,正则中第一个捕获对象当做国际化文本', commaSeparatedList) + .option('--keep-key-rules ', '模块的国际化的json文件需要被保留下的key,即使这些组件在项目中没有被引用', commaSeparatedList) + .option('--out-dir ', '生成的国际化资源包的输出目录') + .option('-l, --i18n-languages ', '需要生成的国际化语言文件,目前支持zh、en多个用逗号分割,默认全部', commaSeparatedList) + .option('--config ', '配置文件的路径,没有配置,默认路径是在${cwd}/vve-i18n-cli.config.js') + .option('--no-config', '是否取配置文件') + .option('-t, --translate', '是否翻译') + .option('--translate-from-lang', '翻译的基础语言,默认是用中文翻译') + .option('--force-translate', '是否强制翻译,即已翻译修改的内容,也重新用翻译生成') + .option('--translate-language ', '翻译的语言', commaSeparatedList) + .option('--copy-index', '模块下${outDir}/index.js文件不存在才拷贝index.js') + .option('--force-copy-index', '是否强制拷贝最新index.js') + .parse(process.argv); + +const config = { + // 工作目录 + cwd: '.', + // 根目录,国际文本所在的根目录 + rootDir: 'src', + // 默认所有模块,如果有传module参数,就只处理某个模块 + // '**/module-**/**/index.js' + moduleIndexRules: [ + 'main.js' + ], + // 匹配含有国际化文本的文件规则 + i18nFileRules: [ + '**/*.+(vue|js)' + ], + // 国际化文本的正则表达式,正则中第一个捕获对象当做国际化文本 + i18nTextRules: [ + /(?:[\$.])t\(['"](.+?)['"]/g, + ], + // 模块的国际化的json文件需要被保留下的key,即使这些组件在项目中没有被引用 + // key可以是一个字符串,正则,或者是函数 + keepKeyRules: [ + /^G\/+/ // G/开头的会被保留 + ], + // 生成的国际化资源包的输出目录 + outDir: 'lang', + // 生成的国际化的语言 + i18nLanguages: [ + 'zh', // 中文 + 'en', // 英文 + ], + // 配置文件的路径,没有配置,默认路径是在${cwd}/vve-i18n-cli.config.js + config: undefined, + // 是否取配置文件 + noConfig: false, + // 是否翻译 + translate: false, + // 翻译的基础语言,默认是用中文翻译 + translateFromLang: 'zh', + // 是否强制翻译,即已翻译修改的内容,也重新用翻译生成 + forceTranslate: false, + // 翻译的语言 + translateLanguage: [ + 'zh', + 'en' + ], + // 模块下${outDir}/index.js文件不存在才拷贝index.js + copyIndex: false, + // 是否强制拷贝最新index.js + forceCopyIndex: false, +} + +Object.assign(config, program) + +const CONFIG_JS_FILENAME = "vve-i18n-cli.config.js"; + +const absoluteCwd = path.resolve(config.cwd); + +// 优先判断是否需要读取文件 +if (!config.noConfig) { + let configFilePath = path.join(absoluteCwd, CONFIG_JS_FILENAME) + if (config.config) { + configFilePath = path.resolve(config.config) + } + if (fs.existsSync(configFilePath)) { + const conf = loadConfig(configFilePath) + if (conf) { + Object.assign(config, conf.options, program) + } + } +} + +const absoluteRootDir = path.resolve(absoluteCwd, config.rootDir); + +const fsExistsSync = utils.fsExistsSync +const filterObjByKeyRules = utils.filterObjByKeyRules +const translateArr = trans.translateArr + +const i18nData = {} +const tmpRegData = {} + +// 从文件中提取模块的的国际化KEY +function getModuleI18nData (modulePath, fileContent) { + if (!i18nData[modulePath]) { + i18nData[modulePath] = [] + } + for (let i = 0; i < config.i18nTextRules.length; i++) { + const regI18n = new RegExp(config.i18nTextRules[i], 'g') + while ((tmpRegData.matches = regI18n.exec(fileContent))) { + i18nData[modulePath].push(tmpRegData.matches[1]) + } + } +} + +// 删除重复的key,并排序方便git比对 +function normalizeI18nData () { + const moduleKeys = Object.keys(i18nData) + moduleKeys.forEach(key => { + i18nData[key] = uniq(i18nData[key]).sort() + }) +} + +// 根据旧数据,生成新数据 +async function makeNewData (key, lang, originData) { + const newData = filterObjByKeyRules(originData, config.keepKeyRules) // 根据配置保留一些keys值,保证即使在项目中不被引用也能保存下来 + + let newAddDataArr = [] // 新增的数据,即在旧的翻译文件中没有出现 + + i18nData[key].forEach(key => { + if (originData.hasOwnProperty(key)) { + newData[key] = originData[key] + } else { + newData[key] = key + newAddDataArr.push(key) + } + }) + + // 基础语言不翻译(默认中文),因为由中文翻译成其他语言 + if (config.translate && lang !== config.translateFromLang) { + let translateRst = {} + + // 如果强制翻译,则翻译所有的key + if (config.forceTranslate) { + newAddDataArr= Object.keys(newData) + } + + // 配合--translate使用,需要翻译的语言,目前支持en、ko,多个用逗号分割,默认全部 + if (!config.translateLanguage) { + translateRst = await translateArr(config.translateFromLang, lang, newAddDataArr) + } else if (config.translateLanguage.includes(lang)) { + translateRst = await translateArr(config.translateFromLang, lang, newAddDataArr) + } + Object.assign(newData, translateRst) + } + return newData +} + +// 保存国际化文件 +async function saveI18nFile({ + dirPath, +} = {}) { + const i18nLanguages = config.i18nLanguages + + for (let i = 0; i < i18nLanguages.length; i++) { + const item = i18nLanguages[i] + const i18nDir = path.resolve(dirPath, config.outDir) + if (!fsExistsSync(i18nDir)) { + fs.mkdirSync(i18nDir); + } + + // 模块下i18n/index.js文件不存在才拷贝index.js,或者forceCopyIndex=true强制拷贝 + const i18nIndexFile = path.resolve(i18nDir, 'index.js') + if ((config.copyIndex && !fsExistsSync(i18nIndexFile)) || config.forceCopyIndex) { + fs.writeFileSync(i18nIndexFile, require('./res/index.js')(i18nLanguages)) + } + + // 没有对应语言的国际化文件,就创建一个 + const langFilePath = path.resolve(i18nDir, item + '.json') + if (!fsExistsSync(langFilePath)) { + jsonfile.writeFileSync(langFilePath, {}, { spaces: 2, EOL: '\n' }) + } + + // 读取原有的国际化文件信息,重新与新收集的国际化信息合并 + const originData = jsonfile.readFileSync(langFilePath) || {} + const newData = await makeNewData(dirPath, item, originData) + + // 写文件 + jsonfile.writeFile(langFilePath, newData, { spaces: 2, EOL: '\n' }, err => { + if (err) return console.log('提取失败' + langFilePath + '\n' + err) + console.log('提取完成' + langFilePath) + }) + } +} + +// 保存模块的I18n文件 +function saveModuleI18nFile() { + const moduleKeys = Object.keys(i18nData) + moduleKeys.forEach(key => { + saveI18nFile({ dirPath: key }) + }) +} +vfs.src( + config.moduleIndexRules.map(item => path.resolve(absoluteRootDir, item)), +{ + dot: false +}).pipe(map((file, cb) => { + const modulePath = path.dirname(file.path) + + vfs.src(config.i18nFileRules.map( + item => path.resolve(modulePath, item)), + { dot: false} ) + .pipe(map((file, cb) => { + const contents = file.contents.toString() + getModuleI18nData(modulePath, contents) + cb(null) + })).on('end', () => { + cb(null) + }) +})).on('end', () => { + normalizeI18nData() + saveModuleI18nFile() +}) diff --git a/i18n/res/index.js b/i18n/res/index.js new file mode 100644 index 0000000..ff15212 --- /dev/null +++ b/i18n/res/index.js @@ -0,0 +1,22 @@ + + +const camelizeRE = /[-_](\w)/g +function camelize (str) { + return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '') +} + +module.exports = function makeIndex (langArr = []) { + const importArr = langArr.map((lang, index) => { + return `import ${camelize(lang)} from './${lang}.json'` + }) + + const exportArr = langArr.map((lang, index) => { + const cLang = camelize(lang) + const langItem = cLang === lang + ? ` ${lang}` + : ` '${lang}': ${cLang}` + return langItem + }) + + return `${importArr.join('\n')}${importArr.length ? '\n\n' : ''}export default {${exportArr.length ? '\n' : ''}${exportArr.join(',\n')}${exportArr.length ? '\n' : ''}}\n` +} diff --git a/i18n/translation.js b/i18n/translation.js new file mode 100644 index 0000000..79a8d6c --- /dev/null +++ b/i18n/translation.js @@ -0,0 +1,47 @@ +const { google, baidu, youdao } = require('translation.js') + +/** + * 翻译 + * https://github.com/Selection-Translator/translation.js + * youdao, baidu, google + */ +function translate (fromLang, lang, word) { + const from = fromLang === 'zh' ? 'zh-CN' : fromLang + + // 默认使用Baidu + return baidu.translate({ + text: word, + from, + to: lang + }).then(result => { + return (result.result[0] || '') + }) +} + +exports.translate = translate + +/** + * 翻译列表 + * 如果其中一个翻译错误,跳过 + * 顺序执行,防止同时开太多进程,程序异常 + */ +async function translateArr (fromLang, lang, wordArr) { + const result = [] + for (let i = 0; i < wordArr.length; i++) { + const word = wordArr[i] + const p = translate(fromLang, lang, word).then(res => { + console.log(word, '\t' + res) + result[word] = res + }).catch(err => { + console.log(err) + }) + await p + } + return result +} + +exports.translateArr = translateArr + +// translateArr('zh', 'en', ['您好', '哈哈']).then(res => { +// console.log(res) +// }) diff --git a/i18n/utils.js b/i18n/utils.js new file mode 100644 index 0000000..467c392 --- /dev/null +++ b/i18n/utils.js @@ -0,0 +1,45 @@ +'use strict' +const path = require('path') +const fs = require('fs') + +// 判断文件是否存在 +function fsExistsSync(path) { + try{ + fs.accessSync(path, fs.F_OK) + }catch(e){ + return false; + } + return true; +} + +exports.fsExistsSync = fsExistsSync + +// 拷贝文件 +function copyFile(src, dist) { + fs.writeFileSync(dist, fs.readFileSync(src)); +} + +exports.copyFile = copyFile + +// 过滤出满足规则的key,规则可以是一个字符串,正则或者函数 +function filterObjByKeyRules(obj = {}, keyRules = []) { + const newObj = {} + if (keyRules.length === 0) { + return newObj + } + const keys = Object.keys(obj) + keys.forEach(key => { + for (let i = 0; i < keyRules.length; i++) { + const keyRole = keyRules[i] + if ((Object.prototype.toString.call(keyRole) === '[object RegExp]' && keyRole.test(key)) + || (Object.prototype.toString.call(keyRole) === '[object Function]' && keyRole(key)) + || keyRole === key) { + newObj[key] = obj[key] + break + } + } + }) + return newObj +} + +exports.filterObjByKeyRules = filterObjByKeyRules diff --git a/package.json b/package.json index c636b34..6ce5281 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "test:unit": "jest --clearCache && vue-cli-service test:unit", "test:ci": "npm run lint && npm run test:unit", "test": "npm run lint && npm run test:unit", - "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml" + "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml", + "i18n": "node i18n/index.js" }, "dependencies": { "axios": "0.18.1", @@ -40,10 +41,15 @@ "babel-eslint": "10.0.1", "babel-jest": "23.6.0", "chalk": "2.4.2", + "commander": "^3.0.1", "connect": "3.6.6", + "debug": "^4.1.1", "eslint": "5.15.3", "eslint-plugin-vue": "5.2.2", "html-webpack-plugin": "3.2.0", + "jsonfile": "^5.0.0", + "lodash.uniq": "^4.5.0", + "map-stream": "0.0.7", "mockjs": "1.0.1-beta3", "node-sass": "^4.9.0", "runjs": "^4.3.2", @@ -53,6 +59,8 @@ "serve-static": "^1.13.2", "svg-sprite-loader": "4.1.3", "svgo": "1.2.2", + "translation.js": "^0.7.9", + "vinyl-fs": "^3.0.3", "vue-template-compiler": "2.6.10" }, "engines": { diff --git a/vve-i18n-cli.config.js b/vve-i18n-cli.config.js new file mode 100644 index 0000000..055a233 --- /dev/null +++ b/vve-i18n-cli.config.js @@ -0,0 +1,3 @@ +module.exports = { + outDir: 'lang' +} \ No newline at end of file pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy