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

news2025/1/23 7:12:42

先来看最后的效果

在这里插入图片描述

问题

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

飞天使-学以致用-devops知识点1-安装gitlabharbor

文章目录 rpm 安装gitlab页面配置配置secretsecret 查看信息-chatgpt回复 为项目配置webhook,等jenkins部署完毕后在配置卸载 harbor配置secret所有k8s集群节点安装信任 http rpm 安装gitlab # 下载安装包 wget https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el7/gitla…

HP笔记本电脑如何恢复出厂设置?这里提供几种方法

要恢复出厂设置Windows 11或10的HP笔记本电脑,你可以使用操作系统的标准方法。如果你运行的是早期版本,你可以使用HP提供的单独程序清除计算机并重新安装操作系统。 恢复出厂设置运行Windows 11的HP笔记本电脑​ 所有Windows 11计算机都有一个名为“重置此电脑”的功能,可…

行为树入门:BehaviorTree.CPP Groot2练习(叶子节点)(2)

以《行为树BehaviorTree学习记录1_基本概念》练习。 1 SequenceNode顺序控制节点 代码下载 git clone https://gitee.com/Luweizhiyuan2020/ros2_bt.git例程 1.1 sequence 顺序执行 下载版本SequenceNode1。 1.2 ReactiveSequence 异步执行 注意&#xff1a; ①only a…

【k8s配置与存储--持久化存储(PV、PVC、存储类)】

1、PV与PVC 介绍 持久卷&#xff08;PersistentVolume&#xff0c;PV&#xff09; 是集群中的一块存储&#xff0c;可以由管理员事先制备&#xff0c; 或者使用存储类&#xff08;Storage Class&#xff09;来动态制备。 持久卷是集群资源&#xff0c;就像节点也是集群资源一样…

13.云原生之常用研发中间件部署

云原生专栏大纲 文章目录 mysql主从集群部署mysql高可用集群高可用互为主从架构互为主从架构如何实现主主复制中若是两台master上同时出现写操作可能会出现的问题该架构是否存在问题&#xff1f; heml部署mysql高可用集群 nacos集群部署官网文档部署nacoshelm部署nacos redis集…

Windows系统x86机器安装(麒麟、统信)ARM系统详细教程

本次介绍在window系统x86机器上安装国产系统 arm 系统的详细教程。 注:ubuntu 的arm系统安装是一样的流程。 1.安装环境准备。 首先,你得有台电脑,配置别太差,至少4核8G内存,安装window10或者11都行(为啥不能是Window7,你要用也不是不行,你先解决win7补丁更新问题)。…

高效网络采集实践:使用 Haskell 和 html-conduit 下载 www.baidu.com 视频完整教程

一、引言 网络采集在当今信息时代中发挥着关键作用&#xff0c;用于从互联网上获取数据并进行分析。本文将介绍如何使用 Haskell 进行网络编程&#xff0c;从数据采集到图片分析&#xff0c;为你提供一个清晰的指南。我们将探讨如何使用爬虫代理来确保高效、可靠的数据获取&am…

Kubernetes剖析

Kubernetes剖析 前言 ​ 容器技术这样一个新生事物&#xff0c;完全重塑了整个云计算市场的形态。它不仅催生出了一批年轻有为的容器技术人&#xff0c;更培育出了一个具有相当规模的开源基础设施技术市场。 ​ 在这个市场里&#xff0c;不仅有 Google、Microsoft 等技术巨擘…

爬取某牙视频

爬取页面链接&#xff1a;游戏视频_游戏攻略_虎牙视频 爬取步骤&#xff1a;点进去一个视频播放&#xff0c;查看media看有没有视频&#xff0c;发现没有。在xhr中发现有许多ts文件&#xff0c;但这种不是很长的视频一般都有直接的播放链接&#xff0c;所以目标还是找直接的链…

自定义神经网络一之Tensor和神经网络

文章目录 前言Tensor神经网络深度神经网络DNN卷积神经网络CNN卷积神经网络有2大特点 循环神经网络RNN残差网络ResNetTransformer自我注意力机制并行效率 总结 前言 神经网络是AI界的一个基础概念&#xff0c;当下火热的神经网络例如RNN循环神经网络或者CNN卷积神经网络&#x…

