通用的静态资源国际化方案
i18n-static
forked fromIntellectual property of OneWay
强烈建议 Chrome 用户安装 Octotree、Octo Mate、OctoLinker 插件以提高阅读体验
§ 快速体验
定界符 _#
与 #_
包裹的 XXX
为待翻译内容,而 <i18n>
内容为其对应的译文(支持 YAML / JS 对象 / JSON 三种格式):
{ console;}/*<i18n>每页显示: 'Show '条记录: ' items per page'</i18n>*/----------------------------------------------- Uglify + i18n ↓-----------------------------------------------{console}
待翻译内容 和 译文 的定界符都是可配置的
§ 灵感来源
试想,如果让您把公司所有的项目都支持国际化,会是一种怎么样的体验?
这些项目新旧不一:
- 有的是 Grunt + RequireJS / Sea.js + jQuery / Zepto
- 有的是 Gulp + Browserify + Backbone / Knockout / ...
- 更多的是 Webpack
(1.x|2.x)
+ React(0.x|15.x)
/ Vue.js(0.x|1.x|2.x)
/ Angular(1.x|2.x)
/ ... - 还有的是完全没有工程化的静态页
由于技术栈极其混杂,且不可能一个一个坑去踩,因此以下这些相对主流的国际化方案都不可能采用:
i18n-webpack-plugin
/i18n-loader
/babel-plugin-i18n
react-intl
/redux-i18n
vue-i18n
/vuex-i18n
angular-translate
/ng2-translate
jquery.i18n
/jquery-i18n
/jquery-i18n-properties
虽说上述的所有方案都不予采用,但可以总结它们的经验,造一个更好用更通用的轮子
国际化方案大致分为两类:编译时翻译与运行时翻译,各有千秋。不过在二者的选择上,我更倾向于前者
作为通用的国际化方案,不应受技术栈更迭的影响。运行时翻译强依赖于所用的框架/类库,故不符合要求
所谓的国际化,说白了就是字符串的替换,最常见的就是把整个网站所有待翻译内容都汇总起来做成翻译表:
// en.json
// jp.json
(此处省略十几个翻译表...)
这种做法最大的缺陷在于维护困难,冗余不堪,源码中待翻译内容的任何改动都需要同步到所有的翻译表
且由于脱离了上下文,因此校对的时候非常痛苦
如果没有遇见 Vux(基于 Vue.js 的移动 UI 组件库) 的这个 issue,就没有本方案的诞生。它的组件国际化方案如下:
title: zh-CN: 标题 content: en: content zh-CN: 内容
其配备的 vux-loader
可以解析 <i18n>
内容,来对当前组件中的待翻译内容进行替换
这样的话就完美地解决了单独维护翻译表的问题,且由于都在同一上下文中,因此更改与校对都显得非常方便!
vux-loader
之所以能这么做,是因为 vue-loader
只在乎 <template>
、<script>
、<style>
,其他的都忽略
这给了我很大的启发!
由于前端的特殊性,线上的代码一般都需要经过压缩处理,因此我们还可以更进一步:
通过注释的方式,直接在待翻译内容旁编写其译文!
当然也完全可以像
vux-loader
那样,把一个文件中所有待翻译内容集中到最后进行翻译
进行国际化的时候,只需要提取源码目录中散落的翻译表,即可获得最终的翻译表
我们还能将翻译表保存下来,既满足高可维护性,又满足传统方案的整体校对需求
§ 流程详解
我们使用 example/
下的这个简单的例子来说明本国际化方案的流程
首先我们来分析一下 package.json
里面的 npm scripts
:
"scripts": {
# 删除原有的 build 目录
"clean": "rimraf example/dist",
# 运行 npm run build 前的预备工作
"prebuild": "npm run clean && mkdirp example/dist/__build__",
# 使用 html-minifier 压缩 HTML 文件,使用 Browserify 与 Uglifyify 合并压缩 vendor.js 与 app.js
"build:html": "html-minifier --remove-comments --file-ext html --input-dir example/src --output-dir example/dist/__build__",
"build:js:vendor": "browserify example/src/vendor/ -o example/dist/__build__/vendor.js",
"build:js:app": "browserify example/src/app.js -o example/dist/__build__/app.js",
"build": "npm run build:html && npm run build:js:vendor && npm run build:js:app",
# 执行国际化任务
"i18n": "node example/i18n.js",
# 运行例子
"example": "npm run build && npm run i18n",
# 静态检测
"lint": "jshint lib/"
}
敲下 npm run example
后,其实就是执行 npm run build
与 npm run i18n
npm run build
1. 标准的构建工作流,一般是 Webpack / Grunt / Gulp 等构建工具处理文件后吐出到一个目录中
(在这里我们为了简单,仅用 npm scripts
来完成)
example/
├── dist/
│ └── __build__/
│ ├── app.js ←------┐
│ ├── index.html ←---|---┐
│ └── vendor.js ←---|---|---┐
| | | |
└── src/ | | |
├── app.js --------┬---┘ | | # 打包合并压缩混淆...
├── index.html ----|-------┘ | # html-minifier
├── main/ | | # Browserify + UglifyJS
│ ├── main1.js --┤ |
│ └── main2.js --┘ |
└── vendor/ |
├── index.js ------┬-------┘
├── vendor1.js ----┤
├── vendor2.js ----┤
└── vendor3.js ----┘
反正处理后的文件最终都到了 example/dist/__build__/
以上并不是我们的重点,因为每个项目都有不同的构建工作流
重点是下面,对 example/dist/__build__/
的所有静态资源进行国际化
npm run i18n
2. 对应的命令是 node example/i18n.js
,因此我们来看看 example/i18n.js
:
var path = i18n = ; ;
这里要着重解释一下 srcDir
/ buildDir
/ distDir
的含义:
srcDir
,源码目录,用于提取翻译表buildDir
,构建工具处理源码后,所生成静态文件的存放目录distDir
,我们的i18n
工具将buildDir
内所有静态资源翻译后所生成文件的存放处
还有就是 sourceLang
与 defaultLang
的区别:
sourceLang
,源语言,即_#XXX#_
直接生成XXX
,无需翻译defaultLang
,默认翻译语言,即:
<i18n>{ 'XXX': 'XXX-default' }</i18n>
等同于<i18n>{ 'XXX': { [defaultLang]: 'XXX-default' } }</i18n>
defaultLang
适用于仅提供两种语言版本的应用场景,应用见example/src/vendor/vendor1.js
最后是:
saveLocalesTo
,即翻译表的保存路径,用于人工核对翻译,且有利于提高处理效率
为什么说可以提高处理效率?
皆因i18n(conf)
函数还能接受conf.locales
配置
若提供该参数,则直接使用该翻译表而非重新遍历srcDir
提取
对于待翻译内容不常更改的项目而言,此举可减少一些编译耗时
配置讲完了,接下来是国际化三步走:
⑴ 提取
从 srcDir
中递归提取出所有 <i18n>
的内容进行解析合并(称为 i18nContent
):
⑵ 转换
将上述内容转换成翻译表(称为 locales
):
由于
sourceLang
设置为zh-cn
,因此locales['zh-cn']
为空,表示不翻译
当然还有locales.jp
与locales.fr
都缺少「好的
」的译文,因此也是不翻译
⑶ 替换
最后,就是根据翻译表对 example/dist/__build__/
进行翻译,最终生成:
example/dist/
├── __build__/
│ ├── app.js
│ ├── index.html
│ └── vendor.js
├── en/ # defaultLang
│ ├── app.js
│ ├── index.html
│ └── vendor.js
├── fr/
│ ├── app.js
│ ├── index.html
│ └── vendor.js
├── jp/
│ ├── app.js
│ ├── index.html
│ └── vendor.js
├── zh-cn/ # sourceLang
| ├── app.js
| ├── index.html
| └── vendor.js
└── locales.json # saveLocalesTo *here*
※ 总流程图
recursive-readdir-sync
srcDir ─────────────────────────┐
| ↓ >_ npm run i18n
| for each file in files:
| ⑴ extract-i18n-content(file)
| |
| ↓ i18n-content2locales
| i18nContent ───────── ⑵ ────────────┐
| ↓ ┌ en ┐
| >_ npm run build locales | fr |
└------------------------------------------→ buildDir ─────── ⑶ ────────→┤ jp ├→ distDir
Webpack / Gulp / Grunt ... gulp-replace └ zh-cn ┘
由上图
⑶
可知,我们实际上是借助 Gulp +gulp-replace
强大的流并行处理能力来进行文本的替换
§ 使用说明
首先安装:npm i -D i18n-static
,再引入:
var i18n = ;;
上述 conf
的配置项定义见 lib/conf/conf-def.js
,如下所示:
moduleexports = // if provided, we don't need to extract locales from srcDir any more locales: type: 'string|object' default: null required: false // source code with i18n content, it's required if locales is missing srcDir: type: 'string' default: null required: '!locales' // build code dir for i18n buildDir: type: 'string' default: null required: true // for gulp.dest distDir: type: 'string' default: null required: true // for gulp.src of buildDir (not srcDir), as it's unnecessary to translate vendor files glob: type: 'string|array' default: '*' required: true // don't forget the `g` flag regDelimeter: type: 'regexp' default: /_##_/g required: true regI18nContent: type: 'regexp' default: /<i18n><\/i18n>/g required: true // your mother tongue sourceLang: type: 'string' default: null required: true // defualt language to translate into defaultLang: type: 'string' default: null required: true // do not translate into these languages excludeLangs: type: 'array' default: required: false // set it to a falsy value if not needed saveI18nContentTo: type: 'string|boolean|null|undefined' default: null required: false // if the translations do not change frequently, you can save locales to speed up i18n process saveLocalesTo: type: 'string|boolean|null|undefined' default: null required: false ;
一般是在 Webpack / Gulp 构建完成后使用:
// Webpack; // Gulpgulp;
§ 注意事项
- 本项目并不是一个 Gulp 插件,完全可以独立使用
conf.locales
可以是文件路径(支持 YAML / JS object / JSON 格式),也可以直接就是翻译表- 自定义的
conf.regDelimeter
与conf.regI18nContent
都需要带上g
以进行全局匹配 - 以下示例会导致提取的失败,因为
<i18n>
与</i18n>
之间的掺入了 5 个不可解析的*
{ console;}/** * <i18n> * { * '每页显示' : 'Show ', * '条记录': ' items per page' * } * </i18n> */
- 不建议整体翻译带变量的模板字符串,因为会匹配不上原译文
{ console}// <i18n>每页显示 ${num} 条记录: 'Show ${num} items per page'</i18n>----------------------------------------------- Babel ↓----------------------------------------------- { console;}// <i18n>每页显示 ${num} 条记录: 'Show ${num} items per page'</i18n>
- Webpack 的开发需要使用 replace-loader 来去除待翻译内容的定界符(默认为
_# #_
):
module: preLoaders: test: /.*$/ // 针对所有文件 loader: 'string-replace?search=(_#|#_)&replace=&flags=g' exclude: /node_modules/