1 国际化方案选择
国际化的方案有很多
-
按照语言种类分别开发前端界面
这种方式在大型项目中不适用,浪费劳动力,并且维护困难;
-
实用配置文件
使用一套界面,同样的样式文件,调用对应的语言文件进行渲染,该方式可以快速实现,并且只用维护一套前端文件,非常适用于单页应用;
- 定义国际化配置
- 根据环境读取配置
- 将配置展现在页面上
展开说:
- 定义国际化配置:定义的方式有多种,多以文件的形式单独保存,如json,js,properties 等, 并且将配置信息以键值对的形式保存备用
- 根据环境读取配置:就是用户选择的标志,形式如下: hash型:#cn; #en; #us saerch型:?lan=cn; ?lan=en; ?lan=us url/meta型: 163.com/cn/; 163.com/en 缓存型:缓存形式多为cookie,默认cn,用户重新设定后将缓存更新
- 将配置展现在页面上: 使用三方插件或者自己编写插件将配置信息映射到页面上,基本原理都是做字典查询键值匹配替换。
以上三步任意组合都可以完成国际化的任务,只是效率各有不同,可根据项目做自由组合
显然,第二种方式是现在国际化的主流,其针对不同技术栈也有不同的适配方案,比如:
vue+vue-i18nangular+angular-translatereact+react-intljquery+jquery.i18n.property
本项目是原生小程序项目,小程序官方文档已经给出了国际化方案 ——miniprogram-i18n。
2 接入i18n
首先看一下小程序切换语言相关的流程思路:
miniprogram-i18n 的用法主要分为四部分。分别是:构建脚本与i18配置、i18n文本定义、WXML中的用法及JavaScript中的用法。
根据上述流程,开始接入i18n,接入方案如下:
2.1 安装
该方案目前需要依赖 Gulp 并且对源文件目录结构有一定的要求,需要确保小程序源文件放置在特定目录下(例如 src/ 目录)。
- 首先在项目根目录运行以下命令安装 gulp 及 miniprogram-i18n 的 gulp 插件。
npm i -D gulp @miniprogram-i18n/gulp-i18n-locales @miniprogram-i18n/gulp-i18n-wxml
- 在小程序运行环境下安装国际化运行时并在开发工具"构建npm"。
npm i -S @miniprogram-i18n/core
- 在项目根目录新建 gulpfile.js,并编写构建脚本,参考文档: examples/gulpfile.js。
const gulpI18nWxml = require('@miniprogram-i18n/gulp-i18n-wxml');
const gulpI18nLocales = require('@miniprogram-i18n/gulp-i18n-locales');
const weappAliasConfig = {
'@': path.join(__dirname, './src'),
};
gulp.task('compile:less', cb => {
pump(
[
...
weappAlias(weappAliasConfig),
...
],
cb,
);
});
/**
* 国际化,复制国际化文件到dist
*/
gulp.task('mergeAndGenerateLocales', () => {
return gulp
.src(sourceRoot + '/**/i18n/*.json')
.pipe(gulpI18nLocales({ defaultLocale: 'CN', fallbackLocale: 'CN' }))
.pipe(gulp.dest(dist + '/i18n/'));
});
/**
* 国际化,遍历所有wxml,进行转换复制到dist
*/
gulp.task('transpileWxml', () => {
return (
gulp
.src('src/**/*.wxml')
// .pipe(debug())
.pipe(gulpI18nWxml())
.pipe(gulp.dest(dist))
);
});
2.2 i18n文本定义
miniprogram-i18n 目前采用 JSON 文件对 i18n 文本进行定义。使用之前,需要在项目源文件下新建 i18n 目录。
目录结构如下:
├── dist // 小程序构建目录
├── gulpfile.js
├── node_modules
├── package.json
└── src // 小程序源文件目录
| ├── app.js
| ├── app.json
| ├── app.wxss
| ├── i18n // 国际化文本目录
| | ├── en-US.json
| | └── zh-CN.json
i18n 目录可以放置在源文件目录下的任意位置,例如可以跟 Page 或 Component 放在一起。但是需要注意的是,多个 i18n 目录下的文件在构建时会被合并打包,因此如果翻译文本有重复的 key 可能会发生覆盖。如果分开多个 i18n 目录定义需要自行确保 key 是全局唯一的。例如使用 page.index.testKey 这样的能确保唯一的名称
/* 定义 */
// src/config/index.ts
// 定义枚举 - 语言类型
export enum LANG {
CN = 'CN',
EN = 'EN',
}
// src/config/constants.ts
// 定义变量 - 语言
export const SYSTEM_LANGUAGE = 'system-language';
/* 定义文本 */
// i18n/EN.json
{
"plainText": "This is a plain text",
"withParams": "{value} is what you pass in"
}
// i18n/CN.json
{
"plainText": "这是一段纯文本",
"withParams": "你传入的值是{value}"
}
2.3 在storage中保存/读取语言类型
/* basePage.ts */
import { getI18nInstance } from '@miniprogram-i18n/core';
export const getSystemLanguage = () => getStorage(SYSTEM_LANGUAGE) || LANG.CN;
export const setSystemLanguage: (params: string) => void = lang => {
const _lang = lang || LANG.CN;
setStorage(SYSTEM_LANGUAGE, _lang);
const i18n = getI18nInstance();
i18n.setLocale(_lang); // 设置i18n语言类型
};
2.4 设置请求拦截器,将language加入请求头
/* HTTPClient.ts */
//添加请求拦截器
// 请求拦截器中的request对象结构如下:
httpClient.interceptors.request.use(request => {
// 如果构建参数中有泳道,header头添加泳道字段
if (tversion) request.headers['tversion'] = tversion;
// 获取小程序消息订阅配置时,小程序还未实例化,因此这边加了个条件判断
if (getApp() === undefined) {
return request;
}
const token = getToken();
const systemLanguage = getSystemLanguage();
// 检测token 的过期时间
const invalidToken = checkTokenInvalid();
request.headers['system-language'] = systemLanguage; // 加入语言请求头
...
return request;
});
2.5 抽取语言切换按钮组件
import { BaseComponent } from '../../../core/base/baseComponent';
import { wxComponent } from '../../../core/decorator/index';
import { getSystemLanguage, setSystemLanguage } from '@/utils/base';
import { setLanguage } from '@/services/membership/index';
import { getI18nInstance, I18n } from '@miniprogram-i18n/core';
import Toast from '@/core/base/helpers/toast';
@wxComponent()
export default class extends BaseComponent {
behaviors = [I18n];
properties = {
isAuth: {
type: Boolean,
default: false,
},
langType: {
type: String,
default: 'membership',
},
};
data = {
language: this.$global('lang') || 'CN', // 中文:CN,英文:EN
};
attached() {
const lang = getSystemLanguage(); // 获取本地 storage 语言
this.setData({
language: lang,
});
// 当检测到本地语言类型改变时触发 ( 如静默登录后语言改变 )
getI18nInstance().onLocaleChange(val => {
this.setData({
language: val,
});
});
}
handleButtonPress(e: any) {
let lang;
// 判断用户是否登录,已登录则设置语言类型
if (this.properties.isAuth) {
lang = e.target.dataset.lang;
} else {
lang = e.detail.language;
}
const language = getSystemLanguage();
// 判断所选语言是否是当前语言
if (language !== lang) {
this.handleSetLang(lang);
}
this.handleChange();
}
// 设置语言
handleSetLang(lang: string) {
// 发送语言设置请求
setLanguage({ language: lang }).then(data => {
const { code } = data;
if (code === 'Success') {
// 请求成功修改本地语言类型
setSystemLanguage(lang);
getApp().globalData.events.emit('srm:switchLang', lang);
this.toast(this.t('components.BizChangeLangSuccess'));
this.setData({
language: lang,
});
}
});
}
}
/* 未登陆时进入language-button-container组件 */
@wxComponent()
export default class PhoneContainer extends BaseComponent {
properties = {
...
};
attached() {
getLoginCodeWrap().then(code => {
this.setData({
code,
});
});
}
data = {
code: undefined,
};
// 已经授权
handleClick() {
this.triggerEvent('getuserinfo', {
isBind: true,
language: this.properties.language,
type: this.properties.type,
url: this.properties.url,
toBuy: this.properties.toBuy,
});
}
// @getWxCode()
getUserInfo(
e: WechatMiniprogram.Event<
WechatMiniprogram.GetUserInfoSuccessCallbackResult
>,
) {
const { code } = this.data;
// 用户成功授权
wx.getSetting({
success: res => {
// 授权了
if (res.authSetting['scope.userInfo']) {
const { session }: { session: SessionType } = getApp().globalData;
const hasUser: boolean = session.hasUser();
// 有用户信息
if (hasUser) {
this.triggerEvent('getuserinfo', {
...e.detail,
isBind: false,
code,
language: this.properties.language,
type: this.properties.type,
url: this.properties.url,
});
} else {
session.authLogin({ ...e.detail, code }).then((isBind: boolean) => {
this.triggerEvent('getuserinfo', {
...e.detail,
isBind,
code,
language: this.properties.language,
type: this.properties.type,
url: this.properties.url,
});
});
}
}
},
complete: res => console.log('请求设置信息:', res),
});
}
}
3 如何使用
3.1 普通用法
WXML中的用法
定义好 i18n 文本之后,就可以在 WXML 文件里使用了。
<view class="membership-one {{activeIndex === 0 ? 'active': ''}}" data-index="0" bind:tap="clickTab">
{{t('membership.vip')}}
</view>
<input placeholder="{{ t('withParams', {value}) }}"></input>
JavaScript 中的用法
在 JavaScript 里可以直接引用 @miniprogram-i18n/core 这个 NPM 包来获取翻译文本。
import { getI18nInstance } from '@miniprogram-i18n/core'
const i18n = getI18nInstance()
@wxPage()
export default class extends BasePage {
}
Component({
onLoad() {
const text = i18n.t('withParams', { value: 'Test' })
console.log(text) // Test is what you pass in
}
})
这种用法每个页面都会生成一个i18n实例,可以进行优化,如下:
3.2 在Page中使用
注意:这里建议 Page 以及 Component 都采用 Component 构造器进行定义,这样可以使用 I18n 这个 Behavior。如果需要在 Page 构造上使用 I18n 则需要引入 I18nPage 代替 Page 构造器。
import { wxApp, wxDecoratorConfig } from '@/core/decorator/index';
import { I18nPage } from '@miniprogram-i18n/core';
// 系统启动更新环境
@wxApp()
export default class MyApp extends BaseApp {
async onLaunch(options) {
...
wxDecoratorConfig.Page = (...args) => {
// 因为没有使用插件,sr和emonitor是可以直接默认使用
return I18nPage(...args);
};
...
}
...
}
页面装饰器增加页面标题国际化配置,封装到wxPage
import { getI18nInstance } from '@miniprogram-i18n/core';
export function wxPage(decoratorOptions?: DecoratorOptions) {
const { storeBindingOptions, title } = decoratorOptions || {};
const i18n = getI18nInstance();
return function(constructor: new () => BasePage): void {
class WxPage extends constructor {
storeBindings: any;
constructor(..._args: any[]) {
super();
}
...
// 设置页面头部标题
async onLoad(options?: any) {
if (title) {
wx.setNavigationBarTitle({
title: i18n.t(title),
});
i18n.onLocaleChange(() => {
wx.setNavigationBarTitle({
title: i18n.t(title),
});
});
}
...
}
...
}
const current = new WxPage();
const obj = toObject(current);
wxDecoratorConfig.Page(obj);
};
}
设置好后在.ts页面,可以通过调用this.t('xxx'),来获取文案
3.3 在Component中使用
增加配置,增加I18n behavior,封装到wxComponent
import { I18n } from '@miniprogram-i18n/core';
export function wxComponent(decoratorOptions?: DecoratorOptions) {
return function(constructor: new () => BaseComponent): void {
class WxComponent extends constructor {
storeBindings: any;
constructor(..._args: any[]) {
super();
}
...
}
const current = new WxComponent();
// console.log(current);
const obj = toComponent(toObject(current));
obj.behaviors = wxDecoratorConfig.extendBehaviors.concat(
obj.behaviors || [],
[I18n],
);
Component(obj);
};
}
// 设置后即可在页面通过调用this.t('xxx'),来获取文案内容
this.toast(this.t('components.BizChangeLangSuccess'));
4 几种插值用法
4.1 文本插值
/*EN.json*/
{
"key": "Inserted value: {value}"
}
/*CN.json*/
{
"key": "插入的值是: {value}"
}
i18n.t('key', { value: 'Hello!' }) // Inserted value: Hello!
为了方便调用深层嵌套的对象,当前支持使用 . 点语法来访问对象属性。
/*CN.json*/
{
"dotted": "嵌套的值是: { obj.nested.value }"
}
/*EN.json*/
{
"dotted": "Nested value is: { obj.nested.value }"
}
const value = {
obj: {
nested: {
value: 'Catch you!'
}
}
}
i18n.t('dotted', value) // Nested value is: Catch you!
4.2 select 语句
/*EN.json*/
{
"key": "{gender, select, male {His inbox} female {Her inbox} other {Their inbox}}"
}
i18n.t('key', { gender: 'male' }) // His inbox
i18n.t('key', { gender: 'female' }) // Her inbox
i18n.t('key')
select 语句支持子语句文本插值:
/*EN.json*/
{
"key": "{mood, select, good {{how} day!} sad {{how} day.} other {Whatever!}}"
}
/*xxx.ts*/
i18n.t('key', { mood: 'good', how: 'Awesome' }) // Awesome day!
i18n.t('key', { mood: 'sad', how: 'Unhappy' }) // Unhappy day.
i18n.t('key') // Whatever!
注:select 语句支持子句嵌套 select 语句