├── .DS_Store ├── .gitignore ├── README.md ├── autoi8nScheme.md ├── cli ├── bin │ └── index.js ├── command │ ├── collect.js │ ├── initFileConf.js │ └── restore.js ├── index.js └── utils │ ├── autoi18n.config.js │ ├── baseUtils.js │ ├── localeFile.js │ ├── log.js │ └── mergeIi8nConfig.js ├── core ├── index.js ├── restore │ └── index.js ├── transform │ ├── ast.js │ ├── index.js │ ├── transform.js │ ├── transformJs.js │ ├── transformReact.js │ └── transformVue.js └── utils │ ├── baseUtils.js │ ├── cacheCommentHtml.js │ ├── cacheCommentJs.js │ └── cacheI18nField.js ├── examples ├── react-autoi18n-cli │ ├── .gitignore │ ├── README.md │ ├── autoi18n.config.js │ ├── config │ │ ├── env.js │ │ ├── getHttpsConfig.js │ │ ├── jest │ │ │ ├── babelTransform.js │ │ │ ├── cssTransform.js │ │ │ └── fileTransform.js │ │ ├── modules.js │ │ ├── paths.js │ │ ├── pnpTs.js │ │ ├── webpack.config.js │ │ └── webpackDevServer.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── scripts │ │ ├── build.js │ │ ├── start.js │ │ └── test.js │ ├── src │ │ ├── App.css │ │ ├── App.js │ │ ├── i18n.js │ │ ├── index.css │ │ ├── index.js │ │ ├── locales │ │ │ ├── en-us.json │ │ │ └── zh-cn.json │ │ ├── logo.svg │ │ ├── reportWebVitals.js │ │ └── setupTests.js │ └── yarn.lock ├── react-autoi18n-loaders │ ├── .gitignore │ ├── README.md │ ├── autoi18n.config.js │ ├── config │ │ ├── env.js │ │ ├── getHttpsConfig.js │ │ ├── jest │ │ │ ├── babelTransform.js │ │ │ ├── cssTransform.js │ │ │ └── fileTransform.js │ │ ├── modules.js │ │ ├── paths.js │ │ ├── pnpTs.js │ │ ├── webpack.config.js │ │ └── webpackDevServer.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── scripts │ │ ├── build.js │ │ ├── start.js │ │ └── test.js │ ├── src │ │ ├── App.css │ │ ├── App.js │ │ ├── i18n.js │ │ ├── index.css │ │ ├── index.js │ │ ├── locales │ │ │ ├── en-us.json │ │ │ └── zh-cn.json │ │ ├── logo.svg │ │ ├── reportWebVitals.js │ │ └── setupTests.js │ └── yarn.lock ├── vue-autoi18n-cli │ ├── .gitignore │ ├── .prettierrc.js │ ├── README.md │ ├── autoi18n.config.js │ ├── babel.config.js │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── src │ │ ├── App.vue │ │ ├── assets │ │ │ └── logo.png │ │ ├── components │ │ │ └── HelloWorld.vue │ │ ├── i18n.js │ │ ├── locales │ │ │ ├── en-us.json │ │ │ └── zh-cn.json │ │ └── main.js │ └── vue.config.js └── vue-autoi18n-loaders │ ├── .gitignore │ ├── .prettierrc.js │ ├── README.md │ ├── autoi18n.config.js │ ├── babel.config.js │ ├── package.json │ ├── public │ ├── favicon.ico │ └── index.html │ ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── i18n.js │ ├── locales │ │ ├── en-us.json │ │ └── zh-cn.json │ └── main.js │ └── vue.config.js ├── loaders └── index.js └── package.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaneyxs/autoi18n/9252a85c25415f13a5131128ff570bdb5e28a2b9/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | /logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # parcel-bundler cache (https://parceljs.org/) 61 | .cache 62 | 63 | # Nuxt generate 64 | dist 65 | 66 | # vuepress build output 67 | .vuepress/dist 68 | 69 | # Serverless directories 70 | .serverless 71 | 72 | # IDE / Editor 73 | .idea 74 | 75 | # Service worker 76 | sw.* 77 | 78 | # macOS 79 | .DS_Store 80 | 81 | # Vim swap files 82 | *.swp 83 | 84 | package-lock.json 85 | 86 | yarn-lock.json 87 | 88 | dist.zip 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # autoi18n 2 | 3 | [![](https://img.shields.io/badge/npm-v1.0.8-blue)](https://www.npmjs.com/package/autoi18n-tool) 4 | 5 | ## 介绍 6 | 7 | 1. 自动转换、基于`Command`或者`webpack loader`的前端国际化方案 8 | 2. 目的实现前端国际化自动化、自动提取项目中的中文字符生成资源文件 9 | 3. 实现项目侵入式与非侵入式自动国际化,大大提高国际化的开发效率 10 | 4. 目前该库支持[vue](https://cn.vuejs.org/)和[react](https://react.docschina.org/) 11 | 12 | ## 为什么要写该库 13 | 14 | 如果一个老项目迭代了多年,有上百个页面,如果忽然说要做国际化:fearful:,这时我估计你心里会万马奔腾。 15 | 16 | 1. 手动提取工作量大,还很容易遗漏 17 | 2. 手动做国际化,会改变源码,源码上连个中文标识都没有,这样我们想找到点击按钮都很困难 18 | 19 | ## 安装 20 | 21 | ```bash 22 | npm i -D autoi18n-tool 23 | 24 | yarn add -D autoi18n-tool 25 | ``` 26 | 27 | ## 使用 28 | 29 | > 使用方式有两种,第一种:使用`Command`的形式生成国际化资源和替换国际化的原文件,第二种:使用`Command`的形式生成国际化资源使用`webpack loader`的形式无侵入式自动国际化。 30 | 31 | ### 纯命令的形式 32 | 33 | ```shell 34 | npx autoi18n init # 初始化自动国际化配置,生成国际化配置文件和生成国际化资源文件 35 | npx autoi18n sync -r # 同步国际化资源和替换原文件国际化字段 36 | ``` 37 | 38 | 执行命令会写入 39 | 40 | ```html 41 |
{{$t('a018615b35588a01')}}
42 | ``` 43 | 44 | 资源文件 45 | 46 | ```js 47 | // zh-cn.json 48 | { 49 | "dbefd3ada018615b35588a01e216ae6e": "你好,世界" // key是根据中文生成16的MD5 50 | } 51 | // en-us.js 52 | { 53 | "dbefd3ada018615b35588a01e216ae6e": "Hello, world" // key是根据中文生成16的MD5 54 | } 55 | ``` 56 | 57 | ### webpack loader的形式 不会改变源码 58 | 59 | 第一步:初始化自动国际化 60 | 61 | ```shell 62 | npx autoi18n init # 初始化自动国际化配置,生成国际化配置文件和生成国际化资源文件 63 | npx autoi18n sync # 同步国际化资源 64 | ``` 65 | 66 | 第二步:配置webpack loader 67 | 68 | ```js 69 | module.exports = { 70 | // ... 其他配置 71 | module:{ 72 | rules:[ 73 | { 74 | enforce: 'pre', // 此项一定要加上 优先执行的loader 75 | test: /\.(js|mjs|jsx|ts|tsx|vue)$/, 76 | use: [ 77 | { 78 | loader: 'autoi18n', 79 | options: {} 80 | } 81 | ], 82 | exclude: /node_modules/ 83 | } 84 | ] 85 | } 86 | } 87 | ``` 88 | 89 | *HTML* 90 | 91 | ```html 92 |
你好,世界
93 | ``` 94 | 95 | *资源文件* 96 | 97 | ```js 98 | // zh-cn.json 99 | { 100 | "dbefd3ada018615b35588a01e216ae6e": "你好,世界" // key是根据中文生成16的MD5 101 | } 102 | // en-us.js 103 | { 104 | "dbefd3ada018615b35588a01e216ae6e": "Hello, world" // key是根据中文生成16的MD5 105 | } 106 | ``` 107 | 108 | **页面在中文下展示为** 109 | 110 | 你好世界 111 | 112 | **在英文下展示为** 113 | 114 | Hello, world 115 | 116 | ## 组成部分 117 | 118 | > 该库分为两部分,一部分是cli,目的是通过命令生成资源文件和替换源文件的国际化字段,另一部分是webpack loader,目的是无侵入式的替换源文件的国际化字段,我们最好在打包测试/上线前执行以下cli命令,生成资源文件,然后拷贝一份资源文件给翻译组进行各国语言的翻译 119 | 120 | ### cli 121 | 122 | ```shell 123 | npx autoi18n init # 初始化自动国际化配置,这个命令会生成国际化配置文件和生成国际化资源文件 124 | npx autoi18n sync # 同步国际化资源文件 125 | npx autoi18n sync -r # 同步国际化资源文件并且会写入源文件 注意:这个命令会修改源码 -r 其实就是 replace 是否替换国际化字段 126 | npx autoi18n restore -f ./src/locales/zh-cn.ts # 根据指定的配置文件恢复代码中的国际化文案 如果存在多余的国际化文案数据,可以先恢复,重新执行 npx autoi18n sync -r 自动国际化操作,就不用手动去除多余的字段了 127 | npx autoi18n -h # 查看使用帮助 128 | npx autoi18n -V # 查看版本 129 | ``` 130 | 131 | 执行`npx autoi18n init`会在项目根目录生成`autoi8n.config.js`配置文件 132 | 133 | ```js 134 | module.exports = { 135 | /** 136 | * 需要国际化的语言种类 137 | */ 138 | language: ['zh-cn', 'en-us'], 139 | /** 140 | * 国际化资源文件应用的 模块模式 根据这个模式 使用 module.exports 或者 export default 141 | * 如果localeFileExt 配置为json时 此配置不起效 142 | */ 143 | modules: 'es6', 144 | /** 145 | * 需要国际化的目录 146 | */ 147 | entry: ['./src'], 148 | /** 149 | * 国际化资源文件输出目录 150 | */ 151 | localePath: './src/locales', 152 | /** 153 | * 国际化文件类型 默认 为 .json文件 支持.js和.json 154 | */ 155 | localeFileExt: '.json', 156 | /** 157 | * 需要处理国际化的文件后缀 158 | */ 159 | extensions: [], 160 | /** 161 | * 需要排除国际化的文件 glob模式数组 162 | */ 163 | exclude: [], 164 | /** 165 | * 要忽略做国际化的方法 166 | */ 167 | ignoreMethods: ['i18n.t', '$t'], 168 | /** 169 | * 要忽略做标签属性 170 | */ 171 | ignoreTagAttr: ['class', 'style', 'src', 'href', 'width', 'height'], 172 | /** 173 | * 国际化对象方法,可以自定义使用方法返回 注意:如果改变国际化方法记得把该方法加到ignoreMethods忽略列表里面 174 | */ 175 | i18nObjectMethod: 'i18n.t', 176 | /** 177 | * 国际化方法简写模式,可以自定使用方法返回 注意:如果改变国际化方法记得把该方法加到ignoreMethods忽略列表里面 178 | */ 179 | i18nMethod: '$t', 180 | /** 181 | * 如果不喜欢又臭又长的key 可以自定义国际化配置文件的key 182 | * 默认为 false 不自定义 183 | */ 184 | setMessageKey: false, 185 | /** 186 | * 生成md5的key长度 true: 32位字符 false: 16位字符 187 | */ 188 | maxLenKey: false, 189 | /** 190 | * 国际化要注入到js里面的实例 会在js文件第一行注入 191 | */ 192 | i18nInstance: "import i18n from '~/i18n'", 193 | /** 194 | * 格式化文件配置 195 | */ 196 | prettier: { 197 | singleQuote: true, 198 | trailingComma: 'es5', 199 | endOfLine: 'lf', 200 | } 201 | } 202 | ``` 203 | 204 | ### webpack loader 205 | 206 | 在webpack配置文件加入loader配置 207 | 208 | ```js 209 | module.exports = { 210 | // ... 其他配置 211 | module:{ 212 | rules:[ 213 | { 214 | enforce: 'pre', // 此项一定要加上 优先执行的loader 215 | test: /\.(js|mjs|jsx|ts|tsx|vue)$/, 216 | use: [ 217 | { 218 | loader: 'autoi18n', 219 | options: {} 220 | } 221 | ], 222 | exclude: /node_modules/ 223 | } 224 | ] 225 | } 226 | } 227 | ``` 228 | 229 | 230 | 231 | > **注意** 232 | > 233 | > 1. 在vue模板上不支持模板字符串嵌套模板字符串使用,js正则没有平衡组的概念,目前没有很好的处理方案 234 | > 2. 每次有新的中文字段加入需要使用`npx autoi8n sync`进行国际化资源同步,所以建议在打包项目前执行同步操作 235 | 236 | 项目还在完善中,欢迎大家pr,如果你觉得不错也欢迎给个start :smile::smile::smile: 237 | 238 | # License 239 | 240 | [MIT](https://opensource.org/licenses/MIT) -------------------------------------------------------------------------------- /autoi8nScheme.md: -------------------------------------------------------------------------------- 1 | # 自动国际化方案探究 2 | 3 | > [叶兴胜](https://github.com/Gertyxs)/ 2021-8-30 4 | 5 | ## 前言 6 | 7 | 1. 为什么要做国际化自动化? 8 | 2. 国际化自动化有什么好处,解决了什么问题? 9 | 3. 怎么做国际化自动化? 10 | 11 | 让我们带着这些问题进入本文的探索。 12 | 13 | ## 一、为什么要做国际化自动化? 14 | 15 | **场景1:** 16 | 17 | 假如说你公司有一个项目,该项目已经迭代了五六年,页面有一两百个,有一天,你领导说要做国际化,让你评估一下开发时间。此时我猜你的心情是这样的: 18 | 19 | ![](http://img.doutula.com/production/uploads/image/2017/10/15/20171015037242_gNfleB.jpg) 20 | 21 | 内心旁白: 22 | 23 | 不干了? 24 | 25 | 辞职? 26 | 27 | 想想贫穷的自己,还是稳稳的加班吧! 28 | 29 | 问题暴露出:手动国际化枯燥无味,工作量大,容易漏掉国际化字段,开发效率低下。 30 | 31 | **场景2** 32 | 33 | 如果手动做国际化基本上都要修改源码,被改过的源码基本上连一个中文标识符都找不到,有时候找一个按钮都得先到国际化资源文件找到对应的key,这种侵入式的手动国际化导致加大了维护成本。 34 | 35 | 基于以上两个场景: 36 | 37 | **我们要做国际化自动化!!!** 38 | 39 | ## 二、国际化自动化有什么好处,解决了什么问题? 40 | 41 | 1. 高效的提高了开发效率 42 | 2. 自动提取国际化字段不易遗漏 43 | 3. 可以通过无侵入式国际化自动化使项目易于维护 44 | 45 | ## 三、该怎么做国际化自动化 46 | 47 | ### 1、我们来分析一下怎么做国际化自动化 48 | 49 | 1. 自动提取代码中的国际化字段 50 | 2. 自动翻译代码中的国际化字段(由于谷歌和百度翻译api调用有次数和字数限制,而且翻译不理想)我们采用手动翻译 51 | 3. 自动替换代码中的国际化字段 52 | 53 | > 国际化处理的是代码中的中文字符,中文字符在代码中其实就是字符串,所以我们归根结底就是要处理代码中的字符串。 54 | 55 | ### 2、获取项目的代码 56 | 57 | 可以通过[glob](https://github.com/isaacs/node-glob#readme)获取所以代码文件然后通过`fs.readFileSync`模块来读取源码 58 | 59 | ```js 60 | const getSourceFiles = () => { 61 | const sourceFiles = glob.sync(`${curEntry}/**/*.{js,ts,vue,jsx,tsx}`, { ignore: [] }) 62 | return targetFiles 63 | } 64 | // 获取所有入口文件路劲 65 | let targetFiles = getSourceFiles() 66 | // 开始读取文件进行操作 67 | for (let i = 0; i < targetFiles.length; i++) { 68 | const sourceCode = fs.readFileSync(targetFiles[i].filePath, 'utf8') 69 | const code = transform({ code: sourceCode, targetFile: targetFiles[i], options, messages }) 70 | } 71 | ``` 72 | 73 | 读取到源码后我们就可以对其进行操作了 74 | 75 | ### 3、代码中的字符该怎么处理 76 | 77 | #### 3.1 JavaScript中的字符 78 | 79 | ```js 80 | let str1 = '我是字符串1' 81 | let str2 = "我是字符串2" 82 | let str3 = `我是字符串3` 83 | ``` 84 | 85 | 对于`JavaScript`,只要处理带有中文的字符串就行了,处理这些字符我们可以使用`RegExp`匹配或者[bable ast](https://astexplorer.net/)树解析`JavaScript`,在[autoi8n](https://github.com/Gertyxs/autoi18n)中我使用`ast`解析进行处理。 86 | 87 | ```js 88 | const babel = require('@babel/core') 89 | const generate = require('@babel/generator').default 90 | const traverse = require('@babel/traverse').default 91 | 92 | const ast = babel.parseSync(code, transformOptions) 93 | const makeVisitor = () => { 94 | return { 95 | TemplateLiteral(path) { 96 | // ... 97 | }, 98 | StringLiteral(path) { 99 | // ... 100 | }, 101 | DirectiveLiteral(path) { 102 | // ... 103 | }, 104 | JSXText(path) { 105 | // ... 106 | }, 107 | JSXAttribute(path) { 108 | // ... 109 | } 110 | } 111 | } 112 | const visitor = makeVisitor({ code, options, messages, ext, codeType }) 113 | traverse(ast, visitor) 114 | const output = generate(ast, code) 115 | ``` 116 | 117 | #### 3.2 vue文件中的字符 118 | 119 | ```vue 120 | 126 | 131 | ``` 132 | 133 | 在`vue`单文件组织中可以看出我们只要处理`template`以及`script`里面的代码即可 134 | 135 | 对于`vue`单文件组件我们可以使用官方提供的解析库[vue-template-compiler](https://github.com/vuejs/vue/tree/dev/packages/vue-template-compiler#readme)提取`template`和`JavaScript` 136 | 137 | ```js 138 | const compiler = require('vue-template-compiler') 139 | const sfc = compiler.parseComponent(code, { pad: 'space', deindent: false }) 140 | const { template, script, styles, customBlocks } = sfc 141 | ``` 142 | 143 | 上面就把`vue`单文件组织分成了`template`和`JavaScript`了。 144 | 145 | **template** 146 | 147 | 对于`template`我们只需要处理标签的静态属性、动态属性和标签内容即可,处理这些字符我们可以使用[parse5](https://www.npmjs.com/package/parse5)去解析标签或者用`RegExp`匹配,在[autoi8n](https://github.com/Gertyxs/autoi18n)中我使用`RegExp`匹配,如有需要后期会采用标签解析器处理。 148 | 149 | ```js 150 | const matchTagAttr = ({ code }) => { 151 | code = code.replace(/(<[^\/\s]+)([^<>]+)(\/?>)/gm, (match, startTag, attrs, endTag) => { 152 | return code 153 | }) 154 | return code 155 | } 156 | const matchTagContent = ({ code, options, ext, codeType, messages }) => { 157 | code = code.replace(/(>)([^><]*[\u4e00-\u9fa5]+[^><]*)(<)/gm, (match, beforeSign, value, afterSign) => { 158 | return code 159 | }) 160 | return code 161 | } 162 | ``` 163 | 164 | **JavaScript** 165 | 166 | 在上面我们已经讲解过`JavaScript`的处理了,在这里不再敖述。 167 | 168 | #### 3.3 React jsx中的字符 169 | 170 | ```jsx 171 | const render = () => { 172 | return ( 173 |
174 |
175 | {'我是react内容'} 176 | 我是标签静态内容 177 |
178 |
179 | ) 180 | } 181 | ``` 182 | 183 | 在`react`中,我们同样处理`JavaScript`字符串,`jsx`标签属性和标签内容,由于`jsx`本身支持[bable ast](https://astexplorer.net/)解析所以我们直接使用`ast`进行解析。 184 | 185 | **代码分析完成** 186 | 187 | 通过上面对js、vue、JavaScript、的代码分析,我们已经匹配到所有需要做国际化的字符,我们再把对应的字符抽出生成一个messages对象写进文件就完成了资源文件的生成了。然后把对应的字符替换成我们国际化设置的方法就大功告成了。 188 | 189 | #### 3.4 webpack loader 实现无侵入式的自动国国际化 190 | 191 | 实现了对代码处理之后,还可以在webpack loader里面把源码传进来进行无侵入式处理 192 | 193 | ```js 194 | const path = require('path') 195 | const fs = require('fs') 196 | const mergeIi8nConfig = require('../cli/utils/mergeIi8nConfig'); 197 | const { transform } = require('../core/index') 198 | let messages = {} 199 | 200 | module.exports = function (source) { 201 | const configOptions = mergeIi8nConfig() 202 | let targetFile = { ext: path.extname(this.resourcePath), filePath: this.resourcePath } 203 | source = transform({ code: source, targetFile, options: configOptions, messages }) 204 | messages = {} 205 | return source 206 | } 207 | ``` 208 | 209 | webpack loader 配置 210 | 211 | ```js 212 | { 213 | enforce: 'pre', // 此项一定要加上 优先执行的loader 214 | test: /\.(js|mjs|jsx|ts|tsx)$/, 215 | use: [ 216 | { 217 | loader: 'autoi18n', 218 | options: {} 219 | }], 220 | exclude: /node_modules/ 221 | } 222 | ``` 223 | 224 | 225 | 226 | ## 总结 227 | 228 | 整篇文章主要围绕怎么实现项目国际化自动化,希望本文可以给你带来国际化自动化的思路,总的来说国际化自动化就是处理代码中的字符串,可以通过代码解析器或者正则去匹配对应的字符,然后抽取出来替换成对应的国际化key,感谢你的阅读。 229 | 230 | 231 | 232 | 分享一个国际化自动化的库[autoi18n](https://github.com/Gertyxs/autoi18n) 233 | 234 | 项目还在完善中,欢迎大家pr,如果你觉得不错也欢迎给个start 😄😄😄 -------------------------------------------------------------------------------- /cli/bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('..') -------------------------------------------------------------------------------- /cli/command/collect.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const mergeIi8nConfig = require('../utils/mergeIi8nConfig') 3 | const prettier = require('prettier') 4 | const log = require('../utils/log') 5 | const baseUtils = require('../utils/baseUtils') 6 | const { transform } = require('../../core/index') 7 | const LocaleFile = require('../utils/localeFile') 8 | 9 | /** 10 | * 同步国际化配置文件并替换为对应的国际化字段 11 | * @param {*} programOption 命令行参数 12 | */ 13 | module.exports = async function (programOption) { 14 | // 合并配置文件 15 | const options = mergeIi8nConfig(programOption) 16 | 17 | // 指定目录类型错误 18 | if (!Array.isArray(options.entry) && typeof options.entry !== 'string') { 19 | log.error('entry must be a string or array'); 20 | process.exit(2); 21 | } 22 | 23 | // 没有指定国际化目录 24 | if (!options.entry || Array.isArray(options.entry) && options.entry.length <= 0) { 25 | log.error('no entry is specified'); 26 | process.exit(2); 27 | } 28 | 29 | // 国际化配置数据 30 | const messages = {} 31 | 32 | // 获取所有入口文件路劲 33 | let targetFiles = baseUtils.getSourceFiles(options) 34 | // 开始读取文件进行操作 35 | for (let i = 0; i < targetFiles.length; i++) { 36 | const sourceCode = fs.readFileSync(targetFiles[i].filePath, 'utf8'); 37 | let code = transform({ code: sourceCode, targetFile: targetFiles[i], options, messages }) 38 | if (programOption.replace) { 39 | code = prettier.format(code, baseUtils.getPrettierOptions(targetFiles[i].ext, options)) 40 | fs.writeFileSync(targetFiles[i].filePath, code, { encoding: 'utf-8' }) 41 | } 42 | log.success(`done: ${targetFiles[i].filePath}`) 43 | } 44 | 45 | // 创建生成国际化文件对象 46 | const localeFile = new LocaleFile(options.localePath) 47 | // 生成配置文件 48 | createTasks = options.language.map(locale => { 49 | let data = localeFile.getConf(locale, options) 50 | data = baseUtils.mergeMessages(data, messages) 51 | return localeFile.createConf(data, locale, options) 52 | }) 53 | log.success('生成国际化配置文件完成') 54 | 55 | } -------------------------------------------------------------------------------- /cli/command/initFileConf.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const inquirer = require('inquirer'); 4 | const prettier = require('prettier'); 5 | const log = require('../utils/log'); 6 | const defaultOptions = require('../utils/autoi18n.config') 7 | const LocaleFile = require('../utils/localeFile') 8 | const baseUtils = require('../utils/baseUtils') 9 | const { transform } = require('../../core/index') 10 | 11 | 12 | async function doInquire() { 13 | // 1. 配置文件是否存在 14 | let configExist = true; 15 | try { 16 | fs.accessSync('./autoi18n.config.js'); 17 | } catch (e) { 18 | configExist = false; 19 | } 20 | 21 | if (configExist) { 22 | const ans = await inquirer.prompt([ 23 | { 24 | name: 'overwrite', 25 | type: 'confirm', 26 | message: '配置文件 autoi18n.config.js 已存在,是否覆盖?', 27 | }, 28 | ]); 29 | 30 | if (!ans.overwrite) process.exit(0); 31 | } 32 | 33 | // 2. first i18n? 34 | let ans = await inquirer.prompt([ 35 | { 36 | name: 'firstI18n', 37 | type: 'confirm', 38 | message: '是否初次国际化?' 39 | }, 40 | ]); 41 | 42 | return ans; 43 | } 44 | 45 | module.exports = async function initFileConf(programOption) { 46 | const answers = await doInquire(); 47 | const { firstI18n } = answers; 48 | 49 | // 如果命令传入会覆盖配置文件 50 | const options = defaultOptions 51 | 52 | // 配置信息写入文件 53 | fs.writeFileSync( 54 | './autoi18n.config.js', 55 | prettier.format( 56 | 'module.exports = ' + JSON.stringify(options), { 57 | parser: 'babel', 58 | singleQuote: true, 59 | trailingComma: 'es5', 60 | } 61 | ), 62 | 'utf8' 63 | ); 64 | log.success('成功创建配置文件') 65 | 66 | // 创建生成国际化文件对象 67 | const localeFile = new LocaleFile(options.localePath) 68 | // 是否是第首次初始化国际化文件 69 | let createTasks = []; 70 | if (firstI18n) { 71 | // 首次国际化 72 | } else { 73 | // 非首次国际化,本地代码中已有国际化资源 74 | } 75 | 76 | // 国际化配置数据 77 | const messages = {} 78 | 79 | // 获取所有入口文件路劲 80 | let targetFiles = baseUtils.getSourceFiles(options) 81 | // 开始读取文件进行操作 82 | for (let i = 0; i < targetFiles.length; i++) { 83 | const sourceCode = fs.readFileSync(targetFiles[i].filePath, 'utf8'); 84 | const code = transform({ code: sourceCode, targetFile: targetFiles[i], options, messages }) 85 | log.success(`done: ${targetFiles[i].filePath}`) 86 | } 87 | 88 | // 生成配置文件 89 | createTasks = options.language.map(locale => { 90 | let data = localeFile.getConf(locale, options) 91 | data = baseUtils.mergeMessages(data, messages) 92 | return localeFile.createConf(data, locale, options); 93 | }) 94 | log.success(firstI18n ? '生成国际化配置文件完成' : '更新国际化配置文件完成'); 95 | }; 96 | -------------------------------------------------------------------------------- /cli/command/restore.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const mergeIi8nConfig = require('../utils/mergeIi8nConfig') 3 | const prettier = require('prettier') 4 | const log = require('../utils/log') 5 | const baseUtils = require('../utils/baseUtils') 6 | const { restore } = require('../../core/index') 7 | const LocaleFile = require('../utils/localeFile') 8 | 9 | /** 10 | * 恢复国际化字段为对应文案 11 | * @param {*} programOption 命令行参数 12 | */ 13 | module.exports = async function (programOption) { 14 | // 合并配置文件 15 | const options = mergeIi8nConfig(programOption) 16 | // 国际化配置文件路径 17 | const firstLocalePath = options.language && options.language[0] ? options.language[0] : 'zh-cn' 18 | 19 | // 没有指定国际化文件配置 20 | if (!firstLocalePath && !programOption.file) { 21 | log.error('Internationalization configuration file not found'); 22 | process.exit(2); 23 | } 24 | 25 | // 创建生成国际化文件对象 26 | const localeFile = new LocaleFile(options.localePath) 27 | 28 | // 国际化配置数据 29 | let messages = {} 30 | if (programOption.file) { 31 | messages = localeFile.getConf(firstLocalePath, options, programOption.file) 32 | } else { 33 | messages = localeFile.getConf(firstLocalePath, options) 34 | } 35 | 36 | // 获取所有入口文件路劲 37 | let targetFiles = baseUtils.getSourceFiles(options) 38 | // 开始读取文件进行操作 39 | for (let i = 0; i < targetFiles.length; i++) { 40 | const sourceCode = fs.readFileSync(targetFiles[i].filePath, 'utf8'); 41 | let code = restore({ code: sourceCode, targetFile: targetFiles[i], options, messages }) 42 | code = prettier.format(code, baseUtils.getPrettierOptions(targetFiles[i].ext, options)) 43 | fs.writeFileSync(targetFiles[i].filePath, code, { encoding: 'utf-8' }) 44 | log.success(`done: ${targetFiles[i].filePath}`) 45 | } 46 | } -------------------------------------------------------------------------------- /cli/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const program = require('commander') 3 | const initFileConf = require('./command/initFileConf') 4 | const collect = require('./command/collect') 5 | const package = require('../package') 6 | const restore = require('./command/restore') 7 | 8 | // 用法 版本说明 9 | program 10 | .version(package.version) // 定义版本 11 | .usage('') // 定义用法 12 | 13 | // 初始化配置文件 14 | program 15 | .command('init') // 定义命令 16 | .alias('i') // 命令别名 17 | .description('init locales conf') // 对命令参数的描述信息 18 | .action(function (options) { 19 | initFileConf(options) 20 | }) 21 | .on('--help', function () { 22 | console.log(' Examples:') 23 | console.log(' $ autoi18n init') 24 | }) 25 | 26 | // 同步国际化配置文件并替换为对应的国际化字段 27 | program 28 | .command('sync') // 定义命令 29 | .alias('s') // 命令别名 30 | .description('Synchronize the Chinese configuration to the internationalization profile') // 对命令参数的描述信息 31 | .option('-r, --replace', 'Replace Internationalization Fields') // 替换国际化字段 如果为true 会写入源文件 默认为false 32 | .option('-c, --config ', 'set config path. defaults to ./autoi18n.config.js') // 指定配置文件 33 | .action(function (options) { 34 | collect(options) 35 | }) 36 | .on('--help', function () { 37 | console.log(' Examples:'); 38 | console.log(' $ autoi18n sync') 39 | }) 40 | 41 | // 恢复国际化字段为对应文案 42 | program 43 | .command('restore') // 定义命令 44 | .alias('r') // 命令别名 45 | .description('Restore the internationalized field to the corresponding text') // 对命令参数的描述信息 46 | .option('-c, --config ', 'set config path. defaults to ./autoi18n.config.js') // 指定配置文件 47 | .option('-f, --file ', 'Internationalization configuration file path') // 国际化配置文件路径 默认会获取 language的第一个文件配置数据 48 | .action(function (options) { 49 | restore(options) 50 | }) 51 | .on('--help', function () { 52 | console.log(' Examples:') 53 | console.log(' $ autoi18n restore -f ./src/locales/zh-cn.ts') 54 | }) 55 | 56 | program.parse(process.argv) 57 | -------------------------------------------------------------------------------- /cli/utils/autoi18n.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * 需要国际化的语言种类 4 | */ 5 | language: ['zh-cn', 'en-us'], 6 | /** 7 | * 国际化资源文件应用的 模块模式 根据这个模式 使用 module.exports 或者 export default 8 | * 如果localeFileExt 配置为json时 此配置不起效 9 | */ 10 | modules: 'es6', 11 | /** 12 | * 需要国际化的目录 13 | */ 14 | entry: ['./src'], 15 | /** 16 | * 国际化资源文件输出目录 17 | */ 18 | localePath: './src/locales', 19 | /** 20 | * 国际化文件类型 默认 为 .json文件 支持.js和.json 21 | */ 22 | localeFileExt: '.json', 23 | /** 24 | * 需要处理国际化的文件后缀 25 | */ 26 | extensions: [], 27 | /** 28 | * 需要排除国际化的文件 glob模式数组 29 | */ 30 | exclude: [], 31 | /** 32 | * 要忽略做国际化的方法 33 | */ 34 | ignoreMethods: ['i18n.t', '$t'], 35 | /** 36 | * 要忽略做标签属性 37 | */ 38 | ignoreTagAttr: ['class', 'style', 'src', 'href', 'width', 'height'], 39 | /** 40 | * 国际化对象方法,可以自定义使用方法返回 注意:如果改变国际化方法记得把该方法加到ignoreMethods忽略列表里面 41 | */ 42 | i18nObjectMethod: 'i18n.t', 43 | /** 44 | * 国际化方法简写模式,可以自定使用方法返回 注意:如果改变国际化方法记得把该方法加到ignoreMethods忽略列表里面 45 | */ 46 | i18nMethod: '$t', 47 | /** 48 | * 如果不喜欢又臭又长的key 可以自定义国际化配置文件的key 49 | * 默认为 false 不自定义 50 | */ 51 | setMessageKey: false, 52 | /** 53 | * 生成md5的key长度 true: 32位字符 false: 16位字符 54 | */ 55 | maxLenKey: false, 56 | /** 57 | * 国际化要注入到js里面的实例 会在js文件第一行注入 58 | */ 59 | i18nInstance: "import i18n from '~/i18n'", 60 | /** 61 | * 格式化文件配置 62 | */ 63 | prettier: { 64 | singleQuote: true, 65 | trailingComma: 'es5', 66 | endOfLine: 'lf', 67 | } 68 | } -------------------------------------------------------------------------------- /cli/utils/baseUtils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const glob = require('glob') 3 | const path = require('path') 4 | const cwdPath = process.cwd() 5 | 6 | module.exports = { 7 | /** 8 | * 获取格式化配置 9 | * @param ext 格式化文件后缀 10 | * @param options 国际化配置 11 | * @returns 格式配置 12 | */ 13 | getPrettierOptions(ext, options) { 14 | const filePath = path.join(cwdPath, '.prettierrc.js') 15 | let prettier = {} 16 | if (fs.existsSync(filePath)) { 17 | try { 18 | prettier = require(filePath) 19 | prettier = { ...prettier, ...options.prettier } 20 | } catch (err) { 21 | prettier = options.prettier 22 | } 23 | } else { 24 | prettier = options.prettier 25 | } 26 | let parser = 'babel' 27 | if (ext === '.vue') { 28 | parser = 'vue' 29 | } 30 | if (['.ts', '.tsx'].includes(ext)) { 31 | parser = 'typescript' 32 | } 33 | prettier.parser = parser 34 | return prettier 35 | }, 36 | /** 37 | * 合并国际化文件数据 如果内容一致不以旧的数据为主 38 | * @param oldMessages 国际化配置文件目录 39 | * @param messages 排除的目录 40 | * @returns messages 新的国际化数据 41 | */ 42 | mergeMessages(oldMessages, messages) { 43 | const newMessages = oldMessages || {} 44 | Object.keys(messages).forEach((key) => { 45 | const keys = Object.keys(oldMessages) 46 | const values = Object.values(oldMessages) 47 | // 如果key相同 不写入当条数据 48 | if (!keys.includes(key)) { 49 | newMessages[key] = messages[key] 50 | } 51 | }) 52 | return newMessages 53 | }, 54 | 55 | /** 56 | * 获取需要处理国际化的文件 57 | * @param options.entry 国际化配置文件目录 58 | * @param options.exclude 排除的目录 59 | * @param options.extensions 处理文件的后缀 60 | */ 61 | getSourceFiles(options) { 62 | const localePath = path.resolve(__dirname, options.localePath) // 国际化存储路径 63 | const extensions = options.extensions && options.extensions.length ? options.extensions.join(',') : `js,ts,tsx,jsx,vue` 64 | const exclude = options.exclude ? [...options.exclude, `${localePath}/**/*.{js,ts,json}`] : [`${localePath}/**/*.{js,ts,json}`] 65 | let targetFiles = [].concat(options.entry).reduce((prev, curEntry, index) => { 66 | // 忽略国际化配置文件的目录 67 | const sourceFiles = glob.sync(`${curEntry}/**/*.{${extensions}}`, { ignore: exclude }) 68 | const files = sourceFiles.map(file => ({ 69 | filePath: file, 70 | curEntry: curEntry, 71 | ext: path.extname(file), 72 | })); 73 | return prev.concat(files); 74 | }, []); 75 | // 去掉 glob 文件重复 76 | targetFiles = this.duplicate(targetFiles, 'filePath') 77 | return targetFiles 78 | }, 79 | /** 80 | * 数组去重 81 | * @param array 需要去重的数组 82 | * @param key 排除重复用都的key 83 | */ 84 | duplicate(array, key) { 85 | const map = new Map(); 86 | const result = array.filter((item) => !map.has(item[key]) && map.set(item[key], 1)) 87 | return result 88 | } 89 | } -------------------------------------------------------------------------------- /cli/utils/localeFile.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const log = require('./log') 4 | 5 | const cwdPath = process.cwd() 6 | 7 | /** 8 | * 同步创建多级文件夹 9 | * @param {*} dirname 文件名 10 | * @returns 11 | */ 12 | const mkdirMultipleSync = dirname => { 13 | if (fs.existsSync(dirname)) { 14 | return true 15 | } else { 16 | if (mkdirMultipleSync(path.dirname(dirname))) { 17 | fs.mkdirSync(dirname) 18 | return true 19 | } 20 | } 21 | } 22 | 23 | module.exports = class LocaleFile { 24 | constructor(folder) { 25 | this.localesDir = folder 26 | } 27 | 28 | /** 29 | * 创建一个配置 30 | * @param {object} values KV值 31 | * @param {string} locale locales标识 32 | * @param {object} options 自动国际化配置对象 33 | */ 34 | createConf(values, locale, options) { 35 | const folder = this.localesDir.startsWith('/') ? this.localesDir : path.join(cwdPath, this.localesDir) 36 | try { 37 | fs.accessSync(folder) 38 | } catch (e) { 39 | mkdirMultipleSync(folder) 40 | } 41 | const localeFileExt = options.localeFileExt || '.json' 42 | const configFilePath = path.join(folder, `${locale}${localeFileExt}`) 43 | return new Promise((resolve, reject) => { 44 | let moduleIdent = options.modules === 'commonjs' ? 'module.exports = ' : 'export default ' 45 | moduleIdent = localeFileExt === '.json' ? '' : moduleIdent 46 | fs.writeFile(configFilePath, moduleIdent + JSON.stringify(values, null, 2), err => { 47 | if (err) { 48 | reject(err) 49 | } else { 50 | resolve(configFilePath) 51 | } 52 | }) 53 | }) 54 | } 55 | 56 | /** 57 | * 获取配置值 58 | * @param {string} locale key 59 | * @param {object} options 自动国际化配置对象 60 | * @param {object} fullPath 完整路劲 61 | */ 62 | getConf(locale, options, fullPath) { 63 | const localeFileExt = options.localeFileExt || '.json' 64 | let configFilePath = this.localesDir.startsWith('/') ? `${this.localesDir.replace(/\/$/, '')}/${locale}${localeFileExt}` : path.join(cwdPath, this.localesDir, `${locale}${localeFileExt}`) 65 | if (fullPath) { 66 | configFilePath = fullPath.startsWith('/') ? fullPath : path.join(cwdPath, fullPath) 67 | } 68 | let data = {} 69 | if (fs.existsSync(configFilePath)) { 70 | let content = fs.readFileSync(configFilePath, { encoding: 'utf-8' }) 71 | // 去除导出标识符 72 | content = (content || '').replace(/module\.exports\s*=\s*/, '').replace(/export default\s*/, '') 73 | // eval主要是js的解析器封装函数 74 | data = eval(`(${content})`) 75 | data = data ? data : {} 76 | } 77 | return data 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /cli/utils/log.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | 3 | module.exports = { 4 | info: msg => console.log(chalk.cyan(msg)), 5 | warning: msg => console.log(chalk.yellow(msg)), 6 | success: msg => console.log(chalk.green(msg)), 7 | error: msg => console.log(chalk.red(msg)), 8 | }; 9 | -------------------------------------------------------------------------------- /cli/utils/mergeIi8nConfig.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const defaultConfig = require('./autoi18n.config') 4 | const log = require('./log') 5 | 6 | const cwdPath = process.cwd() 7 | 8 | module.exports = function mergeOptions(programOption) { 9 | const options = defaultConfig; 10 | const configFileName = programOption && programOption.config || 'autoi18n.config.js' 11 | 12 | const configFilePath = path.join(cwdPath, configFileName) 13 | // 读取 autoi18n.config.js 中设置的参数,然后并入 options 14 | if (fs.existsSync(configFilePath)) { 15 | let configurationFile = {} 16 | try { 17 | configurationFile = require(configFilePath) 18 | } catch (err) { 19 | log.warning(`请检查 ${configFileName} 配置文件是否正确\n`) 20 | } 21 | 22 | Object.assign(options, configurationFile) 23 | } else { 24 | log.warning(`配置文件 ${configFileName} 不存在\n采用默认配置`) 25 | } 26 | 27 | return options; 28 | }; 29 | -------------------------------------------------------------------------------- /core/index.js: -------------------------------------------------------------------------------- 1 | exports.transform = require('./transform/index') 2 | exports.restore = require('./restore/index') 3 | -------------------------------------------------------------------------------- /core/restore/index.js: -------------------------------------------------------------------------------- 1 | const baseUtils = require('../utils/baseUtils') 2 | /** 3 | * 恢复不同的文件文案 4 | * @param {*} options.code 源代码 5 | * @param {*} options.targetFile 文件对象 6 | * @param {*} options.options 国际化配置对象 7 | * @param {*} options.messages 国际化字段对象 8 | * @returns code 经过恢复文案的代码 9 | */ 10 | module.exports = function ({ code, targetFile, options, messages }) { 11 | let ignoreMethods = options.ignoreMethods 12 | // 转义字符串 13 | ignoreMethods = ignoreMethods.map((item) => baseUtils.stringRegEscape(item)) 14 | const ident = ignoreMethods.join('|') 15 | code = code.replace(new RegExp(`(${ident})\\((['"\`])((((?!\\2|\\().)+))\\2[^(]*?\\)`, 'gm'), (match, method, sign , key) => { 16 | if (messages[key]) { 17 | return `${sign}${messages[key]}${sign}` 18 | } 19 | return match 20 | }) 21 | // 删除import 实例对象 22 | // code = code.replace(new RegExp(baseUtils.stringRegEscape(options.i18nInstance), 'gm'), '') 23 | return code 24 | } -------------------------------------------------------------------------------- /core/transform/ast.js: -------------------------------------------------------------------------------- 1 | const babel = require('@babel/core') 2 | const generate = require('@babel/generator').default 3 | const traverse = require('@babel/traverse').default 4 | const presetTypescript = require('@babel/preset-typescript').default 5 | const t = require('@babel/types') 6 | 7 | const pluginSyntaxJSX = require('@babel/plugin-syntax-jsx'); 8 | const pluginSyntaxProposalOptionalChaining = require('@babel/plugin-proposal-optional-chaining'); 9 | const pluginSyntaxClassProperties = require('@babel/plugin-syntax-class-properties'); 10 | const pluginSyntaxDecorators = require('@babel/plugin-syntax-decorators'); 11 | const pluginSyntaxObjectRestSpread = require('@babel/plugin-syntax-object-rest-spread'); 12 | const pluginSyntaxAsyncGenerators = require('@babel/plugin-syntax-async-generators'); 13 | const pluginSyntaxDoExpressions = require('@babel/plugin-syntax-do-expressions'); 14 | const pluginSyntaxDynamicImport = require('@babel/plugin-syntax-dynamic-import'); 15 | const pluginSyntaxExportExtensions = require('@babel/plugin-syntax-export-extensions'); 16 | const pluginSyntaxFunctionBind = require('@babel/plugin-syntax-function-bind'); 17 | 18 | const baseUtils = require('../utils/baseUtils') 19 | const { replaceStatement } = require('./transform') 20 | const log = require('../../cli/utils/log') 21 | 22 | /** 23 | * 返回处理ast对象 24 | * @param {*} 25 | * @returns 26 | */ 27 | const makeVisitor = ({ options, messages, ext, codeType }) => { 28 | // 生成字符对象 29 | const StringLiteral = (value) => { 30 | return Object.assign(t.StringLiteral(value), { extra: { raw: `'${value}'`, rawValue: value } }) 31 | } 32 | // 返回处理ast的对象 33 | return { 34 | /** 35 | * 处理模板字符串 36 | * @param {} path 37 | */ 38 | TemplateLiteral(path) { 39 | const { node } = path 40 | // 字符串模板内容 41 | node.quasis = (node.quasis || []).map((item) => { 42 | if (item.type === 'TemplateElement') { 43 | if (baseUtils.isChinese(item.value.raw)) { 44 | item.value.raw = `\${${replaceStatement({ value: item.value.raw, options, messages, ext, codeType })}}` 45 | } 46 | } 47 | return item 48 | }) 49 | // 字符串模板占位符内容 占位符内容可以不用做处理 50 | node.expressions = (node.expressions || []).map((item) => { 51 | if (item.type === 'StringLiteral') { 52 | if (baseUtils.isChinese(item.value)) { 53 | // item.extra.raw = `${item.value}` 54 | } 55 | } 56 | return item 57 | }) 58 | }, 59 | /** 60 | * 处理字符串字面量 61 | * @param {*} path 62 | */ 63 | StringLiteral(path) { 64 | const { node } = path 65 | if (baseUtils.isChinese(node.value)) { 66 | switch (path.parent.type) { 67 | case 'JSXAttribute': 68 | // 过滤掉这些属性不处理 69 | if (!options.ignoreTagAttr.includes(node.parent.name.name)) { 70 | node.extra.raw = replaceStatement({ value: node.value, options, messages, ext, codeType }) 71 | } 72 | break 73 | default: 74 | node.extra.raw = replaceStatement({ value: node.value, options, messages, ext, codeType }) 75 | break 76 | } 77 | } 78 | path.skip() // 跳过子节点 79 | }, 80 | /** 81 | * 处理 指令字符串字面量 'asdf' 82 | * @param {*} path 83 | */ 84 | DirectiveLiteral(path) { 85 | const { node } = path 86 | if (baseUtils.isChinese(node.value)) { 87 | node.extra.raw = replaceStatement({ value: node.value, options, messages, ext, codeType }) 88 | } 89 | }, 90 | /** 91 | * jsx 静态文本 92 | * @param {*} path 93 | */ 94 | JSXText(path) { 95 | const { node } = path 96 | if (baseUtils.isChinese(node.value)) { 97 | path.replaceWith(t.JSXExpressionContainer(StringLiteral(node.value))) 98 | } 99 | // path.skip() // 跳过子节点 100 | }, 101 | /** 102 | * jsx 属性 103 | * @param {*} path 104 | */ 105 | JSXAttribute(path) { 106 | const { node } = path 107 | // 如果属性是静态属性 108 | if (node.value && node.value.type === 'StringLiteral') { 109 | // 值是否包含中文 110 | if (baseUtils.isChinese(node.value.value)) { 111 | // 过滤特殊属性 112 | if (!options.ignoreTagAttr.includes(node.name.name)) { 113 | // 改为动态属性 114 | node.value = t.JSXExpressionContainer(StringLiteral(node.value.value)) 115 | } 116 | } 117 | } 118 | // path.skip() // 跳过子节点 119 | }, 120 | } 121 | } 122 | 123 | module.exports = function ({ code, file, options, messages, ext, codeType, lang = 'js' }) { 124 | // 生成ast配置 125 | const transformOptions = { 126 | sourceType: 'module', // 是否使用模块解析文件 127 | ast: true, // 是否生成ast树 128 | configFile: false, // 是否应用babel配置文件配置 129 | presets: lang === 'ts' ? [ [presetTypescript, { isTSX: true, allExtensions: true }]] : [], 130 | plugins: [ 131 | pluginSyntaxJSX, 132 | pluginSyntaxProposalOptionalChaining, 133 | pluginSyntaxClassProperties, 134 | [pluginSyntaxDecorators, { decoratorsBeforeExport: true }], 135 | pluginSyntaxObjectRestSpread, 136 | pluginSyntaxAsyncGenerators, 137 | pluginSyntaxDoExpressions, 138 | pluginSyntaxDynamicImport, 139 | pluginSyntaxExportExtensions, 140 | pluginSyntaxFunctionBind, 141 | ] 142 | } 143 | // 生成ast树 144 | let ast = null 145 | try { 146 | ast = babel.parseSync(code, transformOptions) 147 | } catch (error) { 148 | log.error(`文件${file.filePath} babel ast解析失败`) 149 | console.log(code) 150 | } 151 | // 返回转换对象 152 | const visitor = makeVisitor({ code, options, messages, ext, codeType }) 153 | // 开始转换 154 | traverse(ast, visitor) 155 | // 转换完成生成新的代码 retainLines:保留行 decoratorsBeforeExport:将true设置为在输出中导出之前打印装饰器 jsescOption: { minimal: true }: 防止转为Unicode 156 | const output = generate(ast, { retainLines: true, decoratorsBeforeExport: true, jsescOption: { minimal: true } }, code) 157 | code = output.code.replace(/;*$/, '') // 清空最后的分号 158 | return code 159 | } -------------------------------------------------------------------------------- /core/transform/index.js: -------------------------------------------------------------------------------- 1 | const transformVue = require('./transformVue') 2 | const transformReact = require('./transformReact') 3 | const transformJs = require('./transformJs') 4 | 5 | /** 6 | * 处理不同的文件转换 7 | * @param {*} options.code 源代码 8 | * @param {*} options.targetFile 文件对象 9 | * @param {*} options.options 国际化配置对象 10 | * @param {*} options.messages 国际化字段对象 11 | * @returns code 经过国际化的代码 12 | */ 13 | module.exports = function ({ code, targetFile, options, messages }) { 14 | let data = '' 15 | if (targetFile.ext === '.vue') { 16 | // 处理vue文件 17 | data = transformVue({ code, file: targetFile, ext: targetFile.ext, options, messages }) 18 | } else if (targetFile.ext === '.js' || targetFile.ext === '.ts') { 19 | // 处理js文件 20 | data = transformJs({ code, file: targetFile, ext: targetFile.ext, options, messages }) 21 | } else if (targetFile.ext === '.jsx' || targetFile.ext === '.tsx') { 22 | // 处理react文件 23 | data = transformReact({ code, file: targetFile, ext: targetFile.ext, options, messages }) 24 | } 25 | return data 26 | } 27 | -------------------------------------------------------------------------------- /core/transform/transform.js: -------------------------------------------------------------------------------- 1 | const { md5, formatWhitespace } = require('../utils/baseUtils') 2 | 3 | /** 4 | * 设置替换 5 | * @param {*} code 6 | */ 7 | const replaceStatement = ({ value, options, messages, ext, codeType, sign = "'" }) => { 8 | // 去掉首尾空白字符,中间的连续空白字符替换成一个空格 9 | value = formatWhitespace(value) 10 | // 生成key 11 | let key = md5(value, options.maxLenKey) 12 | // 是否自定义key 13 | if (options.setMessageKey && typeof options.setMessageKey === 'function') { 14 | key = options.setMessageKey({ key, value }) 15 | } 16 | messages[key] = value 17 | let i18nMethod = null 18 | // 类型为vue标签采用缩写国际化方法的形式 19 | if (codeType === 'vueTag') { 20 | i18nMethod = options.i18nMethod 21 | } else { 22 | // 其余情况使用对象的方法 23 | i18nMethod = options.i18nObjectMethod 24 | } 25 | // 如果是函数 26 | if (i18nMethod && typeof i18nMethod !== 'string') { 27 | return i18nMethod({ key, value, options, ext, sign }) 28 | } 29 | return `${i18nMethod}(${sign}${key}${sign})` 30 | } 31 | 32 | /** 33 | * 匹配字符串模块 34 | * @param {*} code 35 | */ 36 | const matchStringTpl = ({ code, options, messages, codeType, ext }) => { 37 | // 匹配存在中文的字符串模板内容 38 | code = code.replace(/(`)(((?!\1).)*[\u4e00-\u9fa5]+((?!\1).)*)\1/g, (match, sign, value) => { 39 | // 匹配占位符外面的内容 40 | const outValues = value 41 | .replace('`', '') 42 | .replace(/(\${)([^}]+)(})/gm, ',,') 43 | .split(',,') 44 | .filter(item => item) 45 | outValues.forEach(item => { 46 | value = value.replace(item, value => { 47 | // 是否是中文 48 | if (/[\u4e00-\u9fa5]+/g.test(value)) { 49 | value = `\${'${value}'}` 50 | } 51 | return value 52 | }) 53 | }) 54 | return `${sign}${value}${sign}` 55 | }) 56 | return code 57 | } 58 | 59 | /** 60 | * 匹配普通字符串 61 | * @param {*} code 62 | */ 63 | const matchString = ({ code, options, messages, ext, codeType }) => { 64 | // 替换所有包含中文的普通字符串 65 | code = code.replace(/(['"])(((?!\1).)*[\u4e00-\u9fa5]+((?!\1).)*)\1/gm, (match, sign, value) => { 66 | return replaceStatement({ value, options, messages, ext, codeType, sign }) 67 | }) 68 | return code 69 | } 70 | 71 | module.exports = { 72 | matchStringTpl, 73 | matchString, 74 | replaceStatement 75 | } 76 | -------------------------------------------------------------------------------- /core/transform/transformJs.js: -------------------------------------------------------------------------------- 1 | const cacheCommentJs = require('../utils/cacheCommentJs') 2 | const cacheI18nField = require('../utils/cacheI18nField') 3 | const ast = require('./ast') 4 | const baseUtils = require('../utils/baseUtils') 5 | 6 | /** 7 | * 转换js 8 | * @param {*} options.code 源代码 9 | * @param {*} options.file 文件对象 10 | * @param {*} options.options 国际化配置对象 11 | * @param {*} options.messages 国际化字段对象 12 | * @param {*} options.codeType 代码类型 13 | * @param {*} options.ext 文件类型 14 | * @returns 15 | */ 16 | module.exports = function ({ code, file, options, messages, lang, codeType = 'js', ext = '.js' }) { 17 | // 复制一份国际化数据配置 18 | const oldMessages = JSON.stringify(messages) 19 | // 暂存注释 20 | // code = cacheCommentJs.stash(code, options) ast替换的是字符串 所以可以不处理注释 21 | // 暂存已经设置的国际化字段 22 | code = cacheI18nField.stash(code, options) 23 | // 转换js 24 | lang = lang ? lang : ext === '.ts' ? 'ts' : 'js' 25 | code = ast({ code, file, options, messages, ext, codeType, lang }) 26 | // 恢复注释 27 | // code = cacheCommentJs.restore(code, options) 28 | // 恢复已经设置的国际化字段 29 | code = cacheI18nField.restore(code, options) 30 | // 国际化数据发生变化才注入 证明该js有国际化字段 31 | if (oldMessages !== JSON.stringify(messages)) { 32 | // 注入实例 33 | code = baseUtils.injectInstance({ code, ext, options }) 34 | } 35 | return code 36 | } 37 | -------------------------------------------------------------------------------- /core/transform/transformReact.js: -------------------------------------------------------------------------------- 1 | const cacheCommentJs = require('../utils/cacheCommentJs') 2 | const cacheI18nField = require('../utils/cacheI18nField') 3 | const ast = require('./ast') 4 | const baseUtils = require('../utils/baseUtils') 5 | 6 | /** 7 | * 转换react 8 | * @param {*} options.code 源代码 9 | * @param {*} options.options 国际化配置对象 10 | * @param {*} options.file 文件对象 11 | * @param {*} options.messages 国际化字段对象 12 | * @param {*} options.ext 文件类型 13 | * @returns 14 | */ 15 | module.exports = function ({ code, file, options, messages, ext = '.jsx' }) { 16 | // 复制一份国际化数据配置 17 | const oldMessages = JSON.stringify(messages) 18 | // 暂存注释 react 注释 就是js注释 ast替换的是字符串 所以可以不处理注释 19 | // code = cacheCommentJs.stash(code, options) 20 | // 暂存已经设置的国际化字段 21 | code = cacheI18nField.stash(code, options) 22 | // 转换react 23 | const lang = ['.ts', '.tsx'].includes(ext) ? 'ts' : 'js' 24 | code = ast({ code, file, options, messages, ext, codeType: 'jsx', lang }) 25 | // 恢复注释 26 | // code = cacheCommentJs.restore(code, options) 27 | // 恢复已经设置的国际化字段 28 | code = cacheI18nField.restore(code, options) 29 | // 国际化数据发生变化才注入 证明该js有国际化字段 30 | if (oldMessages !== JSON.stringify(messages)) { 31 | // 注入实例 32 | code = baseUtils.injectInstance({ code, ext, options }) 33 | } 34 | return code 35 | } 36 | -------------------------------------------------------------------------------- /core/transform/transformVue.js: -------------------------------------------------------------------------------- 1 | const transformJs = require('./transformJs') 2 | const cacheCommentHtml = require('../utils/cacheCommentHtml') 3 | const cacheI18nField = require('../utils/cacheI18nField') 4 | const { matchStringTpl, matchString } = require('./transform') 5 | const baseUtils = require('../utils/baseUtils') 6 | 7 | /** 8 | * 匹配vue标签中的属性 9 | * @param {*} code 10 | */ 11 | const matchTagAttr = ({ code, options, ext, codeType, messages }) => { 12 | code = code.replace(/(<[^\/\s]+)([^<>]+)(\/?>)/gm, (match, startTag, attrs, endTag) => { 13 | // 属性设置成vue的动态绑定 14 | attrs = attrs.replace(/([^\s]+)=(["'])(((?!\2).)*[\u4e00-\u9fa5]+((?!\2).)*)\2/gim, (match, attr, sign, value) => { 15 | if (attr.match(/^(v-|@)/) || options.ignoreTagAttr.includes(attr.trim())) { 16 | // 对于已经是v-开头的以及白名单内的属性,不进行替换 17 | return match 18 | } 19 | if (attr.indexOf(':') === 0) { 20 | // 对所有:开头的属性替换为v-bind: 模式 21 | return `v-bind${attr}=${sign}${value}${sign}` 22 | } else if(attr.indexOf('#') === 0) { 23 | return `${attr}=${sign}${value}${sign}` 24 | } else { 25 | // 对所有的字符串属性替换为v-bind:模式 26 | if (!['true', 'false'].includes(value) && isNaN(value)) { 27 | value = sign === '"' ? `'${value}'` : `"${value}"` 28 | } 29 | return `v-bind:${attr}=${sign}${value}${sign}` 30 | } 31 | }) 32 | // 通过对v-bind属性中包含有中文的部分进行国际化替换 33 | attrs = attrs.replace(/(v-bind:[^=]+=)(['"])(((?!\2).)+[\u4e00-\u9fa5]+((?!\2).)+)\2/gim, (match, attr, sign, value) => { 34 | // value = ast({ code: value, options, messages, ext }) // 防止性能问题 改用正则匹配 35 | // 匹配字符串模板 36 | value = matchStringTpl({ code: value, options, messages, codeType, ext }) 37 | // 进行字符串匹配替换 38 | value = matchString({ code: value, options, messages, codeType, ext }) 39 | // 替换属性为简写模式 40 | attr = attr.replace('v-bind:', ':') 41 | return `${attr}${sign}${value}${sign}` 42 | }) 43 | return `${startTag}${attrs}${endTag}` 44 | }) 45 | return code 46 | } 47 | 48 | /** 49 | * 匹配查找标签内容(包含中文的内容) 50 | * @param {*} code 51 | */ 52 | const matchTagContent = ({ code, options, ext, codeType, messages }) => { 53 | code = code.replace(/(>)([^><]*[\u4e00-\u9fa5]+[^><]*)(<)/gm, (match, beforeSign, value, afterSign) => { 54 | // 将所有不在 {{}} 内的内容,用 {{}} 包裹起来 55 | const outValues = value 56 | .replace(/({{)(((?!\1|}}).)+)(}})/gm, ',,') 57 | .split(',,') 58 | .filter(item => item) 59 | outValues.forEach(item => { 60 | value = value.replace(item, value => { 61 | // 是否是中文 62 | if (/[\u4e00-\u9fa5]+/g.test(value)) { 63 | value = `{{'${value.trim()}'}}` 64 | } 65 | return value 66 | }) 67 | }) 68 | // 对所有的{{}}内的内容进行国际化替换 69 | value = value.replace(/({{)((?:(?!\1|}}).)+)(}})/gm, (match, beforeSign, value, afterSign) => { 70 | // value = ast({ code: value, options, messages, ext }) // 防止性能问题 改用正则匹配 71 | // 匹配字符串模板 72 | value = matchStringTpl({ code: value, options, messages, codeType, ext }) 73 | // 进行字符串匹配替换 74 | value = matchString({ code: value, options, messages, codeType, ext }) 75 | return `${beforeSign}${value.trim()}${afterSign}` 76 | }) 77 | return `${beforeSign}${value.trim()}${afterSign}` 78 | }) 79 | return code 80 | } 81 | 82 | /** 83 | * 匹配vue模板部分 84 | * @param {*} code 85 | */ 86 | const matchVueTemplate = ({ code, options, ext, messages }) => { 87 | // 暂存注释 88 | code = cacheCommentHtml.stash(code, options) 89 | // 暂存已经设置的国际化字段 90 | code = cacheI18nField.stash(code, options) 91 | // 开始匹配 92 | code = code.replace(/(]*>)([\s\S]*)(<\/template>)/gim, (match, startTag, content, endTag) => { 93 | // 匹配模板里面待中文的属性 匹配属性 94 | content = matchTagAttr({ code: content, options, ext, codeType: 'vueTag', messages }) 95 | // 匹配模板里面标签包含中文的内容 匹配内容 96 | content = matchTagContent({ code: content, options, ext, codeType: 'vueTag', messages }) 97 | return `${startTag}${content.trim()}${endTag}` 98 | }) 99 | 100 | // 恢复注释 101 | code = cacheCommentHtml.restore(code, options) 102 | // 恢复已经设置的国际化字段 103 | code = cacheI18nField.restore(code, options) 104 | return code 105 | } 106 | 107 | /** 108 | * 匹配vue JavaScript部分 109 | * @param {*} code 110 | */ 111 | const matchVueJs = ({ code, options, file, ext, messages }) => { 112 | // 获取vue文件里面的script模板 113 | code = code.replace(/(]*>)([\s\S]*)(<\/script>)/gim, (match, startTag, content, endTag) => { 114 | let lang = startTag.match(/lang=(['"])(((?!\1).)+)\1/) 115 | lang = lang && lang[2] ? lang[2] : 'js' 116 | content = transformJs({ code: content, file, options, ext, codeType: 'vueJs', messages, lang }) 117 | return `${startTag}${content.trim()}${endTag}` 118 | }) 119 | return code 120 | } 121 | 122 | /** 123 | * 转换vue 124 | * @param {*} options.code 源代码 125 | * @param {*} options.file 文件对象 126 | * @param {*} options.options 国际化配置对象 127 | * @param {*} options.messages 国际化字段对象 128 | * @param {*} options.ext 文件类型 129 | * @returns 130 | */ 131 | module.exports = function ({ code, file, options, ext = '.vue', messages }) { 132 | // 处理模板 133 | code = baseUtils.handleNestedTags({ code, tagName: 'template' }, code => { 134 | return matchVueTemplate({ code, options, ext, messages }) 135 | }) 136 | // 处理js 137 | code = baseUtils.handleNestedTags({ code, tagName: 'script' }, code => { 138 | return matchVueJs({ code, file, options, ext, messages }) 139 | }) 140 | return code 141 | } -------------------------------------------------------------------------------- /core/utils/baseUtils.js: -------------------------------------------------------------------------------- 1 | const md5 = require('crypto-js/md5') 2 | 3 | module.exports = { 4 | /** 5 | * 是否是中文 6 | * @param value 内容 7 | */ 8 | isChinese(value) { 9 | return /[\u4e00-\u9fa5]/.test(value) 10 | }, 11 | /** 12 | * md5加密 13 | * @param value 加密参数 14 | */ 15 | md5(value, maxLenKey) { 16 | let ciphertext = md5(value).toString() 17 | if (!maxLenKey) { 18 | ciphertext = ciphertext.substring(8, 24) 19 | } 20 | return ciphertext 21 | }, 22 | /** 23 | * 匹配是否导入某个模块 es6 模式 24 | * @param moduleName 模块名称 25 | */ 26 | // matchImportModule(moduleName) { 27 | // return new RegExp(`(import\\s+(?:(?:\\w+|{(?:\\s*\\w\\s*,?\\s*)+})\\s+from)?\\s*['"\`](${moduleName}+?)['"\`])`, 'gm'); 28 | // }, 29 | /** 30 | * 匹配是否导入某个模块 commonjs 模式 31 | * @param moduleName 模块名称 32 | */ 33 | // matchRequireModule(moduleName) { 34 | // return new RegExp(`(?:var|let|const)\\s+(?:(?:\\w+|{(?:\\s*\\w\\s*,?\\s*)+}))\\s*=\\s*require\\s*\\(\\s*['"\`](${moduleName}+?)['"\`]\\s*\\)`, 'gm'); 35 | // }, 36 | /** 37 | * 字符串正则转义 38 | * @param value 加密参数 39 | */ 40 | stringRegEscape(value) { 41 | return value.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') 42 | }, 43 | /** 44 | * 注入国际化实例 45 | * @param options 国际化配置对象 46 | */ 47 | injectInstance({ code, ext, options }) { 48 | // 如果存在需要注入的文件 进行注入 49 | if (options.i18nInstance) { 50 | const matchInstance = code.match(new RegExp(this.stringRegEscape(options.i18nInstance), 'gm')) 51 | // 如果已经存在实例直接返回 52 | if (matchInstance) return code 53 | return `${options.i18nInstance}\n${code.trim()}` 54 | } 55 | return code 56 | }, 57 | /** 58 | * 去掉首尾空白字符,中间的连续空白字符替换成一个空格 59 | */ 60 | formatWhitespace(str) { 61 | return str.trim().replace(/\s+/g, ' ') 62 | }, 63 | /** 64 | * 处理嵌套的html标签 可以 返回多段 主要处理vue3有多段script的问题 65 | * @param options.code 需要匹配html的字符串 66 | * @param options.tagName 需要匹配html 标签名称 如果传 默认匹配所有标签 67 | * @param cb 匹配成功的回调 68 | */ 69 | handleNestedTags({ code, tagName }, cb) { 70 | tagName = tagName ? tagName : '\\w*' 71 | // 标签编号对象 72 | let tagsNo = {} 73 | // 开始标签编号 74 | code = code.replace( 75 | new RegExp(`(<(${tagName})((?:\\s+[^>]*)|)>)|(<\\/(${tagName})>)|(<(${tagName})((?:\\s+[^>]*)|)\\/>)`, 'gim'), 76 | (match, startTag, starTagName, startTagCon, endTag, endTagName, closeTag, closeTagName, closeTagCon) => { 77 | // 如果是闭合标签 类似这种 78 | let value = '' 79 | if (closeTag) { 80 | value = `<${closeTagName + '%%_0'}${closeTagCon || ''}/>` 81 | } 82 | // 开始标签 83 | if (startTag) { 84 | if (tagsNo[starTagName] === undefined) { 85 | tagsNo[starTagName] = 0 86 | } 87 | value = `<${starTagName + '%%_' + tagsNo[starTagName]}${startTagCon || ''}>` 88 | if (tagsNo[starTagName] !== undefined) { 89 | tagsNo[starTagName] += 1 // 开始标签 进行 加一 90 | } 91 | } 92 | // 结束标签 93 | if (endTag) { 94 | if (tagsNo[endTagName] === undefined) { 95 | tagsNo[endTagName] = 0 96 | } 97 | if (tagsNo[endTagName] !== undefined) { 98 | tagsNo[endTagName] -= 1 // 结束标签存在 匹配对减一 99 | } 100 | value = `` 101 | } 102 | return value 103 | } 104 | ) 105 | // 编号完成 进行匹配 这里获取顶级的标签 编号为0 106 | code = code.replace(new RegExp(`<(${tagName}%%_0)((?:\\s+[^>]*)|)>[\\s\\S]*?<\\/\\1>|<(${tagName}%%_0)((?:\\s+[^>]*)|)\\/*>`, 'gim'), match => { 107 | // 编号完成清除标签的编号 108 | match = match.replace(new RegExp(`(<(${tagName}%%_\\-?\\d+)((?:\\s+[^>]*)|)\\/?>)|(<\\/(${tagName}%%_\\-?\\d+)>)`, 'gim'), match => { 109 | return match.replace(/%%_\d+/, '') 110 | }) 111 | return cb ? cb(match) : match 112 | }) 113 | tagsNo = {} 114 | return code 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /core/utils/cacheCommentHtml.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 对html注释缓存 恢复处理 4 | */ 5 | module.exports = { 6 | /** 7 | * 暂存html注释对象 8 | */ 9 | commentsCache: {}, 10 | /** 11 | * 先去掉html的注释 暂存注释 12 | */ 13 | stash(sourceCode, options) { 14 | this.commentsCache = {} 15 | let commentsIndex = 0 16 | sourceCode = sourceCode.replace(//gm, (match, content) => { 17 | let commentsKey = `%%comment_html_${commentsIndex++}%%` 18 | this.commentsCache[commentsKey] = content 19 | return `` 20 | }) 21 | return sourceCode 22 | }, 23 | /** 24 | * 恢复之前删除的注释 25 | */ 26 | restore(sourceCode, options) { 27 | sourceCode = sourceCode.replace(/%%comment_html_\d+%%/gim, (match) => { 28 | return this.commentsCache[match] 29 | }); 30 | this.commentsCache = {} // 清除缓存 31 | return sourceCode 32 | } 33 | } -------------------------------------------------------------------------------- /core/utils/cacheCommentJs.js: -------------------------------------------------------------------------------- 1 | const baseUtils = require('./baseUtils') 2 | /** 3 | * 对js注释缓存 恢复处理 4 | */ 5 | module.exports = { 6 | /** 7 | * 暂存Js注释对象 8 | */ 9 | commentsCache: {}, 10 | /** 11 | * 先去掉js的注释 暂存注释 12 | */ 13 | stash(sourceCode, options) { 14 | this.commentsCache = {} 15 | let commentsIndex = 0 16 | sourceCode = sourceCode.replace(/(\/(\/.*))|(\/\*([\s\S]*?)\*\/)/g, (match, comment1, content1, comment2, content2, offset, string) => { 17 | let comment = '' 18 | let content = '' 19 | // 单行注释 防止匹配到url协议部分 类似 http:// 20 | if (comment1 != undefined && comment1 != null && comment1.length > 0) { 21 | const len = '//'.length 22 | if (offset == 0) { 23 | // 匹配字符串起始就是 //,所以整行都是注释 24 | comment = comment1 25 | content = content1 26 | } 27 | // 获取当前字符串中第一个纯正的单选注释 // 28 | let idxSlash = 0; 29 | while ((idxSlash = comment1.indexOf('//', idxSlash)) >= 0) { 30 | let prefix = string.charAt(offset + idxSlash - 1) 31 | if (prefix === ':') { 32 | // 前一个字符是':',所以不是单行注释 33 | idxSlash = idxSlash + len 34 | continue 35 | } else { 36 | // 拿出注释 37 | comment = comment1.substring(idxSlash, comment1.length) 38 | content = comment1.substring(idxSlash + len, comment1.length) 39 | break 40 | } 41 | } 42 | } 43 | // 多行注释 44 | if (comment2 !== undefined && comment2 !== null && comment2.length > 0) { 45 | comment = comment2 46 | content = content2 47 | } 48 | // 如果存在注释 49 | if (comment && content) { 50 | let commentsKey = `%%comment_js_${commentsIndex++}%%` 51 | this.commentsCache[commentsKey] = content 52 | return match.replace(content, commentsKey) 53 | } else { 54 | return match 55 | } 56 | }) 57 | return sourceCode 58 | }, 59 | /** 60 | * 恢复之前删除的注释 61 | */ 62 | restore(sourceCode, options) { 63 | sourceCode = sourceCode.replace(/%%comment_js_\d+%%/gim, (match) => { 64 | return this.commentsCache[match] 65 | }); 66 | this.commentsCache = {} // 清除缓存 67 | return sourceCode 68 | } 69 | } -------------------------------------------------------------------------------- /core/utils/cacheI18nField.js: -------------------------------------------------------------------------------- 1 | const baseUtils = require('./baseUtils') 2 | /** 3 | * 对已设置国际化的字段进行缓存 恢复 4 | * 忽略已经设置了国际化的字符串 防止key为中文时 会重复设置 5 | */ 6 | module.exports = { 7 | /** 8 | * 暂存国际化字符串对象 9 | */ 10 | i18nFieldCache: {}, 11 | /** 12 | * 先去掉国际化字符串 缓存 13 | */ 14 | stash(sourceCode, options) { 15 | this.i18nFieldCache = {} 16 | let i18nFieldIndex = 0 17 | let ignoreMethods = options.ignoreMethods 18 | // 转义字符串 19 | ignoreMethods = ignoreMethods.map((item) => baseUtils.stringRegEscape(item)) 20 | const ident = ignoreMethods.join('|') 21 | sourceCode = sourceCode.replace(new RegExp(`(${ident})\\([^(]+?\\)`, 'gm'), (match) => { 22 | let i18nFieldKey = `__i18n_field_${i18nFieldIndex++}__()` 23 | this.i18nFieldCache[i18nFieldKey] = match 24 | return i18nFieldKey 25 | }) 26 | return sourceCode 27 | }, 28 | /** 29 | * 恢复之前删除的国际化字符串 30 | */ 31 | restore(sourceCode, options) { 32 | sourceCode = sourceCode.replace(/__i18n_field_\d+__\(\)/gm, (match) => { 33 | return this.i18nFieldCache[match] 34 | }); 35 | this.i18nFieldCache = {} // 清除缓存 36 | return sourceCode 37 | } 38 | } -------------------------------------------------------------------------------- /examples/react-autoi18n-cli/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/react-autoi18n-cli/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `yarn build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /examples/react-autoi18n-cli/autoi18n.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | language: ['zh-cn', 'en-us'], 3 | modules: 'es6', 4 | entry: ['./src'], 5 | localePath: './src/locales', 6 | localeFileExt: '.json', 7 | extensions: [], 8 | exclude: ['./src/locales/*.{js,ts,json}'], 9 | ignoreMethods: ['i18n.get'], 10 | ignoreTagAttr: ['class', 'style', 'src', 'href', 'width', 'height'], 11 | i18nObjectMethod: 'i18n.get', 12 | i18nMethod: 'i18n.get', 13 | setMessageKey: false, 14 | i18nInstance: "import { i18n } from '~/i18n'", 15 | prettier: { singleQuote: true, trailingComma: 'es5', endOfLine: 'lf' }, 16 | }; 17 | -------------------------------------------------------------------------------- /examples/react-autoi18n-cli/config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | const dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | // Don't include `.env.local` for `test` environment 21 | // since normally you expect tests to produce the same 22 | // results for everyone 23 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 24 | `${paths.dotenv}.${NODE_ENV}`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. Variable expansion is supported in .env files. 31 | // https://github.com/motdotla/dotenv 32 | // https://github.com/motdotla/dotenv-expand 33 | dotenvFiles.forEach(dotenvFile => { 34 | if (fs.existsSync(dotenvFile)) { 35 | require('dotenv-expand')( 36 | require('dotenv').config({ 37 | path: dotenvFile, 38 | }) 39 | ); 40 | } 41 | }); 42 | 43 | // We support resolving modules according to `NODE_PATH`. 44 | // This lets you use absolute paths in imports inside large monorepos: 45 | // https://github.com/facebook/create-react-app/issues/253. 46 | // It works similar to `NODE_PATH` in Node itself: 47 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 48 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 49 | // Otherwise, we risk importing Node.js core modules into an app instead of webpack shims. 50 | // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 51 | // We also resolve them to make sure all tools using them work consistently. 52 | const appDirectory = fs.realpathSync(process.cwd()); 53 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 54 | .split(path.delimiter) 55 | .filter(folder => folder && !path.isAbsolute(folder)) 56 | .map(folder => path.resolve(appDirectory, folder)) 57 | .join(path.delimiter); 58 | 59 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 60 | // injected into the application via DefinePlugin in webpack configuration. 61 | const REACT_APP = /^REACT_APP_/i; 62 | 63 | function getClientEnvironment(publicUrl) { 64 | const raw = Object.keys(process.env) 65 | .filter(key => REACT_APP.test(key)) 66 | .reduce( 67 | (env, key) => { 68 | env[key] = process.env[key]; 69 | return env; 70 | }, 71 | { 72 | // Useful for determining whether we’re running in production mode. 73 | // Most importantly, it switches React into the correct mode. 74 | NODE_ENV: process.env.NODE_ENV || 'development', 75 | // Useful for resolving the correct path to static assets in `public`. 76 | // For example, . 77 | // This should only be used as an escape hatch. Normally you would put 78 | // images into the `src` and `import` them in code to get their paths. 79 | PUBLIC_URL: publicUrl, 80 | // We support configuring the sockjs pathname during development. 81 | // These settings let a developer run multiple simultaneous projects. 82 | // They are used as the connection `hostname`, `pathname` and `port` 83 | // in webpackHotDevClient. They are used as the `sockHost`, `sockPath` 84 | // and `sockPort` options in webpack-dev-server. 85 | WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST, 86 | WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH, 87 | WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT, 88 | // Whether or not react-refresh is enabled. 89 | // react-refresh is not 100% stable at this time, 90 | // which is why it's disabled by default. 91 | // It is defined here so it is available in the webpackHotDevClient. 92 | FAST_REFRESH: process.env.FAST_REFRESH !== 'false', 93 | } 94 | ); 95 | // Stringify all values so we can feed into webpack DefinePlugin 96 | const stringified = { 97 | 'process.env': Object.keys(raw).reduce((env, key) => { 98 | env[key] = JSON.stringify(raw[key]); 99 | return env; 100 | }, {}), 101 | }; 102 | 103 | return { raw, stringified }; 104 | } 105 | 106 | module.exports = getClientEnvironment; 107 | -------------------------------------------------------------------------------- /examples/react-autoi18n-cli/config/getHttpsConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const crypto = require('crypto'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const paths = require('./paths'); 8 | 9 | // Ensure the certificate and key provided are valid and if not 10 | // throw an easy to debug error 11 | function validateKeyAndCerts({ cert, key, keyFile, crtFile }) { 12 | let encrypted; 13 | try { 14 | // publicEncrypt will throw an error with an invalid cert 15 | encrypted = crypto.publicEncrypt(cert, Buffer.from('test')); 16 | } catch (err) { 17 | throw new Error( 18 | `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}` 19 | ); 20 | } 21 | 22 | try { 23 | // privateDecrypt will throw an error with an invalid key 24 | crypto.privateDecrypt(key, encrypted); 25 | } catch (err) { 26 | throw new Error( 27 | `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${ 28 | err.message 29 | }` 30 | ); 31 | } 32 | } 33 | 34 | // Read file and throw an error if it doesn't exist 35 | function readEnvFile(file, type) { 36 | if (!fs.existsSync(file)) { 37 | throw new Error( 38 | `You specified ${chalk.cyan( 39 | type 40 | )} in your env, but the file "${chalk.yellow(file)}" can't be found.` 41 | ); 42 | } 43 | return fs.readFileSync(file); 44 | } 45 | 46 | // Get the https config 47 | // Return cert files if provided in env, otherwise just true or false 48 | function getHttpsConfig() { 49 | const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env; 50 | const isHttps = HTTPS === 'true'; 51 | 52 | if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) { 53 | const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE); 54 | const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE); 55 | const config = { 56 | cert: readEnvFile(crtFile, 'SSL_CRT_FILE'), 57 | key: readEnvFile(keyFile, 'SSL_KEY_FILE'), 58 | }; 59 | 60 | validateKeyAndCerts({ ...config, keyFile, crtFile }); 61 | return config; 62 | } 63 | return isHttps; 64 | } 65 | 66 | module.exports = getHttpsConfig; 67 | -------------------------------------------------------------------------------- /examples/react-autoi18n-cli/config/jest/babelTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const babelJest = require('babel-jest'); 4 | 5 | const hasJsxRuntime = (() => { 6 | if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') { 7 | return false; 8 | } 9 | 10 | try { 11 | require.resolve('react/jsx-runtime'); 12 | return true; 13 | } catch (e) { 14 | return false; 15 | } 16 | })(); 17 | 18 | module.exports = babelJest.createTransformer({ 19 | presets: [ 20 | [ 21 | require.resolve('babel-preset-react-app'), 22 | { 23 | runtime: hasJsxRuntime ? 'automatic' : 'classic', 24 | }, 25 | ], 26 | ], 27 | babelrc: false, 28 | configFile: false, 29 | }); 30 | -------------------------------------------------------------------------------- /examples/react-autoi18n-cli/config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /examples/react-autoi18n-cli/config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const camelcase = require('camelcase'); 5 | 6 | // This is a custom Jest transformer turning file imports into filenames. 7 | // http://facebook.github.io/jest/docs/en/webpack.html 8 | 9 | module.exports = { 10 | process(src, filename) { 11 | const assetFilename = JSON.stringify(path.basename(filename)); 12 | 13 | if (filename.match(/\.svg$/)) { 14 | // Based on how SVGR generates a component name: 15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 16 | const pascalCaseFilename = camelcase(path.parse(filename).name, { 17 | pascalCase: true, 18 | }); 19 | const componentName = `Svg${pascalCaseFilename}`; 20 | return `const React = require('react'); 21 | module.exports = { 22 | __esModule: true, 23 | default: ${assetFilename}, 24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) { 25 | return { 26 | $$typeof: Symbol.for('react.element'), 27 | type: 'svg', 28 | ref: ref, 29 | key: null, 30 | props: Object.assign({}, props, { 31 | children: ${assetFilename} 32 | }) 33 | }; 34 | }), 35 | };`; 36 | } 37 | 38 | return `module.exports = ${assetFilename};`; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /examples/react-autoi18n-cli/config/modules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | const chalk = require('react-dev-utils/chalk'); 7 | const resolve = require('resolve'); 8 | 9 | /** 10 | * Get additional module paths based on the baseUrl of a compilerOptions object. 11 | * 12 | * @param {Object} options 13 | */ 14 | function getAdditionalModulePaths(options = {}) { 15 | const baseUrl = options.baseUrl; 16 | 17 | if (!baseUrl) { 18 | return ''; 19 | } 20 | 21 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 22 | 23 | // We don't need to do anything if `baseUrl` is set to `node_modules`. This is 24 | // the default behavior. 25 | if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { 26 | return null; 27 | } 28 | 29 | // Allow the user set the `baseUrl` to `appSrc`. 30 | if (path.relative(paths.appSrc, baseUrlResolved) === '') { 31 | return [paths.appSrc]; 32 | } 33 | 34 | // If the path is equal to the root directory we ignore it here. 35 | // We don't want to allow importing from the root directly as source files are 36 | // not transpiled outside of `src`. We do allow importing them with the 37 | // absolute path (e.g. `src/Components/Button.js`) but we set that up with 38 | // an alias. 39 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 40 | return null; 41 | } 42 | 43 | // Otherwise, throw an error. 44 | throw new Error( 45 | chalk.red.bold( 46 | "Your project's `baseUrl` can only be set to `src` or `node_modules`." + 47 | ' Create React App does not support other values at this time.' 48 | ) 49 | ); 50 | } 51 | 52 | /** 53 | * Get webpack aliases based on the baseUrl of a compilerOptions object. 54 | * 55 | * @param {*} options 56 | */ 57 | function getWebpackAliases(options = {}) { 58 | const baseUrl = options.baseUrl; 59 | 60 | if (!baseUrl) { 61 | return {}; 62 | } 63 | 64 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 65 | 66 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 67 | return { 68 | src: paths.appSrc, 69 | }; 70 | } 71 | } 72 | 73 | /** 74 | * Get jest aliases based on the baseUrl of a compilerOptions object. 75 | * 76 | * @param {*} options 77 | */ 78 | function getJestAliases(options = {}) { 79 | const baseUrl = options.baseUrl; 80 | 81 | if (!baseUrl) { 82 | return {}; 83 | } 84 | 85 | const baseUrlResolved = path.resolve(paths.appPath, baseUrl); 86 | 87 | if (path.relative(paths.appPath, baseUrlResolved) === '') { 88 | return { 89 | '^src/(.*)$': '/src/$1', 90 | }; 91 | } 92 | } 93 | 94 | function getModules() { 95 | // Check if TypeScript is setup 96 | const hasTsConfig = fs.existsSync(paths.appTsConfig); 97 | const hasJsConfig = fs.existsSync(paths.appJsConfig); 98 | 99 | if (hasTsConfig && hasJsConfig) { 100 | throw new Error( 101 | 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' 102 | ); 103 | } 104 | 105 | let config; 106 | 107 | // If there's a tsconfig.json we assume it's a 108 | // TypeScript project and set up the config 109 | // based on tsconfig.json 110 | if (hasTsConfig) { 111 | const ts = require(resolve.sync('typescript', { 112 | basedir: paths.appNodeModules, 113 | })); 114 | config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config; 115 | // Otherwise we'll check if there is jsconfig.json 116 | // for non TS projects. 117 | } else if (hasJsConfig) { 118 | config = require(paths.appJsConfig); 119 | } 120 | 121 | config = config || {}; 122 | const options = config.compilerOptions || {}; 123 | 124 | const additionalModulePaths = getAdditionalModulePaths(options); 125 | 126 | return { 127 | additionalModulePaths: additionalModulePaths, 128 | webpackAliases: getWebpackAliases(options), 129 | jestAliases: getJestAliases(options), 130 | hasTsConfig, 131 | }; 132 | } 133 | 134 | module.exports = getModules(); 135 | -------------------------------------------------------------------------------- /examples/react-autoi18n-cli/config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 13 | // "public path" at which the app is served. 14 | // webpack needs to know it to put the right 30 | 31 | 41 | -------------------------------------------------------------------------------- /examples/vue-autoi18n-cli/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaneyxs/autoi18n/9252a85c25415f13a5131128ff570bdb5e28a2b9/examples/vue-autoi18n-cli/src/assets/logo.png -------------------------------------------------------------------------------- /examples/vue-autoi18n-cli/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 43 | 44 | 60 | -------------------------------------------------------------------------------- /examples/vue-autoi18n-cli/src/i18n.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | 4 | Vue.use(VueI18n) 5 | 6 | const messages = {} 7 | const files = require.context('./locales', true, /\.json$/) 8 | 9 | files.keys().forEach((key) => { 10 | const name = key.replace(/^\.\/(.*)\.\w+$/, '$1') 11 | messages[name] = files(key) 12 | }) 13 | 14 | const i18n = new VueI18n({ 15 | locale: 'zh-cn', 16 | fallbackLocale: 'zh-cn', 17 | messages, 18 | }) 19 | 20 | export default i18n 21 | -------------------------------------------------------------------------------- /examples/vue-autoi18n-cli/src/locales/en-us.json: -------------------------------------------------------------------------------- 1 | { 2 | "1457a8cf081b42e8461e210209b9661c": "Welcome to use autoi18n", 3 | "a7bac2239fcdcb3a067903d8077c4a07": "Chinese", 4 | "f9fb6a063d1856da86a06def2dc6b921": "English" 5 | } -------------------------------------------------------------------------------- /examples/vue-autoi18n-cli/src/locales/zh-cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "1457a8cf081b42e8461e210209b9661c": "欢迎使用 autoi18n", 3 | "a7bac2239fcdcb3a067903d8077c4a07": "中文", 4 | "f9fb6a063d1856da86a06def2dc6b921": "英文" 5 | } -------------------------------------------------------------------------------- /examples/vue-autoi18n-cli/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import i18n from '@/i18n' 4 | Vue.config.productionTip = false 5 | 6 | new Vue({ 7 | i18n, 8 | render: (h) => h(App), 9 | }).$mount('#app') 10 | -------------------------------------------------------------------------------- /examples/vue-autoi18n-cli/vue.config.js: -------------------------------------------------------------------------------- 1 | 2 | const isDev = process.env.DEPLOY_ENV === 'development' // 开发环境 3 | const isProd = process.env.DEPLOY_ENV === 'production' // 生产环境 4 | const isTest = process.env.DEPLOY_ENV === 'test' // 测试环境 5 | const isDeploy = isProd || isTest // 是否是部署环境 6 | 7 | module.exports = { 8 | parallel: false, 9 | productionSourceMap: isDev, 10 | lintOnSave: true, 11 | publicPath: isDeploy ? '' : '', 12 | assetsDir: 'static', 13 | css: { 14 | // 是否使用css分离插件 ExtractTextPlugin 15 | extract: isDeploy, 16 | // 开启 CSS source maps? 17 | sourceMap: false 18 | }, 19 | chainWebpack: (config) => { 20 | if (isProd) { 21 | // 压缩去除console等信息 22 | config.optimization.minimizer('terser').tap((args) => { 23 | Object.assign(args[0].terserOptions.compress, { 24 | // warnings: false , // 默认 false 25 | // drop_console: false, // 默认 26 | // drop_debugger: true, // 去掉 debugger 默认也是true 27 | pure_funcs: ['console.log'] 28 | }) 29 | return args 30 | }) 31 | } 32 | if (isTest) { 33 | config.optimization.minimizer('terser').tap((args) => { 34 | Object.assign(args[0].terserOptions.compress, { 35 | // warnings: false , // 默认 false 36 | // drop_console: false, // 默认 37 | // drop_debugger: true, // 去掉 debugger 默认也是true 38 | // pure_funcs: ['console.log'] 39 | }) 40 | return args 41 | }) 42 | } 43 | }, 44 | devServer: { 45 | open: true, //是否自动弹出浏览器页面 46 | port: 8089, // 设置端口号 47 | https: false, //是否使用https协议 48 | hotOnly: false, //是否开启热更新 49 | proxy: { 50 | // 互金API代理 51 | '/cashier/api': { 52 | // '/cashier-uat/api/': { 53 | // target: 'http://172.16.1.204:9001', // 水平电脑 54 | // target: 'http://172.16.1.103:9001', // 曼婷电脑 55 | // target: 'http://172.16.1.63:9001', // 智文电脑 56 | target: 'https://kwgmb-test.kwgproperty.com', // 测试环境 57 | ws: true, // 代理websockets 58 | changeOrigin: true // 是否跨域,虚拟的站点需要更管origin 59 | // pathRewrite: { 60 | // '/cashier/api': '' 61 | // } 62 | }, 63 | // APP API 代理 64 | '/kwgdspappapi/v2': { 65 | target: 'https://kwgmb-test.kwgproperty.com', // 测试环境 66 | ws: true, // 代理websockets 67 | changeOrigin: true // 是否跨域,虚拟的站点需要更管origin 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/vue-autoi18n-loaders/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /examples/vue-autoi18n-loaders/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // prettier配置 3 | printWidth: 180, // 超过最大值换行 4 | tabWidth: 2, // 缩进字节数 5 | useTabs: false, // 缩进不使用tab,使用空格 6 | semi: false, // 句尾添加分号 7 | singleQuote: true, // 使用单引号代替双引号 8 | proseWrap: 'preserve', // 默认值。因为使用了一些折行敏感型的渲染器 而按照markdown文本样式进行折行 9 | arrowParens: 'always', // (x) => {} 箭头函数参数只有一个时是否要有小括号。always:不省略括号 10 | jsxBracketSameLine: false, // 在jsx中把'>' 是否单独放一行 11 | jsxSingleQuote: false, // 在jsx中使用单引号代替双引号 12 | htmlWhitespaceSensitivity: 'ignore', // 指定HTML文件的全局空格敏感度 13 | endOfLine: 'auto', // 行结束 14 | // trailingComma: 'none' // 无尾随逗号 15 | }; 16 | -------------------------------------------------------------------------------- /examples/vue-autoi18n-loaders/README.md: -------------------------------------------------------------------------------- 1 | # vue-autoi18n 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /examples/vue-autoi18n-loaders/autoi18n.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | language: ['zh-cn', 'en-us'], 3 | modules: 'es6', 4 | entry: ['./src'], 5 | localePath: './src/locales', 6 | localeFileExt: '.json', 7 | extensions: [], 8 | exclude: ['./src/locales/*.{js,ts,json}'], 9 | ignoreMethods: ['i18n.t', '$t'], 10 | ignoreTagAttr: ['class', 'style', 'src', 'href', 'width', 'height'], 11 | i18nObjectMethod: 'i18n.t', 12 | i18nMethod: '$t', 13 | setMessageKey: false, 14 | i18nInstance: "import i18n from '@/i18n'", 15 | prettier: { singleQuote: true, trailingComma: 'es5', endOfLine: 'lf' }, 16 | }; 17 | -------------------------------------------------------------------------------- /examples/vue-autoi18n-loaders/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /examples/vue-autoi18n-loaders/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-autoi18n", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.6.5", 12 | "vue": "^2.6.11", 13 | "vue-i18n": "^8.25.0" 14 | }, 15 | "devDependencies": { 16 | "@vue/cli-plugin-babel": "~4.5.0", 17 | "@vue/cli-plugin-eslint": "~4.5.0", 18 | "@vue/cli-service": "~4.5.0", 19 | "autoi18n": "^1.0.6", 20 | "babel-eslint": "^10.1.0", 21 | "eslint": "^6.7.2", 22 | "eslint-plugin-vue": "^6.2.2", 23 | "vue-template-compiler": "^2.6.11" 24 | }, 25 | "eslintConfig": { 26 | "root": true, 27 | "env": { 28 | "node": true 29 | }, 30 | "extends": [ 31 | "plugin:vue/essential", 32 | "eslint:recommended" 33 | ], 34 | "parserOptions": { 35 | "parser": "babel-eslint" 36 | }, 37 | "rules": {} 38 | }, 39 | "browserslist": [ 40 | "> 1%", 41 | "last 2 versions", 42 | "not dead" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /examples/vue-autoi18n-loaders/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaneyxs/autoi18n/9252a85c25415f13a5131128ff570bdb5e28a2b9/examples/vue-autoi18n-loaders/public/favicon.ico -------------------------------------------------------------------------------- /examples/vue-autoi18n-loaders/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/vue-autoi18n-loaders/src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 30 | 31 | 41 | -------------------------------------------------------------------------------- /examples/vue-autoi18n-loaders/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaneyxs/autoi18n/9252a85c25415f13a5131128ff570bdb5e28a2b9/examples/vue-autoi18n-loaders/src/assets/logo.png -------------------------------------------------------------------------------- /examples/vue-autoi18n-loaders/src/i18n.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | 4 | Vue.use(VueI18n) 5 | 6 | const messages = {} 7 | const files = require.context('./locales', true, /\.json$/) 8 | 9 | files.keys().forEach((key) => { 10 | const name = key.replace(/^\.\/(.*)\.\w+$/, '$1') 11 | messages[name] = files(key) 12 | }) 13 | 14 | const i18n = new VueI18n({ 15 | locale: 'zh-cn', 16 | fallbackLocale: 'zh-cn', 17 | messages, 18 | }) 19 | 20 | export default i18n 21 | -------------------------------------------------------------------------------- /examples/vue-autoi18n-loaders/src/locales/en-us.json: -------------------------------------------------------------------------------- 1 | { 2 | "1457a8cf081b42e8461e210209b9661c": "Welcome to use autoi18n", 3 | "a7bac2239fcdcb3a067903d8077c4a07": "Chinese", 4 | "f9fb6a063d1856da86a06def2dc6b921": "English" 5 | } -------------------------------------------------------------------------------- /examples/vue-autoi18n-loaders/src/locales/zh-cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "1457a8cf081b42e8461e210209b9661c": "欢迎使用 autoi18n", 3 | "a7bac2239fcdcb3a067903d8077c4a07": "中文", 4 | "f9fb6a063d1856da86a06def2dc6b921": "英文" 5 | } -------------------------------------------------------------------------------- /examples/vue-autoi18n-loaders/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import i18n from '@/i18n' 4 | Vue.config.productionTip = false 5 | 6 | new Vue({ 7 | i18n, 8 | render: (h) => h(App), 9 | }).$mount('#app') 10 | -------------------------------------------------------------------------------- /examples/vue-autoi18n-loaders/vue.config.js: -------------------------------------------------------------------------------- 1 | 2 | const isDev = process.env.DEPLOY_ENV === 'development' // 开发环境 3 | const isProd = process.env.DEPLOY_ENV === 'production' // 生产环境 4 | const isTest = process.env.DEPLOY_ENV === 'test' // 测试环境 5 | const isDeploy = isProd || isTest // 是否是部署环境 6 | 7 | module.exports = { 8 | parallel: false, 9 | productionSourceMap: isDev, 10 | lintOnSave: true, 11 | publicPath: isDeploy ? '' : '', 12 | assetsDir: 'static', 13 | css: { 14 | // 是否使用css分离插件 ExtractTextPlugin 15 | extract: isDeploy, 16 | // 开启 CSS source maps? 17 | sourceMap: false 18 | }, 19 | chainWebpack: (config) => { 20 | // 配置自动国际化loader 无侵入式 21 | config.module 22 | .rule('autoi18n') 23 | .test(/\.(vue|(j|t)sx?)$/) 24 | .pre() // 这个必须加上 优先执行的loader 顺序一定要在use方法前面 否则会报找不到pre方法 25 | .use('autoi18n') 26 | .loader('autoi18n') 27 | .end() 28 | }, 29 | devServer: { 30 | open: true, //是否自动弹出浏览器页面 31 | port: 8089, // 设置端口号 32 | https: false, //是否使用https协议 33 | hotOnly: false, //是否开启热更新 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /loaders/index.js: -------------------------------------------------------------------------------- 1 | const loaderUtils = require('loader-utils') 2 | const path = require('path') 3 | const fs = require('fs') 4 | const mergeIi8nConfig = require('../cli/utils/mergeIi8nConfig'); 5 | const { transform } = require('../core/index') 6 | let messages = {} 7 | 8 | module.exports = function (source) { 9 | const configOptions = mergeIi8nConfig() 10 | // const options = loaderUtils.getOptions(this); 11 | let targetFile = { ext: path.extname(this.resourcePath), filePath: this.resourcePath } 12 | source = transform({ code: source, targetFile, options: configOptions, messages }) 13 | messages = {} 14 | return source 15 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "autoi18n-tool", 3 | "version": "1.0.8", 4 | "main": "./loaders/index.js", 5 | "license": "MIT", 6 | "bin": { 7 | "autoi18n": "./cli/bin/index.js" 8 | }, 9 | "keywords": [ 10 | "i18n", 11 | "autoi18n", 12 | "auto-i18n", 13 | "autoi18n-tool" 14 | ], 15 | "repository": { 16 | "url": "https://github.com/Gertyxs/autoi18n/tree/main" 17 | }, 18 | "files": [ 19 | "cli", 20 | "core", 21 | "loaders" 22 | ], 23 | "author": "Gertyxs ", 24 | "scripts": {}, 25 | "dependencies": { 26 | "@babel/core": "^7.14.8", 27 | "@babel/generator": "^7.14.8", 28 | "@babel/plugin-proposal-optional-chaining": "^7.14.5", 29 | "@babel/plugin-syntax-async-generators": "^7.8.4", 30 | "@babel/plugin-syntax-class-properties": "^7.12.13", 31 | "@babel/plugin-syntax-decorators": "^7.14.5", 32 | "@babel/plugin-syntax-do-expressions": "^7.14.5", 33 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 34 | "@babel/plugin-syntax-export-extensions": "^7.0.0-beta.32", 35 | "@babel/plugin-syntax-function-bind": "^7.14.5", 36 | "@babel/plugin-syntax-jsx": "^7.14.5", 37 | "@babel/plugin-syntax-object-rest-spread": "^7.8.3", 38 | "@babel/preset-typescript": "^7.14.5", 39 | "@babel/traverse": "^7.14.8", 40 | "@babel/types": "^7.14.8", 41 | "chalk": "^4.1.1", 42 | "commander": "^8.0.0", 43 | "crypto-js": "^4.1.1", 44 | "glob": "^7.1.7", 45 | "inquirer": "^8.1.1", 46 | "prettier": "^2.3.2" 47 | }, 48 | "peerDependencies": {}, 49 | "devDependencies": {} 50 | } 51 | --------------------------------------------------------------------------------