小工具知识网

当前位置: 首页 > 行业知识 > 解决i18n国际化可读性问题,傻瓜式webpack中文支持国际化插件开发

行业知识

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

2026-11-02 07:58:31 来源:互联网转载

先来看最后的效果

问题

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

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

// webpack.config.jsvar 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 方法来绑定,也可以用 tapAsync 或 tapPromise 这两个异步方法来绑定。

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

      Language插件开发

      流程

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

      源码

      //languagePlugin.jsconst 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 => {                    // 文件类型是js才做检测和替换                    var fileType = filename.split('.').pop()                    if(fileType==='js' && compilation.assets[filename] && compilation.assets[filename].source) {                        // 获取到源码                        var source = compilation.assets[filename].source();                        var newVal = source                        var reg = /((i18n\.t)|($t))\((\)*(\'|\")(.+?)(\)*(\'|\")\)/g                        // 替换源码                        newVal = newVal.replace(reg, function(val) {                            let str = val.replace(/((i18n\.t)|($t))\((\)*(\'|\")/g,'').replace(/(\)*(\'|\")\)/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')            // 提取{}之间的有效内容            langFileContent = langFileContent.match(/\{[\S\s]+\}/g)[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) => {            // 删除没有使用到的key            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) {                    content += '\n"' + key + '":"' + keyFileItem.val[key] + '",'                }                // 去掉最后一个逗号                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 => {                            // 获取中文并且获取crc                            let 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.val                                if(!obj[hashKey]) {                                    obj[hashKey] = str                                    this.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版本的插件,不过这是以后的事情啦。。。公司的国际化改造告一段落,撒花~

上一篇:邪神下载秘籍,神秘资源一键获取!

下一篇:CSS虚线样式详解,轻松打造吸引人的界面元素!