前言
对现有的Vue项目进行国际化,需要一个个文件提取中文和替换vue-i18n的语法,所以开发一个解决上述问题的项目。
源码:https://gitee.com/timelessq/i18n
功能特性:
- 提取Vue项目中的中文词条
- 批量机器翻译
- 替换Vue项目中的中文
- 对基于 vue-i18n 的 Vue 项目执行静态分析
使用方法:
用喜欢的包管理工具安装依赖
pnpm install/npm install
- 把项目文件放到 src/vue-files
- npm run pick 提取中文
- 把提取到的中文格式化下放到language-files,主要是处理文件的键名
- npm run translate 批量机器翻译,谷歌可能不可以用,这步不是必须的,可以自己人工翻译
- npm run replace 把项目中的中文替换成 vue-i18n 的语法
- npm run extract 分析丢失的键和未用到的词条
1、提取中文
将源码里面的除注释外的中文进行提取
const fs = require('fs-extra');
const path = require('path');
const filePath = path.join(__dirname, 'src/vue-files')
const blacklist = ['api','assets','icons','locales','styles', 'vendor', 'linkage', 'combined-alarm'] // 忽略的文件夹
// const blacklist = ['assets','styles','vendor', 'w-picker']
const fileExtReg = /\.(js|json|vue)$/
const i18n = {}
/**
* 提取中文,导出excel
*/
async function setup() {
await readdir(filePath)
const distPath = path.join(__dirname, `dist/pick-files/zh-CN.json`)
fs.writeJSON(distPath, i18n, { spaces: 2 })
}
/**
* 递归读取文件
* @param {String} filePath 目录
*/
async function readdir(filePath) {
const files = await fs.readdir(filePath).catch(err => {
console.error('Error:(readdir)', err)
})
for(let i = 0; i < files.length; i ++) {
const filename = files[i]
if(blacklist.includes(filename)) {
continue
}
//获取当前文件的绝对路径
const filedir = path.join(filePath, filename)
const stats = await fs.stat(filedir).catch(err => {
console.error('Error:(stat)', err)
})
// 是否是文件
const isFile = stats.isFile()
// 是否是文件夹
const isDir = stats.isDirectory()
if (isFile) {
if(fileExtReg.test(filename)) {
await readFile(filedir).catch(() => {})
}
}
// 如果是文件夹
if (isDir) {
await readdir(filedir)
}
}
}
/**
* 读取文件,记录中文
* @param {String} filedir 文件路径
*/
async function readFile(filedir) {
console.log(filedir)
const dataStr = await fs.readFile(filedir,'utf-8').catch(err => {
console.error('Error:(readFile)', err)
})
// /.*[\u4e00-\u9fa5]+.*/gi 匹配一整行
// /[\u4e00-\u9fa5]+{{ \w+ }}[\u4e00-\u9fa5]+|[\u4e00-\u9fa5]+\${\w+}[\u4e00-\u9fa5]+|(?<!\/\/\s.*|<!--\s.*|\*.*)[\u4e00-\u9fa5]+/gi 匹配${}、{{ }}
// /(?<!\/\/\s.*|<!--\s.*|\*.*)[\u4e00-\u9fa5]+/gi
const matchArr = dataStr.match(/[\u4e00-\u9fa5]+\s?{{ \w+ }}\s?[\u4e00-\u9fa5]+|[\u4e00-\u9fa5]+\${.*}[\u4e00-\u9fa5]+|(?<!\/\/\s.*|<!--\s.*|\*.*)[\u4e00-\u9fa5]+/gi)
const uniqArr = Array.from(new Set(matchArr))
const result = {}
uniqArr.forEach(item => {
result[item] = item
})
i18n[filedir.replace(filePath, '')] = result
}
setup()
2、机器翻译
方案一使用谷歌翻译Api:@vitalets/google-translate-api
方案二抓取 https://translate.google.cn/m 谷歌翻译移动端页面
const fs = require('fs-extra');
const path = require('path');
const axios = require('axios');
const translate = require('@vitalets/google-translate-api');
const cheerio = require('cheerio')
/**
* planA 调用谷歌翻译Api (限频次,可能会被封IP)
* planB 抓取谷歌翻译网页
* @sumary 建议自己换成百度翻译Api
*/
const mode = 'fetchApi'
const handleTranslate = mode === 'fetchApi' ? fetchApi : cheerioPage
const expectList = [
{ lang: 'en', filename: 'en-US' }
] // 期望生成的语言文件
async function setup() {
const sourceLang = fs.readJSONSync(path.join(__dirname, 'dist/language-files/zh-CN.json'))
let taskList = []
const lastGroup = Object.keys(sourceLang).pop()
const lastKey = Object.keys(sourceLang[lastGroup]).pop()
for(const item of expectList) {
item.result = {}
for(const i in sourceLang) {
for(const j in sourceLang[i]) {
taskList.push(
handleTranslate({
source: sourceLang[i][j],
group: i,
key: j,
to: item.lang
})
)
if (taskList.length === 10 || (i === lastGroup && j === lastKey)) {
await Promise.allSettled(taskList).then(res => {
res.forEach(d => {
if (d.status === 'fulfilled') {
const { group, key, to, text } = d.value
if (to === item.lang) {
if (!item.result[group]) {
item.result[group] = {}
}
item.result[group][key] = text
}
}
})
}).catch(() => {})
taskList = []
}
}
}
fs.writeJSON(path.join(__dirname, `dist/translate-files/${item.filename}.json`), item.result, { spaces: 2 })
}
}
/**
* @param {String} source 要翻译的文本
* @param {Number} index 下标
* @param {String} to 目标语言
* @param {String} from 源语言
* @returns {Promise}
*/
function fetchApi({ source, group, key, to = 'en', from = 'zh-CN' }) {
return translate(source, {from, to: 'es', tld: 'cn'}).then(res => {
let text = res.text
text = text.replace(text[0], text[0].toLocaleUpperCase())
console.log(group, key, to, source, text)
return { group, key, to, text }
}).catch(err => {
console.error(err.statusCode);
});
}
async function cheerioPage({ source, group, key, to = 'en', from = 'zh-CN' }) {
return axios({
url: `https://translate.google.cn/m?sl=${from}&tl=${to}&q=${encodeURI(source)}`,
method: 'get'
}).then(async res => {
const html = res.data
const $ = cheerio.load(html)
let text = $('.result-container').text()
text = text.replace(text[0], text[0].toLocaleUpperCase())
console.log(group, key, to, source, text)
return { group, key, to, text }
}).catch(err => {
console.error(err);
})
}
setup()
3、替换源代码
这里我们使用中文做key,例如$t(‘登录成功’),有个好处是不用写注释了,而拿英文翻译转成驼峰命名做key可能会很长,而且到时候英文翻译变了key也会变,
替换代码时需要考虑vue的语法
- html标签:/>(\$t\(.*\))</
- 元素或组件属性:/ (title|label|alt|placeholder)="\$/
- 其次是字符串全部添加this上下文:/'(\$t\(.*\))'/
const fs = require('fs-extra');
const path = require('path');
const filePath = path.join(__dirname, 'src/vue-files')
const blacklist = ['api','assets','icons','locales','styles', 'vendor', 'linkage', 'combined-alarm']
// const blacklist = ['assets','styles','vendor', 'w-picker']
const fileExtReg = /\.(js|json|vue)$/
let i18nData = []
/**
* 提取excel,导出josn,替换源代码
*/
async function setup() {
const sourceLang = fs.readJSONSync(path.join(__dirname, 'dist/language-files/zh-CN.json'))
for(const i in sourceLang) {
for(const j in sourceLang[i]) {
i18nData.push({
group: i,
key: j,
value: sourceLang[i][j]
})
}
}
// 替换源代码
readdir(filePath)
}
/**
* 递归读取文件
* @param {String} filePath 目录
*/
async function readdir(filePath) {
const files = await fs.readdir(filePath).catch(err => {
console.error('Error:(readdir)', err)
})
for(let i = 0; i < files.length; i ++) {
const filename = files[i]
if(blacklist.includes(filename)) {
continue
}
//获取当前文件的绝对路径
const filedir = path.join(filePath, filename)
const stats = await fs.stat(filedir).catch(err => {
console.error('Error:(stat)', err)
})
// 是否是文件
const isFile = stats.isFile()
// 是否是文件夹
const isDir = stats.isDirectory()
if (isFile) {
if(fileExtReg.test(filename)) {
await readFile(filedir).catch(() => {})
}
}
// 如果是文件夹
if (isDir) {
await readdir(filedir)
}
}
}
/**
* 读取文件,替换$t
* @param {String} filedir 文件路径
*/
async function readFile(filedir) {
const targetPath = filedir.replace('src', 'dist')
console.log(targetPath)
const dataStr = await fs.readFile(filedir,'utf-8').catch(err => {
console.error('Error:(readFile)', err)
})
// /.*[\u4e00-\u9fa5]+.*/gi 匹配一整行
// /[\u4e00-\u9fa5]+{{ \w+ }}[\u4e00-\u9fa5]+|[\u4e00-\u9fa5]+\${\w+}[\u4e00-\u9fa5]+|(?<!\/\/\s.*|<!--\s.*|\*.*)[\u4e00-\u9fa5]+/gi 匹配${}、{{ }}
// /(?<!\/\/\s.*|<!--\s.*|\*.*)[\u4e00-\u9fa5]+/gi
const content = dataStr.replace(/(?<!\/\/\s.*|<!--\s.*|\*.*)[\u4e00-\u9fa5]+/gi, (re) => {
const findLocale = i18nData.find(item => item.value === re)
if(findLocale) {
const key = findLocale.key.replace(/\./g, '').trim()
const result = `$t('${findLocale.group}.${key}')`
return result
}
return re
}).replace(/>(\$t\(.*\))</gi, (match, re) => {
// 替换标签内容
return `>{{ ${re} }}<`
}).replace(/ (title|label|alt|placeholder)="\$/gi, (re) => {
// 替换属性
return ` :${re.trimStart()}`
}).replace(/'(\$t\(.*\))'/gi, (match, re) => {
// 替换JS
return 'this.' + re
})
await fs.ensureFile(targetPath) // 先创建目录
await fs.writeFile(targetPath, content)
}
setup()
4、集成vue-i18n
ant-design-vue、moment设置国际化
默认语音为中文简体,其他语音通过import
延迟或异步加载语言转换文件
axios请求头添加语言参数
import moment from 'moment'
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import zhCN from './lang/zh-CN/index'
import { getLocalStorage } from '@/utils'
Vue.use(VueI18n)
// 默认语言
export const defaultLang = 'zh-CN'
moment.updateLocale(zhCN.momentName, zhCN.momentLocale)
// 语言列表
export const locales = [
{ name: '简体中文', value: 'zh-CN' },
{ name: 'English', value: 'en-US' },
{ name: 'Español', value: 'es-ES' }
]
const messages = {
'zh-CN': {
...zhCN
}
}
/**
* 获取客户端语言
* @returns {String}
*/
export function getLanguage() {
const storeLanguage = getLocalStorage('language')
// 如果之前已经选择过语言,直接返回
if (storeLanguage) {
return storeLanguage
}
// 如果没有选择语言,则根据浏览器信息来设置默认语言
const language = (navigator.language || navigator.browserLanguage).toLowerCase()
for (const item of locales) {
if (item.value.toLowerCase().indexOf(language) > -1) {
return item.value
}
}
return defaultLang
}
const i18n = new VueI18n({
// set locale
// options like: en | zh | es
locale: defaultLang,
fallbackLocale: defaultLang,
// set locale messages
messages
})
const loadedLanguages = [defaultLang]
/**
* 设置i18n语言
* @param {String} lang 语言
* @returns {String}
*/
function setI18nLanguage(lang) {
i18n.locale = lang
document.querySelector('html').setAttribute('lang', lang)
return lang
}
/**
* 延迟或异步加载语言转换文件
* @param {String} lang 语言
* @sumary 一次加载所有翻译文件是过度和不必要的,加载新文件是通过import功能完成的
* @see https://kazupon.github.io/vue-i18n/zh/guide/lazy-loading.html
* @returns {Promise}
*/
export function loadLanguageAsync(lang = getLanguage()) {
// 如果语言相同
if (i18n.locale === lang) {
return Promise.resolve(setI18nLanguage(lang))
}
// 如果语言已经加载
if (loadedLanguages.includes(lang)) {
return Promise.resolve(setI18nLanguage(lang))
}
// 如果尚未加载语言
return import(/* webpackChunkName: "lang-[request]" */ `./lang/${lang}`).then(msg => {
const locale = msg.default
i18n.setLocaleMessage(lang, locale)
loadedLanguages.push(lang)
moment.updateLocale(locale.momentName, locale.momentLocale)
return setI18nLanguage(lang)
})
}
/**
* 外部JS文件使用i18n渲染
* @param {String} key 语言字典键名
* @returns {Function} i18n.t()
*/
export function i18nRender(key) {
return i18n.t(`${key}`)
}
export default i18n
5、检查词条使用情况
依赖vue-i18n-extract对 Vue.js 源代码运行静态分析以查找任何vue-i18n
用法:
- 报告语言文件中所有缺失的键。
- 报告语言文件中所有未使用的密钥。
- 可以选择将每个丢失的键写入您的语言文件。
pnpm install --save-dev vue-i18n-extract
作为Node.js脚本
// extract.js
const VueI18NExtract = require("vue-i18n-extract");
// @see https://github.com/Spittal/vue-i18n-extract
VueI18NExtract.createI18NReport({
vueFiles: "./dist/vue-files/**/*.?(js|vue)",
languageFiles: "./dist/language-files/*.?(json|yml|yaml)",
output: "./dist/extract-output.json",
// add: true, // 是否添加缺失的词条
// remove: true, // 是否移除没有用到的词条
exclude: [ // remove忽略的key
"all.",
"example.go"
],
});
将源代码放到vue-files目录下,语言文件放到language-files目录,建议使用json文件
执行node extract.js
还没有评论,快来抢第一吧