├── .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://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 | 
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 |
121 |
122 | {{'我是vue内容'}}
123 | 我是标签静态内容
124 |
125 |
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(/(
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 |
2 |
3 |
{{ msg }}
4 |
5 | For a guide and recipes on how to configure / customize this project,
6 |
7 | check out the
8 | vue-cli documentation
9 | .
10 |
11 |
Installed CLI Plugins
12 |
16 |
Essential Links
17 |
24 |
Ecosystem
25 |
32 |
33 |
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 |
2 |
3 |
欢迎使用 autoi18n
4 |
8 |
9 |
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 |
--------------------------------------------------------------------------------