解决i18n国际化可读性问题,傻瓜式webpack中文支持国际化插件开发

本文主要是介绍解决i18n国际化可读性问题,傻瓜式webpack中文支持国际化插件开发,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

先来看最后的效果

在这里插入图片描述

问题

  1. 用过国际化i18n的朋友都知道,天下苦国际化久矣,尤其是中文为母语的开发者,在面对代码中一堆的$t('abc.def')这种一点也不直观毫无可读性的代码,根本不知道自己写了啥

    (如上图,你看得出来这是些啥吗)

  2. 第二个问题就是i18n各种语言版本的语言包难以维护,随着项目变大这个语言包会越来越难以维护,能不能自动去维护呢

解决思路

所以我们前端组的小伙伴就想了个办法,能不能直接$t('中文')呢,就像下图:

这样是不是就方便看了,但是问题依然有,使用中文做key可能会在打包时乱码或者在浏览器查看下乱码,总归就是直接使用中文不安全。

因此我们想出了一个万全之策

  1. 针对以前做了国际化的项目,写node扫描一遍src目录,找出所有$t('xxxx')替换成$t('对应中文')
  2. 由于需要改的项目是vue2编写,所以写webpack插件做以下事:
    • 在打包开始前扫描src目录,找到 $t('对应中文')
    • 使用crc32将中文转为加密后的key,然后将'key': '对应中文'自动追加到语言包文件中,对应的语言包会长这样:
    • 在打包结束后,扫描打包后的文件,将$t('对应中文')修改为$t(key),打包后的$t会长这样:

这样就不担心乱码问题了,而且可以自动维护语言包

将源码中的英文键替换成中文键

这一步之前没有写国际化的项目不用执行。
这一步只需要执行一次即可,因此不写进webpack插件中去,直接写nodejs脚本,具体步骤如下:

  1. 扫描src下所有文件夹,这个步骤需要用到递归,如果是文件夹就继续往下扫描,用正则表达式找出 $t('xxxx')i18n.t('xxxx')这样的字符串
  2. 从之前的中文语言包中找出对应的中文并替换进源码
// replaceLang.js
const path = require('path')
const fs = require("fs");let zhLang = require("./src/utils/languages/zh.js"); // 扫描文件的根路径
let gFilePath = resolve('/src')
// 需要扫描的文件
let gExtension = ['.js','.vue','.ts','.tsx','.jsx']function resolve(dir){return path.join(__dirname,dir)//path.join(__dirname)设置绝对路径
}// 提取多级嵌套结构中的中文
function getValueByAttrs(attrs){let str = '',langObj = zhLang;attrs.forEach(item=>{str = langObj[item]if(typeof str == 'object'){langObj = langObj[item]}})return str
}/*** 替换文件夹下的英文键* * folderPath: 需要扫描替换的文件夹* extension: 需要替换的文件后缀集合*/
function replaceLangs(folderPath, extension){// 读取文件夹下文件const files = fs.readdirSync(folderPath,'utf8')files.forEach((fileName) => {const filePath = path.join(folderPath, fileName)const stats = fs.statSync(filePath)if(stats.isDirectory()) {// 如果该目录是文件夹,继续往下扫描this.replaceLangs(filePath,extension)}else if(stats.isFile()) {// 如果该目录是文件,进一步判断文件类型if(extension.includes(path.extname(fileName).toLowerCase())) {//读取文件内容const fileContent = fs.readFileSync(filePath, 'utf8')// 用正则表达式找出 `$t('xxxx')` 和 `i18n.t('xxxx')`这样的字符串let results = fileContent.match(/\$t\((.+?)\)/g)||[]let results2 = fileContent.match(/i18n.t\((.+?)\)/g)||[]results.concat(results2).forEach(info=>{let regex = /(?<=\()(.+?)(?=\))/g;  let attr = info.match(regex)[0]try{let attrs = eval(attr).split('.')||[];// 从之前的语言包中获取对应的中文let str = getValueByAttrs(attrs)if(str){if(info.includes('i18n.t')){fileContent = fileContent.replace(info,"i18n.t('"+str+"')")}else{fileContent = fileContent.replace(info,"$t('"+str+"')")}}}catch(e){console.log(e)}})// 更新文件fs.writeFileSync(filePath, fileContent)}}})
}replaceLangs(gFilePath, gExtension)

webpack插件开发基础知识

可以参考插件开发文档
插件向第三方开发者提供了 webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以在 webpack 构建流程中引入自定义的行为。

插件可以做些什么

