解决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 => {
                    // 文件类型是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版本的插件,不过这是以后的事情啦。。。公司的国际化改造告一段落,撒花~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/414095.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

云里物里轻薄系列电子价签,如何革新零售?

云里物里的DS轻薄系列电子价签&#xff0c;凭借轻巧外观和强劲性能&#xff0c;为零售行业提供了更便捷的商品改价方案。这不仅是对纸质价标的替代&#xff0c;更以其安全性和可持续发展性&#xff0c;实现对零售行业的效率升级&#xff0c;让商家们轻松迎接数字化时代的挑战&a…

可视化图文报表

Apache Echarts介绍 Apache Echarts是一款基于Javascript的数据可视化图表库&#xff0c;提供直观&#xff0c;生动&#xff0c;可交互&#xff0c;可个性化定制的数据可视化图表。 官网&#xff1a;Apache ECharts 入门案例&#xff1a; <!DOCTYPE html> <html>…

Firefox Focus,一个 “专注“ 的浏览器

近期才开始使用 Firefox Focus&#xff0c;虽然使用频率其实并不高&#xff0c;基本上只有想到了才去用&#xff0c;但每次使用的体验都很不错。 Firefox Focus 这款浏览器大约在 2015 年首次发布&#xff0c;不同于一般版本的 Firefox&#xff0c;它主打“自动删除浏览记录”…

数据结构:树/二叉树

一、树的概念 逻辑结构&#xff1a;层次结构&#xff0c;一对多 节点&#xff1a;树中的一个数据元素根节点&#xff1a;树中的第一个节点&#xff0c;没有父节点孩子节点&#xff1a;该节点的直接下级节点父(亲)节点&#xff1a;该结点的直接上级节点兄弟节点&#xff1a;有…

机器学习-02-机器学习算法分类以及在各行各业的应用

总结 本系列是机器学习课程的第02篇&#xff0c;主要介绍机器学习算法分类以及在各行各业的应用 本门课程的目标 完成一个特定行业的算法应用全过程&#xff1a; 定义问题&#xff08;Problem Definition&#xff09; -> 数据收集(Data Collection) -> 数据分割(Data…

初识Maven

介绍&#xff1a; web后端开发技术ApacheMaven是一个项目管理和构建工具&#xff0c;它基于项目对象模型&#xff08;POM&#xff09;的概念&#xff0c;通过一小段描述信息来管理项目的构建。安装&#xff1a;http://maven.apache.org/ Apache软件基金会&#xff0c;成立于19…

新能源汽车出海潮起,智能驾驶方案成差异化优势

2023年&#xff0c;中国汽车产销分别达3016.1万辆和3009.4万辆&#xff0c;巨大的规模之下是激烈的品牌竞争。由整车企业引领&#xff0c;汽车产业链的电动化智能化转型逐渐倒逼企业自行开拓成长空间。转型力度偏小、产品更新较慢的海外市场&#xff0c;成为蕴含金矿的待开掘目…

电子电器架构新趋势 —— 最佳着力点:域控制器

电子电器架构新趋势 —— 最佳着力点&#xff1a;域控制器 我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师&#xff08;Wechat&#xff1a;gongkenan2013&#xff09;。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师…

mac电脑监控软件哪个好

在Mac电脑使用日益普及的今天&#xff0c;企业对于Mac终端的安全管理需求也日益增长。Mac电脑监控软件作为一种有效的管理工具&#xff0c;能够帮助企业提高数据安全性和员工工作效率。 在众多Mac电脑监控软件中&#xff0c;域智盾软件以其卓越的功能和性能脱颖而出&#xff0c…

【办公类-21-04】20240227单个word按“段落数”拆分多个Word(三级育婴师操作参考题目 有段落文字和表格 1拆13份)

作品展示 背景需求&#xff1a; 最近学育婴师&#xff0c;老师发了一套doc操作参考 但是老师是一节节授课的&#xff0c;每节都有视频&#xff0c;如果做在一个文档里&#xff0c;会很长很长&#xff0c;容易找不到。所以我需要里面的单独文字的docx。 以前的方法是 1、打开源…

论文阅读:SOLOv2: Dynamic, Faster and Stronger

目录 概要 Motivation 整体架构流程 技术细节 小结 论文地址&#xff1a;[2003.10152] SOLOv2: Dynamic and Fast Instance Segmentation (arxiv.org) 代码地址&#xff1a;GitHub - WXinlong/SOLO: SOLO and SOLOv2 for instance segmentation, ECCV 2020 & NeurIPS…

逆变器专题(10)-电流环控制参数设计

相应仿真原件请移步资源下载 对跟网型逆变器来说&#xff0c;电流环的PI参数设计尤其重要 如上图所示为电流环解耦控制模型 而电压、电流采样和计算都是在开关周期的中间时刻进行&#xff0c;SVPWM调制出的磁矢量需要在一个开关周期进行作用&#xff0c;因此&#xff0c;整个逆…

2024年腾讯云4核8G12M配置的轻量服务器同时支持多大访问量?

腾讯云4核8G服务器支持多少人在线访问&#xff1f;支持25人同时访问。实际上程序效率不同支持人数在线人数不同&#xff0c;公网带宽也是影响4核8G服务器并发数的一大因素&#xff0c;假设公网带宽太小&#xff0c;流量直接卡在入口&#xff0c;4核8G配置的CPU内存也会造成计算…

swagger-ui.html报错404,解决办法

swagger-ui.html报错404,解决办法&#xff01;现在后端开发项目中&#xff0c;为了节省时间&#xff0c;使用swagger插件&#xff0c;可以方便的快捷生成接口文档。但是如果你在请求前端页面路径比如&#xff1a;http://127.0.0.1:7777/swagger-ui.html。找不到。那是因为你的配…

Nginx网络服务六-----IP透传、调度算法和负载均衡

1.实现反向代理客户端 IP 透传 就是在日志里面加上一个变量 Module ngx_http_proxy_module [rootcentos8 ~]# cat /apps/nginx/conf/conf.d/pc.conf server { listen 80; server_name www.kgc.org; location / { index index.html index.php; root /data/nginx/html/p…

unity shaderGraph实例-物体线框显示

文章目录 本项目基于URP实现一&#xff0c;读取UV网格&#xff0c;由自定义shader实现效果优缺点效果展示模型准备整体结构各区域内容区域1区域2区域3区域4shader属性颜色属性材质属性后处理 实现二&#xff0c;直接使用纹理&#xff0c;使用默认shader实现优缺点贴图准备材质准…

振弦采集仪在高速公路岩土工程中的监测与评估

振弦采集仪在高速公路岩土工程中的监测与评估 河北稳控科技振弦采集仪是一种常用于结构振动监测的仪器&#xff0c;可以用于高速公路岩土工程中的监测与评估。它的原理是通过测量结构振动引起的振弦的变形来反映结构的振动情况。 在高速公路岩土工程中&#xff0c;振弦采集仪可…

【主题广范|见刊快】2024年电力电气与机械,能源工程国际会议(ICPEMEE 2024)

【主题广范|见刊快】2024年电力电气与机械&#xff0c;能源工程国际会议&#xff08;ICPEMEE 2024&#xff09; 重要信息 会议官网&#xff1a;http://www.icpemee.com会议地址&#xff1a;合肥截稿日期&#xff1a;2024.03.10召开日期&#xff1a;2024.03.20 &#xff08;先投…

图论基础(一)

一、图论 图论是数学的一个分支&#xff0c;它以图为研究对象。图论中的图是若干给定的点&#xff08;顶点&#xff09;以及连接两点的线&#xff08;边&#xff09;构成的图像&#xff0c;这种图形通常用来描述某些事物之间的某种特定关系&#xff0c;用点代表事物&#xff0c…

Springboot+vue的考务报名平台(有报告)。Javaee项目,springboot vue前后端分离项目。

演示视频&#xff1a; Springbootvue的考务报名平台&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot vue前后端分离项目。 项目介绍&#xff1a; 本文设计了一个基于Springbootvue的前后端分离的考务报名平台&#xff0c;采用M&#xff08;model&#xff0…