{"id":18880510,"url":"https://github.com/onewaytech/i18n-static","last_synced_at":"2025-04-14T19:32:21.055Z","repository":{"id":57270386,"uuid":"82553171","full_name":"OneWayTech/i18n-static","owner":"OneWayTech","description":"Universal i18n solution for static resources","archived":false,"fork":false,"pushed_at":"2018-01-16T06:58:55.000Z","size":51,"stargazers_count":173,"open_issues_count":0,"forks_count":30,"subscribers_count":9,"default_branch":"master","last_synced_at":"2025-04-10T02:05:58.444Z","etag":null,"topics":["i18n","replace","static","universal"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/OneWayTech.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-02-20T12:01:24.000Z","updated_at":"2024-05-10T06:37:13.000Z","dependencies_parsed_at":"2022-09-02T09:51:40.062Z","dependency_job_id":null,"html_url":"https://github.com/OneWayTech/i18n-static","commit_stats":null,"previous_names":["kenberkeley/universal-i18n-solution"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OneWayTech%2Fi18n-static","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OneWayTech%2Fi18n-static/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OneWayTech%2Fi18n-static/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OneWayTech%2Fi18n-static/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/OneWayTech","download_url":"https://codeload.github.com/OneWayTech/i18n-static/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248946071,"owners_count":21187444,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["i18n","replace","static","universal"],"created_at":"2024-11-08T06:44:14.883Z","updated_at":"2025-04-14T19:32:20.593Z","avatar_url":"https://github.com/OneWayTech.png","language":"JavaScript","readme":"# 通用的静态资源国际化方案\r\n\r\n## 2018.1 更新：[simplest-i18n](https://github.com/OneWayTech/simplest-i18n)\r\n\r\n\u003e Intellectual property of [OneWay](https://www.oneway.mobi)  \r\n\u003e 强烈建议 Chrome 用户安装 [Octotree](https://chrome.google.com/webstore/detail/octotree/bkhaagjahfmjljalopjnoealnfndnagc)、[Octo Mate](https://chrome.google.com/webstore/detail/octo-mate/baggcehellihkglakjnmnhpnjmkbmpkf?utm_source=chrome-app-launcher-info-dialog)、[OctoLinker](https://chrome.google.com/webstore/detail/octolinker/jlmafbaeoofdegohdhinkhilhclaklkp) 插件以提高阅读体验\r\n\r\n## § 快速体验\r\n定界符 `_#` 与 `#_` 包裹的 `XXX` 为**待翻译内容**，而 `\u003ci18n\u003e` 内容为其对应的**译文**（支持 YAML / JS 对象 / JSON 三种格式）：\r\n\r\n```js\r\nfunction show(num) {\r\n  console.log('_#每页显示#_' + num + '_#条记录#_');\r\n}\r\n/*\u003ci18n\u003e\r\n每页显示: 'Show '\r\n条记录: ' items per page'\r\n\u003c/i18n\u003e*/\r\n-----------------------------------------------\r\n                 Uglify + i18n ↓\r\n-----------------------------------------------\r\nfunction show(o){console.log(\"Show \"+o+\" items per pages\")}\r\n```\r\n\r\n\u003e **待翻译内容** 和 **译文** 的定界符都是可配置的\r\n\r\n## § 灵感来源\r\n试想，如果让您把公司所有的项目都支持国际化，会是一种怎么样的体验？  \r\n这些项目新旧不一：\r\n\r\n* 有的是 Grunt + RequireJS / Sea.js + jQuery / Zepto\r\n* 有的是 Gulp + Browserify + Backbone / Knockout / ...\r\n* 更多的是 Webpack `(1.x|2.x)` + React `(0.x|15.x)` / Vue.js `(0.x|1.x|2.x)` / Angular `(1.x|2.x)` / ...\r\n* 还有的是完全没有工程化的静态页\r\n\r\n由于技术栈极其混杂，且不可能一个一个坑去踩，因此以下这些相对主流的国际化方案都不可能采用：\r\n\r\n* [`i18n-webpack-plugin`](https://github.com/webpack/i18n-webpack-plugin) / [`i18n-loader`](https://github.com/webpack/i18n-loader) / [`babel-plugin-i18n`](https://github.com/valscion/babel-plugin-i18n)\r\n* [`react-intl`](https://github.com/yahoo/react-intl) / [`redux-i18n`](https://github.com/APSL/redux-i18n)\r\n* [`vue-i18n`](https://github.com/kazupon/vue-i18n) / [`vuex-i18n`](https://github.com/dkfbasel/vuex-i18n)\r\n* [`angular-translate`](https://github.com/angular-translate/angular-translate) / [`ng2-translate`](https://www.npmjs.com/package/ng2-translate)\r\n* [`jquery.i18n`](https://github.com/wikimedia/jquery.i18n) / [`jquery-i18n`](https://github.com/recurser/jquery-i18n) / [`jquery-i18n-properties`](https://github.com/jquery-i18n-properties/jquery-i18n-properties)\r\n\r\n虽说上述的所有方案都不予采用，但可以总结它们的经验，造一个更好用更通用的轮子\r\n\r\n国际化方案大致分为两类：**编译时翻译**与**运行时翻译**，各有千秋。不过在二者的选择上，我更倾向于前者  \r\n作为通用的国际化方案，不应受技术栈更迭的影响。**运行时翻译**强依赖于所用的框架/类库，故不符合要求\r\n\r\n所谓的国际化，说白了就是字符串的替换，最常见的就是把整个网站所有待翻译内容都**汇总**起来做成翻译表：\r\n\r\n```json\r\n// en.json\r\n{\r\n  \"你好\": \"Hello\",\r\n  \"爱\": \"Love\"\r\n}\r\n```\r\n\r\n```json\r\n// jp.json\r\n{\r\n  \"你好\": \"こんにちは\",\r\n  \"爱\": \"愛\"\r\n}\r\n```\r\n（此处省略十几个翻译表...）\r\n\r\n这种做法最大的缺陷在于维护困难，冗余不堪，源码中待翻译内容的任何改动都需要**同步**到所有的翻译表  \r\n且由于脱离了**上下文**，因此**校对**的时候非常痛苦\r\n\r\n***\r\n\r\n如果没有遇见 [Vux](https://github.com/airyland/vux)（基于 Vue.js 的移动 UI 组件库） 的这个 [issue](https://github.com/airyland/vux/issues/706)，就没有本方案的诞生。它的组件国际化方案如下：\r\n\r\n```html\r\n\u003ci18n\u003e\r\n title: \r\n  zh-CN: 标题\r\n content:\r\n   en: content\r\n   zh-CN: 内容\r\n\u003c/i18n\u003e\r\n```\r\n其配备的 [`vux-loader`](https://github.com/airyland/vux-loader) 可以解析 `\u003ci18n\u003e` 内容，来对当前组件中的待翻译内容进行替换  \r\n这样的话就完美地解决了单独维护翻译表的问题，且由于都在同一上下文中，因此更改与校对都显得非常方便！  \r\n`vux-loader` 之所以能这么做，是因为 [`vue-loader`](https://github.com/vuejs/vue-loader/) 只在乎 `\u003ctemplate\u003e`、`\u003cscript\u003e`、`\u003cstyle\u003e`，其他的都**忽略**  \r\n这给了我很大的启发！\r\n\r\n由于前端的特殊性，线上的代码一般都需要经过压缩处理，因此我们还可以更进一步：  \r\n**通过注释的方式，直接在待翻译内容旁编写其译文！**\r\n\r\n\u003e 当然也完全可以像 `vux-loader` 那样，把一个文件中所有待翻译内容集中到最后进行翻译\r\n\r\n进行国际化的时候，只需要提取源码目录中散落的翻译表，即可获得最终的翻译表  \r\n我们还能将翻译表保存下来，既满足高可维护性，又满足传统方案的**整体校对**需求\r\n\r\n## § 流程详解\r\n我们使用 [`example/`](./example) 下的这个简单的例子来说明本国际化方案的流程  \r\n\r\n首先我们来分析一下 [`package.json`](./package.json) 里面的 `npm scripts`：\r\n\r\n```\r\n\"scripts\": {\r\n  # 删除原有的 build 目录\r\n  \"clean\": \"rimraf example/dist\",\r\n\r\n  # 运行 npm run build 前的预备工作\r\n  \"prebuild\": \"npm run clean \u0026\u0026 mkdirp example/dist/__build__\",\r\n\r\n  # 使用 html-minifier 压缩 HTML 文件，使用 Browserify 与 Uglifyify 合并压缩 vendor.js 与 app.js\r\n  \"build:html\": \"html-minifier --remove-comments --file-ext html --input-dir example/src --output-dir example/dist/__build__\",\r\n  \"build:js:vendor\": \"browserify example/src/vendor/ -o example/dist/__build__/vendor.js\",\r\n  \"build:js:app\": \"browserify example/src/app.js -o example/dist/__build__/app.js\",\r\n  \"build\": \"npm run build:html \u0026\u0026 npm run build:js:vendor \u0026\u0026 npm run build:js:app\",\r\n\r\n  # 执行国际化任务\r\n  \"i18n\": \"node example/i18n.js\",\r\n\r\n  # 运行例子\r\n  \"example\": \"npm run build \u0026\u0026 npm run i18n\",\r\n\r\n  # 静态检测\r\n  \"lint\": \"jshint lib/\"\r\n}\r\n```\r\n\r\n敲下 `npm run example` 后，其实就是执行 `npm run build` 与 **`npm run i18n`**\r\n\r\n***\r\n\r\n### 1. `npm run build`\r\n标准的构建工作流，一般是 Webpack / Grunt / Gulp 等构建工具处理文件后吐出到一个目录中  \r\n（在这里我们为了简单，仅用 `npm scripts` 来完成）\r\n\r\n```\r\nexample/\r\n  ├── dist/\r\n  │   └── __build__/\r\n  │       ├── app.js  ←------┐\r\n  │       ├── index.html ←---|---┐\r\n  │       └── vendor.js  ←---|---|---┐\r\n  |                          |   |   |\r\n  └── src/                   |   |   |\r\n      ├── app.js --------┬---┘   |   | # 打包合并压缩混淆...\r\n      ├── index.html ----|-------┘   | # html-minifier\r\n      ├── main/          |           | # Browserify + UglifyJS\r\n      │   ├── main1.js --┤           |\r\n      │   └── main2.js --┘           |\r\n      └── vendor/                    |\r\n          ├── index.js ------┬-------┘\r\n          ├── vendor1.js ----┤\r\n          ├── vendor2.js ----┤\r\n          └── vendor3.js ----┘\r\n```\r\n\r\n反正处理后的文件最终都到了 [`example/dist/__build__/`](./example/dist/__build__)  \r\n以上并不是我们的重点，因为每个项目都有不同的构建工作流  \r\n重点是下面，对 `example/dist/__build__/` 的所有静态资源进行国际化\r\n\r\n***\r\n\r\n### 2. `npm run i18n`\r\n对应的命令是 `node example/i18n.js`，因此我们来看看 [`example/i18n.js`](./example/i18n.js) ：\r\n\r\n```js\r\nvar path = require('path'),\r\n  i18n = require('../');\r\n\r\ni18n({\r\n  srcDir: path.join(__dirname, 'src'),\r\n  buildDir: path.join(__dirname, 'dist/__build__'),\r\n  distDir: path.join(__dirname, 'dist'),\r\n  sourceLang: 'zh-cn',\r\n  defaultLang: 'en',\r\n  saveLocalesTo: path.join(__dirname, 'dist/locales.json')\r\n});\r\n```\r\n\r\n这里要着重解释一下 `srcDir` / `buildDir` / `distDir` 的含义：\r\n\r\n* `srcDir`，源码目录，用于提取翻译表\r\n* `buildDir`，构建工具处理源码后，所生成静态文件的存放目录  \r\n* `distDir`，我们的 `i18n` 工具将 `buildDir` 内所有静态资源翻译后所生成文件的存放处\r\n\r\n还有就是 `sourceLang` 与 `defaultLang` 的区别：\r\n\r\n* `sourceLang`，源语言，即 `_#XXX#_` 直接生成 `XXX`，无需翻译\r\n* `defaultLang`，默认翻译语言，即：  \r\n  `\u003ci18n\u003e{ 'XXX': 'XXX-default' }\u003c/i18n\u003e` 等同于 `\u003ci18n\u003e{ 'XXX': { [defaultLang]: 'XXX-default' } }\u003c/i18n\u003e`\r\n\r\n\u003e `defaultLang` 适用于仅提供两种语言版本的应用场景，应用见 [`example/src/vendor/vendor1.js`](./example/src/vendor/vendor1.js)\r\n\r\n最后是：\r\n\r\n* `saveLocalesTo`，即翻译表的保存路径，用于人工核对翻译，且有利于*提高处理效率*\r\n\r\n\u003e 为什么说可以**提高处理效率**？  \r\n\u003e 皆因 `i18n(conf)` 函数还能接受 `conf.locales` 配置  \r\n\u003e 若提供该参数，则直接使用该翻译表而非重新遍历 `srcDir` 提取  \r\n\u003e 对于待翻译内容不常更改的项目而言，此举可减少一些编译耗时\r\n\r\n配置讲完了，接下来是国际化三步走：\r\n\r\n#### ⑴ 提取\r\n从 `srcDir` 中递归提取出所有 `\u003ci18n\u003e` 的内容进行解析合并（称为 `i18nContent`）：\r\n\r\n```json\r\n{\r\n  \"欢迎\": {\r\n    \"en\": \"Welcom\",\r\n    \"jp\": \"歓迎\",\r\n    \"fr\": \"Bienvenue\"\r\n  },\r\n  \"语言 - 中文\": {\r\n    \"en\": \"Language - English\",\r\n    \"jp\": \"言語 - 日本語\",\r\n    \"fr\": \"Langue - Français\"\r\n  },\r\n  \"你好\": {\r\n    \"en\": \"Hello\",\r\n    \"jp\": \"こんにちは\",\r\n    \"fr\": \"Bonjour\"\r\n  },\r\n  \"热爱\": {\r\n    \"en\": \"Love\",\r\n    \"jp\": \"熱愛\",\r\n    \"fr\": \"Aimer\"\r\n  },\r\n  \"国际\": {\r\n    \"en\": \"International\",\r\n    \"jp\": \"国際\",\r\n    \"fr\": \"International\"\r\n  },\r\n  \"中国\": {\r\n    \"en\": \"China,\",\r\n    \"jp\": \"中国,\",\r\n    \"fr\": \"Chine\"\r\n  },\r\n  \"苹果\": {\r\n    \"en\": \"Apple\",\r\n    \"jp\": \"リンゴ\",\r\n    \"fr\": \"Pommes\"\r\n  },\r\n  \"好的\": {\r\n    \"en\": \"Well\"\r\n  },\r\n  \"时间\": {\r\n    \"en\": \"Time\",\r\n    \"jp\": \"時間\",\r\n    \"fr\": \"Le temps\"\r\n  }\r\n}\r\n```\r\n\r\n#### ⑵ 转换\r\n将上述内容转换成翻译表（称为 `locales`）：\r\n\r\n```json\r\n{\r\n  \"en\": {\r\n    \"欢迎\": \"Welcom\",\r\n    \"语言 - 中文\": \"Language - English\",\r\n    \"你好\": \"Hello\",\r\n    \"热爱\": \"Love\",\r\n    \"国际\": \"International\",\r\n    \"中国\": \"China,\",\r\n    \"苹果\": \"Apple\",\r\n    \"好的\": \"Well\",\r\n    \"时间\": \"Time\"\r\n  },\r\n  \"jp\": {\r\n    \"欢迎\": \"歓迎\",\r\n    \"语言 - 中文\": \"言語 - 日本語\",\r\n    \"你好\": \"こんにちは\",\r\n    \"热爱\": \"熱愛\",\r\n    \"国际\": \"国際\",\r\n    \"中国\": \"中国,\",\r\n    \"苹果\": \"リンゴ\",\r\n    \"时间\": \"時間\"\r\n  },\r\n  \"fr\": {\r\n    \"欢迎\": \"Bienvenue\",\r\n    \"语言 - 中文\": \"Langue - Français\",\r\n    \"你好\": \"Bonjour\",\r\n    \"热爱\": \"Aimer\",\r\n    \"国际\": \"International\",\r\n    \"中国\": \"Chine\",\r\n    \"苹果\": \"Pommes\",\r\n    \"时间\": \"Le temps\"\r\n  },\r\n  \"zh-cn\": {}\r\n}\r\n```\r\n\r\n\u003e 由于 `sourceLang` 设置为 `zh-cn`，因此 `locales['zh-cn']` 为空，表示不翻译  \r\n\u003e 当然还有 `locales.jp` 与 `locales.fr` 都缺少「`好的`」的译文，因此也是不翻译\r\n\r\n#### ⑶ 替换\r\n最后，就是根据翻译表对 `example/dist/__build__/` 进行翻译，最终生成：\r\n\r\n```\r\nexample/dist/\r\n  ├── __build__/\r\n  │   ├── app.js\r\n  │   ├── index.html\r\n  │   └── vendor.js\r\n  ├── en/            # defaultLang\r\n  │   ├── app.js\r\n  │   ├── index.html\r\n  │   └── vendor.js\r\n  ├── fr/\r\n  │   ├── app.js\r\n  │   ├── index.html\r\n  │   └── vendor.js\r\n  ├── jp/\r\n  │   ├── app.js\r\n  │   ├── index.html\r\n  │   └── vendor.js\r\n  ├── zh-cn/         # sourceLang\r\n  |   ├── app.js\r\n  |   ├── index.html\r\n  |   └── vendor.js\r\n  └── locales.json   # saveLocalesTo *here*\r\n```\r\n\r\n#### ※ 总流程图\r\n```\r\n        recursive-readdir-sync\r\nsrcDir ─────────────────────────┐\r\n  |                             ↓                    \u003e_ npm run i18n\r\n  |               for each file in files:\r\n  |            ⑴    extract-i18n-content(file)\r\n  |                             |\r\n  |                             ↓        i18n-content2locales\r\n  |                         i18nContent ───────── ⑵ ────────────┐\r\n  |                                                              ↓          ┌  en   ┐\r\n  | \u003e_ npm run build                                          locales       |  fr   |\r\n  └------------------------------------------→ buildDir ─────── ⑶ ────────→┤  jp   ├→ distDir\r\n           Webpack / Gulp / Grunt ...                       gulp-replace    └ zh-cn ┘\r\n```\r\n\r\n\u003e 由上图 `⑶` 可知，我们实际上是借助 Gulp + [`gulp-replace`](https://github.com/lazd/gulp-replace) 强大的流并行处理能力来进行文本的替换\r\n\r\n## § 使用说明\r\n首先安装：`npm i -D i18n-static`，再引入：\r\n\r\n```js\r\nvar i18n = require('i18n-static');\r\ni18n(\u003cconf\u003e);\r\n```\r\n\r\n上述 `conf` 的配置项定义见 [`lib/conf/conf-def.js`](./lib/conf/conf-def.js)，如下所示：\r\n\r\n```js\r\nmodule.exports = {\r\n  // if provided, we don't need to extract locales from srcDir any more\r\n  locales: { type: 'string|object', default: null, required: false },\r\n\r\n  // source code with i18n content, it's required if locales is missing\r\n  srcDir: { type: 'string', default: null, required: '!locales' },\r\n\r\n  // build code dir for i18n\r\n  buildDir: { type: 'string', default: null, required: true },\r\n\r\n  // for gulp.dest\r\n  distDir: { type: 'string', default: null, required: true },\r\n\r\n  // for gulp.src of buildDir (not srcDir), as it's unnecessary to translate vendor files\r\n  glob: { type: 'string|array', default: '*', required: true },\r\n  \r\n  // don't forget the `g` flag\r\n  regDelimeter: { type: 'regexp', default: /_#(.*?)#_/g, required: true },\r\n\r\n  regI18nContent: { type: 'regexp', default: /\u003ci18n\u003e([\\s\\S]*?)\u003c\\/i18n\u003e/g, required: true },\r\n  \r\n  // your mother tongue\r\n  sourceLang: { type: 'string', default: null, required: true },\r\n  \r\n  // defualt language to translate into\r\n  defaultLang: { type: 'string', default: null, required: true },\r\n\r\n  // do not translate into these languages\r\n  excludeLangs: { type: 'array', default: [], required: false },\r\n  \r\n  // set it to a falsy value if not needed\r\n  saveI18nContentTo: { type: 'string|boolean|null|undefined', default: null, required: false },\r\n\r\n  // if the translations do not change frequently, you can save locales to speed up i18n process\r\n  saveLocalesTo: { type: 'string|boolean|null|undefined', default: null, required: false }\r\n};\r\n```\r\n\r\n***\r\n\r\n一般是在 Webpack / Gulp 构建完成后使用：\r\n\r\n```js\r\n// Webpack\r\nwebpack(\u003cWebpack config\u003e, function (err, stat) {\r\n  if (err) return console.error(err);\r\n\r\n  // show build info to console\r\n  console.log(stats.toString({ chunks: false, color: true }));\r\n\r\n  i18n(\u003ci18n conf\u003e);\r\n});\r\n\r\n// Gulp\r\ngulp.task('build', function () {\r\n  return gulp.src(...)\r\n    .pipe(...)\r\n    .on('end', function () {\r\n      i18n(\u003ci18n conf\u003e);\r\n    });\r\n});\r\n```\r\n\r\n## § 注意事项\r\n\r\n* 本项目并不是一个 Gulp 插件，完全可以独立使用\r\n* `conf.locales` 可以是文件路径（支持 YAML / JS object / JSON 格式），也可以直接就是翻译表\r\n* 自定义的 `conf.regDelimeter` 与 `conf.regI18nContent` 都需要带上 `g` 以进行全局匹配\r\n* 以下示例会导致提取的失败，因为 `\u003ci18n\u003e` 与 `\u003c/i18n\u003e` 之间的掺入了 5 个不可解析的 `*`\r\n\r\n```js\r\nfunction show(num) {\r\n  console.log('_#每页显示#_' + num + '_#条记录#_');\r\n}\r\n/**\r\n * \u003ci18n\u003e\r\n * {\r\n *   '每页显示' : 'Show ',\r\n *   '条记录': ' items per page'\r\n * }\r\n * \u003c/i18n\u003e\r\n */\r\n```\r\n\r\n* 不建议整体翻译**带变量**的模板字符串，因为会匹配不上原译文\r\n\r\n```js\r\nfunction show(num) {\r\n  console.log(`_#每页显示 ${num} 条记录#_`)\r\n}\r\n// \u003ci18n\u003e每页显示 ${num} 条记录: 'Show ${num} items per page'\u003c/i18n\u003e\r\n-----------------------------------------------\r\n                   Babel ↓\r\n-----------------------------------------------\r\nfunction show(num) {\r\n  console.log(\"_#每页显示 \" + num + \" 条记录#_\");\r\n}\r\n// \u003ci18n\u003e每页显示 ${num} 条记录: 'Show ${num} items per page'\u003c/i18n\u003e\r\n```\r\n\r\n* Webpack 的开发需要使用 [replace-loader](https://github.com/Va1/string-replace-loader) 来去除待翻译内容的定界符（默认为 `_# #_`）：\r\n\r\n```js\r\nmodule: {\r\n  preLoaders: [{\r\n    test: /.*$/, // 针对所有文件\r\n    loader: 'string-replace?search=(_#|#_)\u0026replace=\u0026flags=g'\r\n    exclude: /node_modules/\r\n  }]\r\n}\r\n```\r\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fonewaytech%2Fi18n-static","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fonewaytech%2Fi18n-static","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fonewaytech%2Fi18n-static/lists"}