可以在关键时间点执行一些逻辑,要改变输出,取决于我们可以获取到什么,以及对它做些什么修改操作,比如我们可以去除注释,去除空格,合并代码,压缩文件,提取公共代码,改变配置,修改,改变输出等。

webpack插件组成

webpack插件由一下组成:

  • 一个JavaScript命名函数JavaScript类
  • 在插件函数的prototype上定义一个 apply 方法。
  • 指定一个绑定到webpack自身的事件钩子。
  • 处理webpack内部实例的特定数据。
  • 功能完成后调用webpack提供的回调。
插件基本架构

插件是由「具有 apply 方法的 prototype 对象」所实例化出来的。这个 apply 方法在安装插件时,会被 webpack compiler 调用一次。apply 方法可以接收一个 webpack compiler 对象的引用,从而可以在回调函数中访问到 compiler 对象。一个插件结构如下:

class HelloWorldPlugin {apply(compiler) {compiler.hooks.done.tap('Hello World Plugin',(stats /* 绑定 done 钩子后,stats 会作为参数传入。 */) => {console.log('Hello World!');});}
}module.exports = HelloWorldPlugin;

然后,要安装这个插件,只需要在你的 webpack 配置的 plugin 数组中添加一个实例:

// webpack.config.js
var HelloWorldPlugin = require('hello-world');module.exports = {// ... 这里是其他配置 ...plugins: [new HelloWorldPlugin({ options: true })],
};
Compiler

Compiler 负责编译,贯穿webpack的整个生命周期,Compiler 对象包含了当前运行Webpack的配置,包括entry、output、loaders等配置,这个对象在启动Webpack时被实例化,而且是全局唯一的。Plugin可以通过该对象获取到Webpack的配置信息进行处理。

常用钩子:

  • beforeRun:
    在开始执行一次构建之前调用,compiler.run 方法开始执行后立刻进行调用。
  • watchRun:
    在监听模式下,一个新的 compilation 触发之后,但在 compilation 实际开始之前执行。
  • compilation:
    compilation 创建之后执行。
  • emit:
    输出 asset 到 output 目录之前执行。
  • done:
    在 compilation 完成时执行。这个钩子 不会 被复制到子编译器。
Compilation

Compilation对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 Compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,简单来讲就是把本次打包编译的内容存到内存里。Compilation 对象也提供了插件需要自定义功能的回调,以供插件做自定义处理时选择使用拓展。
简单来说,Compilation的职责就是构建模块和Chunk,并利用插件优化构建过程。

Compiler 和 Compilation 的区别

Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译,只要文件有改动,compilation就会被重新创建。

注意

有些插件钩子是异步的。我们可以像同步方式一样用 tap 方法来绑定,也可以用 tapAsynctapPromise 这两个异步方法来绑定。

  • 当我们用 tapAsync 方法来绑定插件时,必须 调用函数的最后一个参数 callback 指定的回调函数。
  • 当我们用 tapPromise 方法来绑定插件时,必须 返回一个 pormise ,异步任务完成后 resolve

Language插件开发

流程

  1. 在编译开始前扫描src目录下的所有文件,将 $t('中文') 字符串找到,将其通过crc32加密得到key,并追加到语言包中
  2. 检测到文件变化时,重新执行步骤1,更新语言包
  3. 编译完成后,输出到dist目录前,将打包好的文件中的 $t('中文') 换成 $t('key'),再输出到目标目录

源码