【数据结构和算法初阶(C语言)】链表-单链表(手撕详讲单链表增删查改)

目录 1.前言&#xff1a;顺序表回顾&#xff1a; 1.1顺序表的优缺点 2.主角----链表 2.1链表的概念 2.2定义一个单链表的具体实现代码方式 3.单链表对数据的管理----增删查改 3.1单链表的创建 3.2单链表的遍历实现 3.2.1利用遍历实现一个打印我们链表内容的函数的函数…

matlab 线性四分之一车体模型

1、内容简介 略 57-可以交流、咨询、答疑 路面采用公式积分来获得&#xff0c;计算了车体位移、非悬架位移、动载荷等参数 2、内容说明 略 3、仿真分析 略 线性四分之一车体模型_哔哩哔哩_bilibili 4、参考论文 略

SpringCloud微服务-Eureka注册中心

Eureka注册中心 文章目录 Eureka注册中心前言1、Eureka的作用2、搭建EurekaServer3、服务注册4、启动多个实例5、服务拉取 -实现负载均衡 前言 在服务调用时产生的问题&#xff1a; //2. 利用RestTemplate发起HTTP请求&#xff0c;查询user String url "http://localho…

排序算法之快速排序(挖坑法)

挖坑法的思想&#xff1a;记第一个数为key&#xff0c;要调整key的位置&#xff0c;使得左边的都要比key的小&#xff0c;右边的数都比key的大。 记录下关键字keybegin&#xff0c;把28那个位置挖坑holebegin 让end找到小于28&#xff08;key&#xff09;的数&#xff0c;把那…

docker-mysql:5.7安装

1、下载mysql:5.7镜像 [rootlocalhost ~]# docker search mysql (某个XXX镜像名字) [rootlocalhost ~]# docker pull mysql:5.7 按装之前查看一下是否按装过mysql。如果安装过会占用3306端口。 [rootlocalhost ~]# ps -ef | grep mysql 2、简单的安装 [rootlocalhost ~]# d…

STM32 +合宙1.54“ 电子墨水屏(e-paper)驱动显示示例

STM32 合宙1.54“ 电子墨水屏&#xff08;e-paper&#xff09;驱动显示示例 &#x1f4cd;相关篇《Arduino框架下ESP32/ESP8266合宙1.54“ 电子墨水屏&#xff08;e-paper&#xff09;驱动显示示例》&#x1f516;程序是从GooDisplay品牌下同型号规格墨水屏的示例程序参考Ardui…

计算1/1+1/2+1/3+1/4+1/5...+1/100的值(C语言实现)

如何实现打印小数呢 这里我们需要把数值定义成为float或者double的类型&#xff0c;因为如果是int的话&#xff0c;就会直接取整&#xff0c;输出的结果就会变成0 int main() {float sum 0;int flg 1;for (int i 1; i < 100; i){sum 1.0 / i * flg;flg -flg;}printf(&…

蓝桥杯前端Web赛道-课程列表

蓝桥杯前端Web赛道-课程列表 题目链接&#xff1a;0课程列表 - 蓝桥云课 (lanqiao.cn) 题目要求如下&#xff1a; 分析题目我们发现其实就是需要我们手写一个分页的功能&#xff0c;根据题目的要求&#xff0c;分析如下 需要通过axios获取数据每页显示5条数据&#xff0c;默…

深度学习 精选笔记(3)线性神经网络-线性回归

学习参考&#xff1a; 动手学深度学习2.0Deep-Learning-with-TensorFlow-bookpytorchlightning ①如有冒犯、请联系侵删。 ②已写完的笔记文章会不定时一直修订修改(删、改、增)&#xff0c;以达到集多方教程的精华于一文的目的。 ③非常推荐上面&#xff08;学习参考&#x…

黑马程序员——接口测试——day03——Postman断言、关联、参数化

目录&#xff1a; Potman断言 Postman断言简介Postman常用断言 断言响应状态码断言包含某字符串断言JSON数据Postman断言工作原理Postman关联 简介实现步骤核心代码创建环境案例1案例2Postman参数化 简介数据文件简介编写数据文件 CSV文件JSON文件导入数据文件到postman读取数…