├── .npmignore ├── .gitIgnore ├── webpack.config.js ├── lib ├── config.js ├── utils.js ├── createI18nDir.js └── index.js ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.gitIgnore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea 3 | .vscode 4 | dist -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | module.exports = { 3 | entry: "./lib/index.js", 4 | output: { 5 | filename: "index.js", 6 | path: path.resolve(__dirname, "dist"), 7 | library: 'myLib', 8 | libraryTarget: 'umd', 9 | }, 10 | target: 'node', 11 | node: { 12 | fs: "empty", 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const rootPath = process.cwd(); 3 | const srcPath = path.join(rootPath, "src"); 4 | const i18nPath = path.join(srcPath, "i18n"); 5 | const zhPath = path.join(i18nPath, "zh"); 6 | const enPath = path.join(i18nPath, "en"); 7 | const zhDataPath = path.join(zhPath, "data.json"); 8 | const enDataPath = path.join(enPath, "data.json"); 9 | const i18nJsPath = path.join(i18nPath, "i18n.js"); 10 | console.log("root path:", rootPath) 11 | console.log("src path:", srcPath) 12 | console.log("i18n path:", i18nPath) 13 | module.exports = { 14 | rootPath, 15 | srcPath, 16 | i18nPath, 17 | zhPath, 18 | enPath, 19 | zhDataPath, 20 | enDataPath, 21 | i18nJsPath, 22 | }; 23 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | const md = {} 4 | /** 5 | * 是否含有中文(也包含日文和韩文) 6 | * @param {*} str 7 | */ 8 | md.isChineseChar = (str) => { 9 | var reg = /[\u4E00-\u9FA5\uF900-\uFA2D]/ 10 | return reg.test(str) 11 | } 12 | /** 13 | * log 14 | * @param {*} text 15 | */ 16 | md.log = (text) => { 17 | console.log("\x1b[33m%s\x1b[0m", text) 18 | } 19 | /** 20 | * 去重 21 | * @param {*} key 22 | */ 23 | var o = {} 24 | md.isRepeat = (key) => { 25 | if (o[key]) { 26 | return true 27 | } else { 28 | o[key] = true 29 | return false 30 | } 31 | } 32 | md.createFolder = (dir) => { 33 | if (fs.existsSync(dir)) return; 34 | 35 | fs.mkdirSync(dir); 36 | console.log("文件夹创建成功:", dir); 37 | } 38 | 39 | md.createFile = (file, content) => { 40 | if (fs.existsSync(file)) return; 41 | fs.writeFileSync(file, content); 42 | console.log("文件创建成功:", file); 43 | } 44 | 45 | module.exports = md -------------------------------------------------------------------------------- /lib/createI18nDir.js: -------------------------------------------------------------------------------- 1 | const c = require("./config"); 2 | const utils = require("./utils") 3 | 4 | const dirs = [c.i18nPath, c.zhPath, c.enPath]; 5 | const files = [ 6 | { file: c.enDataPath, content: "{}" }, 7 | { file: c.zhDataPath, content: "{}" }, 8 | { 9 | file: c.i18nJsPath, 10 | content: ` 11 | import zh from "./zh/data.json" 12 | import en from "./en/data.json" 13 | 14 | function i18n(lang) { 15 | let data; 16 | switch (lang) { 17 | case "zh": 18 | data = zh; 19 | break; 20 | case "en": 21 | data = en; 22 | break; 23 | 24 | default: 25 | data = zh; 26 | break; 27 | } 28 | window.$i18n = data; 29 | }; 30 | 31 | i18n("en") 32 | `, 33 | }, 34 | ]; 35 | 36 | 37 | function main() { 38 | dirs.forEach((dir) => { 39 | utils.createFolder(dir); 40 | }); 41 | files.forEach((item) => { 42 | utils.createFile(item.file, item.content); 43 | }); 44 | } 45 | 46 | module.exports = main; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "miao-i18n", 3 | "version": "0.1.6", 4 | "description": "webpack i18n loader", 5 | "main": "dist/index.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "@babel/core": "^7.10.4", 9 | "@babel/parser": "^7.10.4", 10 | "@babel/traverse": "^7.10.4", 11 | "md5": "^2.2.1", 12 | "webpack": "^4.43.0", 13 | "webpack-cli": "^3.3.12" 14 | }, 15 | "files": [ 16 | "dist" 17 | ], 18 | "scripts": { 19 | "test": "echo \"Error: no test specified\" && exit 1", 20 | "build": "webpack --mode production", 21 | "dev": "webpack -w --mode development" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/goldEli/i18nl.git" 26 | }, 27 | "keywords": [ 28 | "i18n", 29 | "loader" 30 | ], 31 | "author": "miaoyu", 32 | "license": "ISC", 33 | "bugs": { 34 | "url": "https://github.com/goldEli/i18nl/issues" 35 | }, 36 | "homepage": "https://github.com/goldEli/i18nl#readme" 37 | } 38 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const parser = require("@babel/parser"); 2 | const traverse = require("@babel/traverse").default; 3 | const babel = require("@babel/core"); 4 | const md5 = require("md5"); 5 | const fs = require("fs"); 6 | 7 | const createI18nDir = require("./createI18nDir.js"); 8 | const utils = require("./utils.js"); 9 | const config = require("./config.js"); 10 | 11 | createI18nDir(); 12 | 13 | /** 14 | * 在调用 this.callback 前,对 code 进行国际化处理 15 | * @param {string} code 16 | * @param {不知道是啥} map 17 | */ 18 | 19 | function i18nLoader(code, map) { 20 | this.callback(null, handleCode(code), map); 21 | } 22 | 23 | /** 24 | * 处理代码字符串 25 | * @param {*} code 26 | */ 27 | function handleCode(code) { 28 | // 该文件代码是否忽略国际化 29 | if (isIgnore(code)) { 30 | return code; 31 | } 32 | 33 | // 代码转语法树 34 | const ast = codeToAst(code); 35 | 36 | // 处理语法树 37 | handleAst(ast); 38 | 39 | // 处理后的语法树转代码 40 | const newCode = astToCode(ast); 41 | 42 | return newCode; 43 | } 44 | 45 | /** 46 | * 如果代码中包含 i18nIgnore 关键字,则该文件忽略国际化 47 | * @param {*} code 48 | */ 49 | function isIgnore(code) { 50 | return code.includes("i18nIgnore"); 51 | } 52 | 53 | /** 54 | * 将语法树转代码字符串 55 | * @param {*} ast 56 | */ 57 | function astToCode(ast) { 58 | const { code } = babel.transformFromAst(ast, null, {}); 59 | return code; 60 | } 61 | 62 | /** 63 | * 遍历语法树,将中文替换成变量 64 | * @param {*} ast 65 | */ 66 | function handleAst(ast) { 67 | // 遍历语法树 68 | traverse(ast, { 69 | StringLiteral({ node }) { 70 | if (node) { 71 | const text = node.value; 72 | if (utils.isChineseChar(text)) { 73 | createI18nData(text); 74 | stringLiteralToIdentifier(node, text); 75 | } 76 | } 77 | }, 78 | }); 79 | } 80 | 81 | function createI18nData(text) { 82 | const key = md5(text); 83 | const zh = JSON.parse(fs.readFileSync(config.zhDataPath, "utf8")); 84 | const en = JSON.parse(fs.readFileSync(config.enDataPath, "utf8")); 85 | if (!(key in zh)) { 86 | zh[key] = text; 87 | fs.writeFileSync(config.zhDataPath, JSON.stringify(zh)); 88 | } 89 | if (!(key in en)) { 90 | en[key] = text; 91 | fs.writeFileSync(config.enDataPath, JSON.stringify(en)); 92 | } 93 | } 94 | 95 | /** 96 | * 代码字符串转成语法树 97 | * @param {*} code 98 | */ 99 | function codeToAst(code) { 100 | const ast = parser.parse(code, { 101 | sourceType: "module", // 识别ES Module 102 | plugins: [ 103 | "jsx", // enable jsx 104 | "classProperties", 105 | "dynamicImport", 106 | "optionalChaining", 107 | "decorators-legacy", 108 | ], 109 | }); 110 | return ast; 111 | } 112 | 113 | /** 114 | * 字符串转为变量 115 | * @param {*} node 116 | * @param {*} text 117 | */ 118 | function stringLiteralToIdentifier(node, text) { 119 | const key = md5(text); 120 | const identifier = `window.$i18n["${key}"]`; 121 | 122 | // if (key in chineseSource) { 123 | delete node.extra; 124 | delete node.value; 125 | 126 | node.loc.identifierName = identifier; 127 | node.name = identifier; 128 | node.type = "Identifier"; 129 | // } else { 130 | // !isRepeat(key) && log(`"${key}": "${text}",`); 131 | // } 132 | } 133 | 134 | module.exports = i18nLoader; 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webpack i18n loader 2 | 3 | 国际化:自动管理工具 4 | 5 | [![NPM version][npm-image]][npm-url] 6 | 7 | [npm-image]: https://img.shields.io/npm/v/miao-i18n.svg?style=flat-square 8 | [npm-url]: https://www.npmjs.com/package/miao-i18n 9 | 10 | ### 背景 11 | 12 | 前段时间需要把一个开发了两年左右的项目进行国际化,支持中英文,逛了一圈社区都没有发现能很好解决痛点的轮子,比如: 13 | 14 | * 维护资源文件太麻烦 15 | * 代码侵入性太强,我不想把一个两年的项目,一个个文件去改 16 | 17 | 于是我决定自己写个工具: 18 | 19 | * 资源文件自动生成,自动更新 20 | * 代码0侵入,写代码时候完全不用去考虑国际化 21 | 22 | 总的来说就是,只要工具引入后,后期维护成本只有一个,只用考虑翻译资源文件。 23 | 24 | ### 思路 25 | 26 | 本质上就是实现一个 `webpack loader` ,在打包的时候自动处理国际化: 27 | 28 | * 遍历所有代码,提取代码中的中文字符串,生成资源文件(资源文件key,通过对应中文的`MD5`加密生成) 29 | * 将资源文件内容挂在到全局 `$i18n`对象上 30 | * 遍历所有代码,将代码中的中文替换成 `$i18n[key]` 31 | 32 | 代码已放到 `Github`:[miao-i18n](https://github.com/goldEli/miao-i18n) 33 | 34 | ### 如何使用 35 | 36 | 以 `create-react-app` 为例,创建一个项目: 37 | 38 | ``` shell 39 | create-react-app myapp 40 | ``` 41 | 42 | 由于我们需要添加`webpack loader`所以需要将配置暴露出来: 43 | 44 | ```shell 45 | yarn eject 46 | ``` 47 | 48 | 安装 [miao-i18n](https://github.com/goldEli/miao-i18n): 49 | 50 | ```shell 51 | yarn add miao-i18n 52 | ``` 53 | 54 | 配置 `webpack`, 打开 `myapp/config/webpackDevServer.config.js`,由于 `loader`是自下而上执行的,所有我们要把我们的loader配置到最上面,这个很重要。 55 | 56 | ```javascript 57 | module: { 58 | strictExportPresence: true, 59 | rules: [ 60 | // Disable require.ensure as it's not a standard language feature. 61 | { parser: { requireEnsure: false } }, 62 | + { 63 | + test: /\.(js|mjs|jsx|ts|tsx)$/, 64 | + exclude: /node_module/, 65 | + loader: require.resolve("miao-i18n"), 66 | + }, 67 | ... 68 | } 69 | ``` 70 | 71 | 配置完成,可以开始愉快的玩耍了😊 72 | 73 | ```shell 74 | yarn start 75 | ``` 76 | 77 | 项目启动后,可以看到 `src` 目录下自动生成了一个 `i18n`文件夹: 78 | 79 | ``` 80 | ├─src 81 | | ├─i18n 82 | | | ├─i18n.js 83 | | | ├─zh 84 | | | | └data.json 85 | | | ├─en 86 | | | | └data.json 87 | ``` 88 | 89 | `zh`、`en`分别对应中文和英文资源,这个就不用说了。 90 | 91 | `i18n.js`用来引入和切换资源文件: 92 | 93 | ```javascript 94 | import zh from "./zh/data.json" 95 | import en from "./en/data.json" 96 | 97 | /** 98 | 如果需要用按钮切换语言,可以将此方法暴露给按钮的点击回调。 99 | */ 100 | function i18n(lang) { 101 | let data; 102 | switch (lang) { 103 | case "zh": 104 | data = zh; 105 | break; 106 | case "en": 107 | data = en; 108 | break; 109 | 110 | default: 111 | data = zh; 112 | break; 113 | } 114 | window.$i18n = data; 115 | }; 116 | 117 | 118 | i18n("en")// 切换为英文 119 | ``` 120 | 121 | 最后把`i18n.js`引入到项目中。打开 `src/index.js`,在项目最前面引入 `i18n.js` 122 | 123 | ``` 124 | + import "./i18n/i18n" 125 | import React from 'react'; 126 | import ReactDOM from 'react-dom'; 127 | import './index.css'; 128 | import App from './App'; 129 | import * as serviceWorker from './serviceWorker' 130 | ... 131 | ``` 132 | 133 | 配置完成!🍾🍾🍾 134 | 135 | 添加中文试试吧。 136 | 137 | 打开 `src/App.js`,修改代码如下 138 | 139 | ``` 140 | import React from 'react'; 141 | function App() { 142 | return ( 143 |
苹果
145 |香蕉
146 |葡萄
147 |