- A+
所属分类:Web前端
做国际化一个很头疼的坑就是,你不知道项目里到底还有哪些中文词条没有国际化处理
纯靠人工去检查不现实,也不靠谱,而且浪费资源
所以还是得通过脚本工具来检查,思路是:
- 先保存好本地代码变更,准备好一个无文件变更的本地环境
- 再通过脚本把代码里的非展示性中文移除掉
- 注释里的中文、console 里的中文,已经国际化处理过的中文
- 再用中文正则在 vscode 的全局搜索里匹配,捞出来的就是未国际化处理的中文词条
- 最后需要回退本地的更改,毕竟脚本是直接改动本地文件
脚本仅仅是检查用,用完记得回退代码
匹配中文词条的正则
- 单个中文:
[u4E00-u9FFF]
- 连续中文:
[u4E00-u9FFF]+
- 掺杂了各种符号、字母的中文句子:
[a-zA-Z0-9、:]*[u4E00-u9FFF]+[u4E00-u9FFF.-*。,,a-zA-Z0-9/()()::”“!?、%_【】《》>~~ ]*
- (这里不建议把 : : - ' " 这几个特殊符号也列到正则里,因为这些符号比较特殊,有的语法层面也支持,列进来反而会引出新问题,所以宁愿这种场景的句子被截成多断)
- 最好再加上文件的排除:
*.css,*.scss,*.less,*.json,*.bat,privacyProtocal.html,userProtocal.html,*.md,webpack**.js,*.txt,*.svg,*.properties,*.npmrc,vve-i18n-cli.config.js,baas,config,*.art,demo_index.html,*.sh,*.xml,*.java
脚本
移除非展示性中文的脚本
// index.js #!/usr/bin/env node /** * 用来移除掉指定项目里的以下几类场景的中文: * - 注释里的中文 * - 被国际化全局函数包裹的中文 $t * * 这样子方便借助 vs code 的全局正则搜索中文功能,来快速 review 未国际化的中文 * 正则: [u4E00-u9FA5]+ */ "use strict"; const program = require("commander"); const { loadConfig } = require("../configuration"); const core = require("./core"); const vfs = require("vinyl-fs"); const map = require("map-stream"); const path = require("path"); const fs = require("fs"); function commaSeparatedList(value, split = ",") { return value.split(split).filter((item) => item); } program .version(require("../../package.json").version) .option("--cwd <path>", "工作目录") .option("--root-dir <path>", "国际文本所在的根目录") .option( "--config <path>", "配置文件的路径,没有配置,默认路径是在${cwd}/vve-i18n-cli.config.js" ) .option("--no-config", "是否取配置文件") .option( "--i18n-file-rules <items>", "匹配含有国际化文本的文件规则", commaSeparatedList ) .option( "--ignore-i18n-file-rules <items>", "不匹配含有国际化文本的文件规则", commaSeparatedList ) .parse(process.argv); const config = { // 工作目录 cwd: ".", // 根目录,国际文本所在的根目录 rootDir: "src", // 配置文件的路径,没有配置,默认路径是在${cwd}/vve-i18n-cli.config.js config: undefined, // 是否取配置文件 noConfig: false, // 匹配含有国际化文本的文件规则 i18nFileRules: ["**/*.+(vue|js|html|htm)"], // 不匹配含有国际化文本的文件规则 ignoreI18nFileRules: ["**/node_modules/**"], }; Object.assign(config, program); const CONFIG_JS_FILENAME = "vve-i18n-cli.config.js"; let 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 && conf.options && conf.options.zhCheck) { Object.assign(config, conf.options.zhCheck, program); } } } // 制定配置文件后,cwd在配置文件中定义,则cwd就需要重新获取 if (!program.cwd) { absoluteCwd = path.resolve(config.cwd); } const absoluteRootDir = path.resolve(absoluteCwd, config.rootDir); function run() { console.log("================================>start"); vfs .src( config.i18nFileRules.map((item) => path.resolve(absoluteRootDir, item)), { ignore: config.ignoreI18nFileRules.map((item) => path.resolve(absoluteRootDir, item) ), dot: false, } ) .pipe( map((file, cb) => { console.log("开始解析 =========================>", file.path); const extname = path.extname(file.path); let fileContent = file.contents.toString(); let newFileContent = fileContent; if (extname.toLowerCase() === ".vue") { newFileContent = core.removeUnusedZhInVue(fileContent); } else if (extname.toLowerCase() === ".js") { newFileContent = core.removeUnusedZhInJs(fileContent); } else if ([".html", ".htm"].includes(extname.toLowerCase())) { newFileContent = core.removeUnusedZhInHtml(fileContent); } if (newFileContent !== fileContent) { console.log("发现无用的中文,正在移除中..."); fs.writeFileSync(file.path, newFileContent); } console.log("解析结束 =========================>", file.path); cb(); }) ) .on("end", () => { console.log("================================>end"); }); } run();
// core.js // 包含中文 const zhReg = new RegExp("[\u4E00-\u9FFF]+", ""); // 处理 vue 文件 function removeUnusedZhInVue(fileContent) { return removeUnusedZh(fileContent); } exports.removeUnusedZhInVue = removeUnusedZhInVue; // 处理 js 文件 function removeUnusedZhInJs(fileContent) { return removeUnusedZh(fileContent); } exports.removeUnusedZhInJs = removeUnusedZhInJs; // 处理 html 文件 // 处理 js 文件 function removeUnusedZhInHtml(fileContent) { return removeUnusedZh(fileContent); } exports.removeUnusedZhInHtml = removeUnusedZhInHtml; function removeUnusedZh(fileContent) { const hasAnnotation = { "/*": false, "<!--": false, }; // 逐行处理 fileContent = fileContent .split("n") .map((line) => { // 移除无用中文 if (line.match(zhReg)) { const regs = [ new RegExp("//(.*[\u4E00-\u9FFF]+)", ""), // 移除 // xx new RegExp("console.log\(['"](.*[\u4E00-\u9FFF]+)", ""), // 移除 console.log(xxx) new RegExp("console.info\(['"](.*[\u4E00-\u9FFF]+)", ""), // 移除 console.info(xxx) new RegExp( "\$t\([ ]*['"`](.*?[\u4E00-\u9FFF]+.*?)['"`]\)", "" ), // 移除 $t("xxx") ]; regs.forEach((reg) => { let match = line.match(reg); while (match && match[1]) { line = line.replace(match[1], ""); match = line.match(reg); } }); } if (!hasAnnotation["/*"] && line.indexOf("/*") > -1) { hasAnnotation["/*"] = true; } if (!hasAnnotation["<!--"] && line.indexOf("<!--") > -1) { hasAnnotation["<!--"] = true; } return line; }) .join("n"); if (hasAnnotation["/*"]) { // 移除 /* xxx */ const reg = new RegExp("/\*([\s\S]*?)\*/", "g"); fileContent = fileContent.replace(reg, function (match, key, index) { // console.log("[/**/] ==1 >", { match, key, index }); let newKey = key; while (newKey.match(zhReg)) { newKey = newKey.replace(zhReg, ""); } return match.replace(key, newKey); }); } // 移除 <!-- xxx --> if (hasAnnotation["<!--"]) { const reg = new RegExp("<!--([\s\S]*?)-->", "g"); fileContent = fileContent.replace(reg, function (match, key, index) { let newKey = key; while (newKey.match(zhReg)) { newKey = newKey.replace(zhReg, ""); } return match.replace(key, newKey); }); } return fileContent; }
// configuration.js 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, };
{ "dependencies": { "commander": "^3.0.2", "debug": "^4.1.1", "jsonfile": "^5.0.0", "lodash.uniq": "^4.5.0", "map-stream": "0.0.7", "pinyin-pro": "^3.11.0", "translation.js": "^0.7.9", "vinyl-fs": "^3.0.3", "xlsx": "^0.18.5" }, "devDependencies": { "chai": "^4.2.0", "mocha": "^6.2.1", "nyc": "^14.1.1", "shelljs": "^0.8.3", "standard-version": "^7.0.0" }, "version": "3.2.3" }
// vve-i18n-cli.config.js module.exports = { // 工作目录 cwd: ".", // 根目录,国际文本所在的根目录 rootDir: "demo", // 默认所有模块,如果有传module参数,就只处理某个模块 // '**/module-**/**/index.js' moduleIndexRules: ["*/pro.properties"], // 匹配含有国际化文本的文件规则 i18nFileRules: ["**/*.+(vue|js)"], // 国际化文本的正则表达式,正则中第一个捕获对象当做国际化文本 i18nTextRules: [/(?:[$.])t(['"](.+?)['"]/g], // 模块的国际化的json文件需要被保留下的key,即使这些组件在项目中没有被引用 // key可以是一个字符串,正则,或者是函数 keepKeyRules: [ /^G/+/, // G/开头的会被保留 ], ignoreKeyRules: [/^el/], // 生成的国际化资源包的输出目录 outDir: "i18n", // 生成的国际化的语言 i18nLanguages: [ "zh", // 中文 "en", // 英文 ], // 是否翻译 translate: false, // 翻译的基础语言,默认是用中文翻译 translateFromLang: "zh", // 是否强制翻译,即已翻译修改的内容,也重新用翻译生成 forceTranslate: false, // 翻译的语言 translateLanguage: ["zh", "en"], // 模块下${outDir}/index.js文件不存在才拷贝index.js copyIndex: true, // 是否强制拷贝最新index.js forceCopyIndex: false, // 国际化文本包裹相关 zhWrap: { cwd: ".", // 根目录,国际文本所在的根目录 rootDir: ".", i18nFileRules: [ "!(node_modules|config)/**/*.+(vue)", // "base/components/login.vue", "base/common/js/httpHandle.js", ], ignorePreReg: [ /ts*(s*$/, /tls*(s*$/, /console.(?:log|error|warn|info|debug)s*(s*$/, new RegExp("//.+"), ], // js相关文件需要引入的国际化文件 i18nImportForJs: "import i18n from '@inap_base/i18n/core'", // js相关文件需要使用国际化方法 jsI18nFuncName: "i18n.t", // vue相关文件需要使用的国际化方法 vueI18nFuncName: "$t", }, };
硬替换脚本
具体查看 zh-i18n.zip