//languagePlugin.js
const path = require('path')
const { crc32 } = require('crc')
const fs = require("fs");// 扫描文件的根路径
let gFilePath = resolve('/src')
// 需要扫描的文件
let gExtension = ['.js','.vue','.ts','.tsx','.jsx']function resolve(dir){return path.join(__dirname,dir)//path.join(__dirname)设置绝对路径
}class LanguagePlugin {constructor(config) {this.config = {// 指定中文语言包zh: resolve(config.zh),// 需要生成的语言包,注意需要包含中文语言包langs: config.langs.map(path => resolve(path))}// 中文语言包内容this.zh = {}// 所有语言包内容this.keyFileList = []// key引用计数,引用为0的key会被删除this.keyUseNumber = {}}apply(compiler) {compiler.hooks.run.tap('LanguagePluginRun',() => {this.saveZhToCrc32JSON()})compiler.hooks.watchRun.tap('LanguagePluginWatch',() => {this.saveZhToCrc32JSON()})compiler.hooks.emit.tapAsync('LanguagePlugin', (compilation, callback) => {const now = Date.now()const zh = this.zh// 检索每个(构建输出的)chunk:compilation.chunks.forEach(chunk => {// 检索由 chunk 生成的每个资源(asset)文件名:chunk.files.forEach(filename => {// Get the asset source for each file generated by the chunk:var fileType = filename.split('.').pop()if(fileType==='js' && compilation.assets[filename] && compilation.assets[filename].source) {var source = compilation.assets[filename].source();var newVal = sourcevar reg = /((i18n\.t)|(\$t))\([\s\S]*?(\\)*(\'|\")(.+?)(\\)*(\'|\")[\s\S]*?\)/g// let matchArr = source.match(reg)// if(matchArr) {//     console.log(matchArr)// }newVal = newVal.replace(reg, function(val) {let str = val.replace(/((i18n\.t)|(\$t))\([\s\S]*?(\\)*(\'|\")/g,'').replace(/(\\)*(\'|\")[\s\S]*?\)/g,'').replace(/\"/g,'\"').replace(/\'/g,'\'')let hashKey = crc32(str).toString(16)if(zh[hashKey]) {let ret = val.replace(str, hashKey)return ret}else{return val}})compilation.assets[filename] = {source: function () {return newVal},size: function () {return newVal.length}}}});});console.log((Date.now() - now) / 1000)callback();});}saveZhToCrc32JSON() {this.keyFileList = []this.keyUseNumber = {}// 判断几个XXkey.js文件存不存在,如果不存在就创建一个this.config.langs.forEach(filePath => {const { dir, base } = path.parse(filePath);try {fs.accessSync(dir, fs.constants.F_OK)try {fs.accessSync(filePath, fs.constants.F_OK)} catch(err) {console.log(filePath + '不存在,将为您自动创建')fs.writeFileSync(filePath,"const lang = {\n}\nexport default lang")}} catch (err) {console.log(dir + '不存在,将为您自动创建')fs.mkdirSync(dir)fs.writeFileSync(filePath,"const lang = {\n}\nexport default lang")}})// 提取出langs文件夹下文件的内容this.config.langs.forEach(filePath => {let langFileContent = fs.readFileSync(filePath,'utf8')const result = langFileContent.match(/\{[\S\s]*\}/g)langFileContent = result ? result[0] : '{}'const obj = JSON.parse(langFileContent)const origin = JSON.parse(langFileContent)for(let key in obj) {// 去掉首尾空格obj[key] = obj[key].replace(/^\s*/g,'').replace(/\s*$/g,'')}this.keyFileList.push({path: filePath,val: obj,origin: JSON.stringify(origin)})for(let key in obj) {this.keyUseNumber[key] = 0}})// 更新langs文件this.updateLangsByFiles(gFilePath, gExtension)this.keyFileList.forEach((keyFileItem) => {// 删除没有使用到的for(let key in this.keyUseNumber) {if(this.keyUseNumber[key] === 0) {console.log("["+keyFileItem.val[key]+"]没有被使用,将为您自动删除")delete keyFileItem.val[key]}}if(JSON.stringify(keyFileItem.val) !== keyFileItem.origin) {console.log("更改了文件"+keyFileItem.path)let content = 'const lang = {'for(let key in keyFileItem.val) {const str = keyFileItem.val[key].replace(/\"/g,'\\"')content += '\n"' + key + '":"' + str + '",'}// 去掉最后一个逗号if(JSON.stringify(keyFileItem.val) !== '{}') {content = content.substring(0, content.length - 1)}content += '\n}\nexport default lang'fs.writeFileSync(keyFileItem.path, content)}if(this.config.zh === keyFileItem.path) {this.zh = keyFileItem.val}})}/*扫描文件更新语言包*/updateLangsByFiles(folderPath, extension) {const files = fs.readdirSync(folderPath,'utf8')files.forEach((fileName) => {const filePath = path.join(folderPath, fileName)const stats = fs.statSync(filePath)if(stats.isDirectory()) {this.updateLangsByFiles(filePath,extension)}else if(stats.isFile()) {if(extension.includes(path.extname(fileName).toLowerCase())) {const fileContent = fs.readFileSync(filePath, 'utf8')let results = fileContent.match(/((i18n\.t)|(\$t))\((\'|\")(.+?)(\'|\")\)/g)if(results) {results.forEach(result => {// 获取中文并且获取crclet str = result.replace(/((i18n\.t)|(\$t))\((\'|\")/g,'').replace(/(\'|\")\)/g,'').replace(/\"/g,'\\"').replace(/\'/g,"\\'")let hashKey = crc32(str).toString(16)this.keyFileList.forEach((keyFileItem) => {const obj = keyFileItem.valif(!obj[hashKey] && obj[hashKey]!=='') {obj[hashKey] = strthis.keyUseNumber[hashKey] = 0}this.keyUseNumber[hashKey]++})})}}}})}
}module.exports = LanguagePlugin;

插件使用

  1. 项目的根目录下添加languagePlugin.js
  2. package.json 的 devDependencies 加上 “crc”:“^4.3.2”,运行npm install安装crc依赖
  3. vue.config.js中使用该插件
configureWebpack(config) {return {plugins: [new LanguagePlugin({zh: '/src/langs/zhKey.js',langs: [// 这里有中文版,中文繁体版,英文版语音包'/src/langs/zhKey.js','/src/langs/zhTWKey.js','/src/langs/enKey.js']})]}
}
  1. 修改国际化i18n插件引入语言包的路径
  2. 运行npm run dev,能看到语言包自动更新,页面效果正常。
  3. 运行npm run build正常打包。

后记

这个插件其实不难,就是我的正则表达式水平不太行,后面我可能会专门花时间去学习正则表达式。
webpack的各种钩子我理解的不是很深刻,目前这个代码里的钩子都是我一个一个试出来的。
关于这个插件我其实还有一些想法想实现,比如可以调用AI翻译的API自动翻译出来对应的语言包,国际化从此以后完全傻瓜式啦。
还可以写个vite版本的插件,不过这是以后的事情啦。。。公司的国际化改造告一段落,撒花~

这篇关于解决i18n国际化可读性问题,傻瓜式webpack中文支持国际化插件开发的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/777107

相关文章

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

中文分词jieba库的使用与实景应用(一)

知识星球:https://articles.zsxq.com/id_fxvgc803qmr2.html 目录 一.定义: 精确模式(默认模式): 全模式: 搜索引擎模式: paddle 模式(基于深度学习的分词模式): 二 自定义词典 三.文本解析   调整词出现的频率 四. 关键词提取 A. 基于TF-IDF算法的关键词提取 B. 基于TextRank算法的关键词提取

Hadoop企业开发案例调优场景

需求 (1)需求:从1G数据中,统计每个单词出现次数。服务器3台,每台配置4G内存,4核CPU,4线程。 (2)需求分析: 1G / 128m = 8个MapTask;1个ReduceTask;1个mrAppMaster 平均每个节点运行10个 / 3台 ≈ 3个任务(4    3    3) HDFS参数调优 (1)修改:hadoop-env.sh export HDFS_NAMENOD

好题——hdu2522(小数问题:求1/n的第一个循环节)

好喜欢这题,第一次做小数问题,一开始真心没思路,然后参考了网上的一些资料。 知识点***********************************无限不循环小数即无理数,不能写作两整数之比*****************************(一开始没想到,小学没学好) 此题1/n肯定是一个有限循环小数,了解这些后就能做此题了。 按照除法的机制,用一个函数表示出来就可以了,代码如下

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

活用c4d官方开发文档查询代码

当你问AI助手比如豆包,如何用python禁止掉xpresso标签时候,它会提示到 这时候要用到两个东西。https://developers.maxon.net/论坛搜索和开发文档 比如这里我就在官方找到正确的id描述 然后我就把参数标签换过来

如何解决线上平台抽佣高 线下门店客流少的痛点!

目前,许多传统零售店铺正遭遇客源下降的难题。尽管广告推广能带来一定的客流,但其费用昂贵。鉴于此,众多零售商纷纷选择加入像美团、饿了么和抖音这样的大型在线平台,但这些平台的高佣金率导致了利润的大幅缩水。在这样的市场环境下,商家之间的合作网络逐渐成为一种有效的解决方案,通过资源和客户基础的共享,实现共同的利益增长。 以最近在上海兴起的一个跨行业合作平台为例,该平台融合了环保消费积分系统,在短

购买磨轮平衡机时应该注意什么问题和技巧

在购买磨轮平衡机时,您应该注意以下几个关键点: 平衡精度 平衡精度是衡量平衡机性能的核心指标,直接影响到不平衡量的检测与校准的准确性,从而决定磨轮的振动和噪声水平。高精度的平衡机能显著减少振动和噪声,提高磨削加工的精度。 转速范围 宽广的转速范围意味着平衡机能够处理更多种类的磨轮,适应不同的工作条件和规格要求。 振动监测能力 振动监测能力是评估平衡机性能的重要因素。通过传感器实时监