mm_lang 2.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -10
- package/example/index.js +7 -15
- package/index.js +11 -17
- package/lib/lang.js +332 -290
- package/lib/langs.js +32 -32
- package/lib/langs_admin.js +13 -0
- package/lib/tools/index.js +16 -0
- package/lib/tools/lang/index.js +59 -0
- package/package.json +6 -5
- package/tool/lang/index.js +7 -0
- package/tool/lang/tool.json +84 -0
- package/lib/store.js +0 -190
package/lib/lang.js
CHANGED
|
@@ -1,44 +1,138 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 内置语言类
|
|
3
|
+
* 国际化(i18n)核心:扁平词条表上的取词、回退、复数与格式化
|
|
4
|
+
* 词条表与持久化同构:内存即 `语言代码.层级键 → 文本` 的扁平形态,save/load 无需转换
|
|
5
|
+
* 只接受扁平入参(键为 `语言.层级键`,或控制键),嵌套语言包由调用方展开(如 mm_tpl)
|
|
6
|
+
* 持久化通过 langsAdmin 管理的 Langs 存储实例(this.store)完成,本类不做文件 IO
|
|
7
|
+
* @class
|
|
3
8
|
*/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
unflatten,
|
|
7
|
-
assignDeep,
|
|
8
|
-
isControlKey,
|
|
9
|
-
createStore
|
|
10
|
-
} = require('./store.js');
|
|
9
|
+
|
|
10
|
+
const langs_admin = require('./langs_admin.js');
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
* 非词条的控制键,不参与取词、语言列表与持久化
|
|
14
|
+
* 词条键必含点号(`语言.键`),控制键均不含点号,二者天然可分
|
|
15
|
+
* @type {Array<string>}
|
|
16
|
+
*/
|
|
17
|
+
const CONTROL_KEYS = ['default', 'fallback', 'locale', 'options', 'file', 'store', 'silent'];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 非字符串词条(复数数组等)落盘时的标记前缀
|
|
21
|
+
* 落盘后仍是字符串,满足 Langs 的 string→string 约束;读取时按前缀还原
|
|
22
|
+
* @type {string}
|
|
23
|
+
*/
|
|
24
|
+
const MARK = '\u0001';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 判断是否为控制键
|
|
28
|
+
* @param {string} key 键名
|
|
29
|
+
* @returns {boolean} 是控制键返回 true
|
|
30
|
+
*/
|
|
31
|
+
function isControlKey(key) {
|
|
32
|
+
return CONTROL_KEYS.indexOf(key) !== -1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 校验参数为非空字符串,否则抛出类型错误
|
|
37
|
+
*/
|
|
38
|
+
function checkKey(val, name) {
|
|
39
|
+
if (!val || typeof val !== 'string') {
|
|
40
|
+
throw new TypeError(name + '必须是字符串');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 将词条值编码为可落盘的字符串(数组加标记后 JSON 化;数字/布尔转字符串)
|
|
46
|
+
* @param {*} val 词条值
|
|
47
|
+
* @returns {string|null} 存储字符串,不可序列化的值返回 null
|
|
48
|
+
*/
|
|
49
|
+
function encodeValue(val) {
|
|
50
|
+
if (typeof val === 'string') {
|
|
51
|
+
return val;
|
|
52
|
+
}
|
|
53
|
+
if (Array.isArray(val)) {
|
|
54
|
+
return MARK + JSON.stringify(val);
|
|
55
|
+
}
|
|
56
|
+
if (typeof val === 'number' || typeof val === 'boolean') {
|
|
57
|
+
return String(val);
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 将落盘字符串还原为词条值(带标记前缀的还原为数组)
|
|
64
|
+
* @param {string} val 存储字符串
|
|
65
|
+
* @returns {*} 词条值
|
|
66
|
+
*/
|
|
67
|
+
function decodeValue(val) {
|
|
68
|
+
if (typeof val !== 'string' || val.charAt(0) !== MARK) {
|
|
69
|
+
return val;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(val.slice(1));
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return val;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 按点号路径向对象写入值(仅 getDict 导出单语言词条时使用)
|
|
80
|
+
* @param {Object} obj 目标对象
|
|
81
|
+
* @param {string} path 点号路径
|
|
82
|
+
* @param {*} val 值
|
|
83
|
+
* @returns {void}
|
|
18
84
|
*/
|
|
85
|
+
function setByPath(obj, path, val) {
|
|
86
|
+
var keys = path.split('.');
|
|
87
|
+
var cur = obj;
|
|
88
|
+
for (var i = 0; i < keys.length - 1; i++) {
|
|
89
|
+
var next = cur[keys[i]];
|
|
90
|
+
if (!next || typeof next !== 'object' || Array.isArray(next)) {
|
|
91
|
+
next = cur[keys[i]] = {};
|
|
92
|
+
}
|
|
93
|
+
cur = next;
|
|
94
|
+
}
|
|
95
|
+
cur[keys[keys.length - 1]] = val;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 两位数补零
|
|
100
|
+
* @param {number} n 数值
|
|
101
|
+
* @returns {string} 补零后的字符串
|
|
102
|
+
*/
|
|
103
|
+
function pad(n) {
|
|
104
|
+
return String(n).padStart(2, '0');
|
|
105
|
+
}
|
|
106
|
+
|
|
19
107
|
class Lang {
|
|
20
108
|
/**
|
|
21
109
|
* 创建语言实例
|
|
22
|
-
* @param {Object} config -
|
|
110
|
+
* @param {Object} config - 扁平词条表:键为 `语言.层级键`,值为字符串/数字/布尔/数组;
|
|
111
|
+
* 可含控制键 default、fallback、locale、silent 及存储配置 file、store
|
|
23
112
|
*/
|
|
24
113
|
constructor(config = {}) {
|
|
25
|
-
var cg = Object.assign({
|
|
114
|
+
var cg = Object.assign({
|
|
115
|
+
default: 'zh_cn',
|
|
116
|
+
fallback: true
|
|
117
|
+
}, config);
|
|
26
118
|
var file = cg.file || '';
|
|
27
119
|
var injected = cg.store;
|
|
28
|
-
// file / store 属于存储配置,不进入词条字典,避免被当作语言代码
|
|
29
120
|
delete cg.file;
|
|
30
121
|
delete cg.store;
|
|
31
122
|
|
|
123
|
+
// 词条值不能是嵌套对象,嵌套语言包需由调用方展开为扁平键
|
|
124
|
+
for (var key in cg) {
|
|
125
|
+
var val = cg[key];
|
|
126
|
+
if (!isControlKey(key) && val && typeof val === 'object' && !Array.isArray(val)) {
|
|
127
|
+
throw new TypeError('词条 "' + key + '" 的值不能是对象,请使用扁平键(语言.层级键)');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
32
131
|
/**
|
|
33
|
-
*
|
|
132
|
+
* 扁平词条表,键为 `语言代码.层级键`,与持久化格式一致;控制键并存于同一层
|
|
34
133
|
* @type {Object}
|
|
35
134
|
*/
|
|
36
|
-
this.config =
|
|
37
|
-
zh_cn: {},
|
|
38
|
-
en: {},
|
|
39
|
-
default: 'zh_cn',
|
|
40
|
-
fallback: true
|
|
41
|
-
}, cg);
|
|
135
|
+
this.config = cg;
|
|
42
136
|
|
|
43
137
|
/**
|
|
44
138
|
* 复数规则表,键为语言代码,值为按数量返回形式的索引的函数
|
|
@@ -57,7 +151,7 @@ class Lang {
|
|
|
57
151
|
* 当前语言代码
|
|
58
152
|
* @type {string}
|
|
59
153
|
*/
|
|
60
|
-
this.now =
|
|
154
|
+
this.now = cg.default || 'zh_cn';
|
|
61
155
|
|
|
62
156
|
/**
|
|
63
157
|
* 区域映射表,完整语言代码到基础语言代码
|
|
@@ -66,7 +160,7 @@ class Lang {
|
|
|
66
160
|
this.locale = Object.assign({
|
|
67
161
|
zh: 'zh_cn',
|
|
68
162
|
en: 'en'
|
|
69
|
-
},
|
|
163
|
+
}, cg.locale);
|
|
70
164
|
|
|
71
165
|
/**
|
|
72
166
|
* 持久化文件路径
|
|
@@ -75,49 +169,61 @@ class Lang {
|
|
|
75
169
|
this.file = file;
|
|
76
170
|
|
|
77
171
|
/**
|
|
78
|
-
*
|
|
172
|
+
* 语言存储实例:优先用注入实例,否则按文件路径经 langsAdmin 取共享实例,
|
|
173
|
+
* 两者都无则为 null(纯内存)
|
|
79
174
|
* @type {Langs|null}
|
|
80
175
|
*/
|
|
81
|
-
this.store =
|
|
176
|
+
this.store = injected || (file ? langs_admin.get(file, { file: file }) : null);
|
|
82
177
|
}
|
|
83
178
|
}
|
|
84
179
|
|
|
85
180
|
/**
|
|
86
|
-
*
|
|
87
|
-
* @param {string} key - 键,支持 a.b 形式的层级键
|
|
88
|
-
* @param {...any} param - 参数,依次替换 {0}、{1} 等占位符
|
|
89
|
-
* @returns {string} 翻译后的文本
|
|
181
|
+
* 取以 `语言.` 为前缀的全部词条键 - 内部方法
|
|
90
182
|
*/
|
|
91
|
-
Lang.prototype.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
for (var i = 0; i < param.length; i++) {
|
|
98
|
-
var reg = new RegExp('\\{' + i + '\\}', 'ig');
|
|
99
|
-
val = val.replace(reg, param[i]);
|
|
183
|
+
Lang.prototype._keysOf = function (lang) {
|
|
184
|
+
var prefix = lang + '.';
|
|
185
|
+
var keys = [];
|
|
186
|
+
for (var key in this.config) {
|
|
187
|
+
if (key.indexOf(prefix) === 0) {
|
|
188
|
+
keys.push(key);
|
|
100
189
|
}
|
|
101
190
|
}
|
|
191
|
+
return keys;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 占位符替换 - 内部方法
|
|
196
|
+
* useCount 为 true 时先替换 {count}(复数场景),序号占位符从 {1} 开始;否则从 {0} 开始
|
|
197
|
+
*/
|
|
198
|
+
Lang.prototype._fill = function (str, count, param, useCount) {
|
|
199
|
+
var val = String(str);
|
|
200
|
+
if (useCount) {
|
|
201
|
+
val = val.replace(/\{count\}/ig, count);
|
|
202
|
+
}
|
|
203
|
+
var offset = useCount ? 1 : 0;
|
|
204
|
+
for (var i = 0; i < param.length; i++) {
|
|
205
|
+
val = val.replace(new RegExp('\\{' + (i + offset) + '\\}', 'ig'), param[i]);
|
|
206
|
+
}
|
|
102
207
|
return val;
|
|
103
208
|
};
|
|
104
209
|
|
|
105
210
|
/**
|
|
106
|
-
*
|
|
211
|
+
* 带回退机制获取词条 - 内部方法
|
|
107
212
|
* 依次尝试:当前语言、基础语言、默认语言
|
|
108
213
|
* @param {string} key - 键
|
|
109
214
|
* @returns {any} 语言值
|
|
110
215
|
* @private
|
|
111
216
|
*/
|
|
112
217
|
Lang.prototype._getVal = function (key) {
|
|
113
|
-
var val = this.
|
|
218
|
+
var val = this.config[this.now + '.' + key];
|
|
114
219
|
if (val === undefined && this.config.fallback) {
|
|
115
|
-
var
|
|
116
|
-
if (
|
|
117
|
-
val = this.
|
|
220
|
+
var base = this._getBase(this.now);
|
|
221
|
+
if (base !== this.now) {
|
|
222
|
+
val = this.config[base + '.' + key];
|
|
118
223
|
}
|
|
119
|
-
|
|
120
|
-
|
|
224
|
+
var def = this.config.default;
|
|
225
|
+
if (val === undefined && def && def !== this.now) {
|
|
226
|
+
val = this.config[def + '.' + key];
|
|
121
227
|
}
|
|
122
228
|
}
|
|
123
229
|
if (val === undefined) {
|
|
@@ -126,32 +232,6 @@ Lang.prototype._getVal = function (key) {
|
|
|
126
232
|
return val;
|
|
127
233
|
};
|
|
128
234
|
|
|
129
|
-
/**
|
|
130
|
-
* 从指定语言中获取值 - 内部方法
|
|
131
|
-
* @param {string} lang - 语言代码
|
|
132
|
-
* @param {string} key - 键
|
|
133
|
-
* @returns {any} 值
|
|
134
|
-
* @private
|
|
135
|
-
*/
|
|
136
|
-
Lang.prototype._getFrom = function (lang, key) {
|
|
137
|
-
var lang_cg = this.config[lang];
|
|
138
|
-
if (!lang_cg) {
|
|
139
|
-
return undefined;
|
|
140
|
-
}
|
|
141
|
-
var arr = key.split('.');
|
|
142
|
-
var val = lang_cg;
|
|
143
|
-
for (var i = 0; i < arr.length; i++) {
|
|
144
|
-
if (val === undefined) {
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
val = val[arr[i]];
|
|
148
|
-
if (val === undefined || typeof (val) === 'string') {
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return val;
|
|
153
|
-
};
|
|
154
|
-
|
|
155
235
|
/**
|
|
156
236
|
* 获取基础语言代码 - 内部方法
|
|
157
237
|
* @param {string} lang - 完整语言代码
|
|
@@ -167,91 +247,32 @@ Lang.prototype._getBase = function (lang) {
|
|
|
167
247
|
};
|
|
168
248
|
|
|
169
249
|
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* @param {
|
|
173
|
-
* @
|
|
174
|
-
|
|
175
|
-
Lang.prototype.set = function (key, val) {
|
|
176
|
-
if (!key || typeof (key) !== 'string') {
|
|
177
|
-
throw new TypeError('key必须是字符串');
|
|
178
|
-
}
|
|
179
|
-
return this._set(key, val);
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* 设置语言键值 - 内部方法
|
|
184
|
-
* @param {string} key - 键名
|
|
185
|
-
* @param {any} val - 值
|
|
186
|
-
* @returns {any} 返回设置的值
|
|
250
|
+
* 输出日志 - 内部方法,使用 console
|
|
251
|
+
* 配置 silent 为 true 时静默(供上层自带日志体系的场景使用,如 mm_tpl)
|
|
252
|
+
* @param {string} level - 日志级别
|
|
253
|
+
* @param {string} message - 日志消息
|
|
254
|
+
* @param {...any} args - 附加参数
|
|
187
255
|
* @private
|
|
188
256
|
*/
|
|
189
|
-
Lang.prototype.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
this._log('error', '当前语言未初始化: ' + this.now);
|
|
193
|
-
return val;
|
|
194
|
-
}
|
|
195
|
-
var arr = key.split('.');
|
|
196
|
-
var obj = lang;
|
|
197
|
-
for (var i = 0; i < arr.length; i++) {
|
|
198
|
-
var k = arr[i];
|
|
199
|
-
var v = obj[k];
|
|
200
|
-
if (i === arr.length - 1) {
|
|
201
|
-
obj[k] = val;
|
|
202
|
-
} else if (v === undefined) {
|
|
203
|
-
obj[k] = {};
|
|
204
|
-
obj = obj[k];
|
|
205
|
-
} else if (typeof (v) === 'object' && !Array.isArray(v)) {
|
|
206
|
-
obj = v;
|
|
207
|
-
} else {
|
|
208
|
-
obj[k] = {};
|
|
209
|
-
obj = obj[k];
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return val;
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* 切换语言
|
|
217
|
-
* @param {string} lang - 语言代码
|
|
218
|
-
* @returns {string} 切换后的语言代码
|
|
219
|
-
*/
|
|
220
|
-
Lang.prototype.setLang = function (lang) {
|
|
221
|
-
if (!lang || typeof (lang) !== 'string') {
|
|
222
|
-
throw new TypeError('lang必须是字符串');
|
|
223
|
-
}
|
|
224
|
-
if (isControlKey(lang)) {
|
|
225
|
-
this._log('error', '语言未找到: ' + lang);
|
|
226
|
-
return this.now;
|
|
227
|
-
}
|
|
228
|
-
if (this.config[lang]) {
|
|
229
|
-
this.now = lang;
|
|
230
|
-
return this.now;
|
|
231
|
-
}
|
|
232
|
-
var base_lang = this._getBase(lang);
|
|
233
|
-
if (this.config[base_lang]) {
|
|
234
|
-
this.now = base_lang;
|
|
235
|
-
return this.now;
|
|
257
|
+
Lang.prototype._log = function (level, message, ...args) {
|
|
258
|
+
if (!this.config.silent) {
|
|
259
|
+
(console[level] || console.log).call(console, message, ...args);
|
|
236
260
|
}
|
|
237
|
-
this._log('error', '语言未找到: ' + lang);
|
|
238
|
-
return this.now;
|
|
239
261
|
};
|
|
240
262
|
|
|
241
263
|
/**
|
|
242
|
-
*
|
|
243
|
-
* @param {string}
|
|
244
|
-
* @
|
|
264
|
+
* 获取翻译文本
|
|
265
|
+
* @param {string} key - 键,支持 a.b 形式的层级键
|
|
266
|
+
* @param {...any} param - 参数,依次替换 {0}、{1} 等占位符
|
|
267
|
+
* @returns {string} 翻译后的文本
|
|
245
268
|
*/
|
|
246
|
-
Lang.prototype.
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
delete this.config[lang];
|
|
252
|
-
return true;
|
|
269
|
+
Lang.prototype.get = function (key, ...param) {
|
|
270
|
+
checkKey(key, 'key');
|
|
271
|
+
var val = this._getVal(key);
|
|
272
|
+
if (typeof val === 'string' && param.length > 0) {
|
|
273
|
+
val = this._fill(val, 0, param, false);
|
|
253
274
|
}
|
|
254
|
-
return
|
|
275
|
+
return val;
|
|
255
276
|
};
|
|
256
277
|
|
|
257
278
|
/**
|
|
@@ -273,80 +294,120 @@ Lang.prototype.t = function (key, ...param) {
|
|
|
273
294
|
* @returns {string} 翻译后的文本
|
|
274
295
|
*/
|
|
275
296
|
Lang.prototype.tc = function (key, count, ...param) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
if (typeof (count) !== 'number') {
|
|
297
|
+
checkKey(key, 'key');
|
|
298
|
+
if (typeof count !== 'number') {
|
|
280
299
|
throw new TypeError('count必须是数字');
|
|
281
300
|
}
|
|
282
|
-
var
|
|
283
|
-
var
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
var val;
|
|
287
|
-
if (typeof (obj) === 'object' && obj !== null) {
|
|
288
|
-
val = obj[idx];
|
|
301
|
+
var rule = this.rules[this.now] || this.rules[this._getBase(this.now)] || this.rules.en;
|
|
302
|
+
var val = this._getVal(key);
|
|
303
|
+
if (val !== null && typeof val === 'object') {
|
|
304
|
+
val = val[rule(count)];
|
|
289
305
|
}
|
|
290
|
-
if (typeof
|
|
291
|
-
val =
|
|
292
|
-
for (var i = 0; i < param.length; i++) {
|
|
293
|
-
var reg = new RegExp('\\{' + (i + 1) + '\\}', 'ig');
|
|
294
|
-
val = val.replace(reg, param[i]);
|
|
295
|
-
}
|
|
306
|
+
if (typeof val === 'string') {
|
|
307
|
+
val = this._fill(val, count, param, true);
|
|
296
308
|
}
|
|
297
309
|
return val;
|
|
298
310
|
};
|
|
299
311
|
|
|
300
312
|
/**
|
|
301
|
-
*
|
|
302
|
-
* @param {
|
|
303
|
-
* @param {
|
|
304
|
-
* @returns {
|
|
313
|
+
* 设置语言键值到当前语言
|
|
314
|
+
* @param {string} key - 键名,支持 a.b 形式的层级键
|
|
315
|
+
* @param {any} val - 值
|
|
316
|
+
* @returns {any} 返回设置的值
|
|
305
317
|
*/
|
|
306
|
-
Lang.prototype.
|
|
307
|
-
|
|
308
|
-
|
|
318
|
+
Lang.prototype.set = function (key, val) {
|
|
319
|
+
checkKey(key, 'key');
|
|
320
|
+
this.config[this.now + '.' + key] = val;
|
|
321
|
+
return val;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* 切换语言
|
|
326
|
+
* 扁平结构下语言是否存在以是否有该语言前缀的词条为准
|
|
327
|
+
* @param {string} lang - 语言代码
|
|
328
|
+
* @returns {string} 切换后的语言代码
|
|
329
|
+
*/
|
|
330
|
+
Lang.prototype.setLang = function (lang) {
|
|
331
|
+
checkKey(lang, 'lang');
|
|
332
|
+
var base = this._getBase(lang);
|
|
333
|
+
var target = this.hasLang(lang) ? lang
|
|
334
|
+
: base !== lang && this.hasLang(base) ? base : '';
|
|
335
|
+
if (isControlKey(lang) || !target) {
|
|
336
|
+
this._log('error', '语言未找到: ' + lang);
|
|
337
|
+
return this.now;
|
|
309
338
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
339
|
+
this.now = target;
|
|
340
|
+
return target;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* 判断某语言是否存在词条
|
|
345
|
+
* @param {string} lang - 语言代码
|
|
346
|
+
* @returns {boolean} 存在该语言的词条时返回 true
|
|
347
|
+
*/
|
|
348
|
+
Lang.prototype.hasLang = function (lang) {
|
|
349
|
+
return !!lang && typeof lang === 'string' && this._keysOf(lang).length > 0;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* 删除某语言的全部词条
|
|
354
|
+
* @param {string} lang - 语言代码
|
|
355
|
+
* @returns {boolean} 是否删除成功
|
|
356
|
+
*/
|
|
357
|
+
Lang.prototype.del = function (lang) {
|
|
358
|
+
checkKey(lang, 'lang');
|
|
359
|
+
var keys = this._keysOf(lang);
|
|
360
|
+
for (var i = 0; i < keys.length; i++) {
|
|
361
|
+
delete this.config[keys[i]];
|
|
314
362
|
}
|
|
315
|
-
|
|
316
|
-
var month = String(d.getMonth() + 1).padStart(2, '0');
|
|
317
|
-
var day = String(d.getDate()).padStart(2, '0');
|
|
318
|
-
var hours = String(d.getHours()).padStart(2, '0');
|
|
319
|
-
var minutes = String(d.getMinutes()).padStart(2, '0');
|
|
320
|
-
var seconds = String(d.getSeconds()).padStart(2, '0');
|
|
321
|
-
return fmt
|
|
322
|
-
.replace('YYYY', year)
|
|
323
|
-
.replace('yyyy', year)
|
|
324
|
-
.replace('MM', month)
|
|
325
|
-
.replace('DD', day)
|
|
326
|
-
.replace('dd', day)
|
|
327
|
-
.replace('HH', hours)
|
|
328
|
-
.replace('hh', hours)
|
|
329
|
-
.replace('mm', minutes)
|
|
330
|
-
.replace('ss', seconds);
|
|
363
|
+
return keys.length > 0;
|
|
331
364
|
};
|
|
332
365
|
|
|
333
366
|
/**
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
* @
|
|
337
|
-
* @returns {string} 格式化后的数字字符串
|
|
367
|
+
* 获取支持的语言列表
|
|
368
|
+
* 取词条键的首段去重,无词条的语言不会出现(空语言字典天然不可见)
|
|
369
|
+
* @returns {Array<string>} 语言代码数组
|
|
338
370
|
*/
|
|
339
|
-
Lang.prototype.
|
|
340
|
-
|
|
341
|
-
|
|
371
|
+
Lang.prototype.getLangs = function () {
|
|
372
|
+
var langs = [];
|
|
373
|
+
for (var key in this.config) {
|
|
374
|
+
var dot = key.indexOf('.');
|
|
375
|
+
if (dot === -1) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
var lang = key.slice(0, dot);
|
|
379
|
+
if (langs.indexOf(lang) === -1) {
|
|
380
|
+
langs.push(lang);
|
|
381
|
+
}
|
|
342
382
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
383
|
+
return langs;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* 导出某语言的全部词条
|
|
388
|
+
* 扁平词条按前缀取出并还原为嵌套字典,形态与语言包文件一致
|
|
389
|
+
* @param {string} lang - 语言代码
|
|
390
|
+
* @returns {Object|null} 嵌套词条字典,该语言无词条时返回 null
|
|
391
|
+
*/
|
|
392
|
+
Lang.prototype.getDict = function (lang) {
|
|
393
|
+
var prefix = lang && typeof lang === 'string' ? lang + '.' : '';
|
|
394
|
+
var keys = prefix ? this._keysOf(lang) : [];
|
|
395
|
+
var dict = null;
|
|
396
|
+
for (var i = 0; i < keys.length; i++) {
|
|
397
|
+
dict = dict || {};
|
|
398
|
+
setByPath(dict, keys[i].slice(prefix.length), this.config[keys[i]]);
|
|
399
|
+
}
|
|
400
|
+
return dict;
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* 检查语言是否支持
|
|
405
|
+
* @param {string} lang - 语言代码
|
|
406
|
+
* @returns {boolean} 是否支持
|
|
407
|
+
*/
|
|
408
|
+
Lang.prototype.isSupported = function (lang) {
|
|
409
|
+
return !!lang && typeof lang === 'string'
|
|
410
|
+
&& (this.hasLang(lang) || this.hasLang(this._getBase(lang)));
|
|
350
411
|
};
|
|
351
412
|
|
|
352
413
|
/**
|
|
@@ -356,10 +417,8 @@ Lang.prototype.formatNumber = function (num, options) {
|
|
|
356
417
|
* @returns {boolean} 是否注册成功
|
|
357
418
|
*/
|
|
358
419
|
Lang.prototype.registerPlural = function (lang, rule) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
if (typeof (rule) !== 'function') {
|
|
420
|
+
checkKey(lang, 'lang');
|
|
421
|
+
if (typeof rule !== 'function') {
|
|
363
422
|
throw new TypeError('rule必须是函数');
|
|
364
423
|
}
|
|
365
424
|
this.rules[lang] = rule;
|
|
@@ -367,62 +426,76 @@ Lang.prototype.registerPlural = function (lang, rule) {
|
|
|
367
426
|
};
|
|
368
427
|
|
|
369
428
|
/**
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
* @
|
|
429
|
+
* 格式化日期
|
|
430
|
+
* @param {Date|number} dt - 日期对象或时间戳
|
|
431
|
+
* @param {string} format - 格式,默认 YYYY-MM-DD hh:mm:ss
|
|
432
|
+
* @returns {string} 格式化后的日期字符串
|
|
373
433
|
*/
|
|
374
|
-
Lang.prototype.
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (!isControlKey(key) && typeof (this.config[key]) === 'object') {
|
|
378
|
-
langs.push(key);
|
|
379
|
-
}
|
|
434
|
+
Lang.prototype.formatDate = function (dt, format) {
|
|
435
|
+
if (!dt) {
|
|
436
|
+
throw new TypeError('date不能为空');
|
|
380
437
|
}
|
|
381
|
-
|
|
438
|
+
var d = new Date(dt);
|
|
439
|
+
if (isNaN(d.getTime())) {
|
|
440
|
+
throw new TypeError('无效的日期');
|
|
441
|
+
}
|
|
442
|
+
var year = d.getFullYear();
|
|
443
|
+
var map = {
|
|
444
|
+
YYYY: year,
|
|
445
|
+
yyyy: year,
|
|
446
|
+
MM: pad(d.getMonth() + 1),
|
|
447
|
+
DD: pad(d.getDate()),
|
|
448
|
+
dd: pad(d.getDate()),
|
|
449
|
+
HH: pad(d.getHours()),
|
|
450
|
+
hh: pad(d.getHours()),
|
|
451
|
+
mm: pad(d.getMinutes()),
|
|
452
|
+
ss: pad(d.getSeconds())
|
|
453
|
+
};
|
|
454
|
+
var fmt = format || 'YYYY-MM-DD hh:mm:ss';
|
|
455
|
+
for (var token in map) {
|
|
456
|
+
fmt = fmt.replace(token, map[token]);
|
|
457
|
+
}
|
|
458
|
+
return fmt;
|
|
382
459
|
};
|
|
383
460
|
|
|
384
461
|
/**
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
* @param {
|
|
388
|
-
* @returns {
|
|
389
|
-
* @private
|
|
462
|
+
* 格式化数字
|
|
463
|
+
* @param {number} num - 要格式化的数字
|
|
464
|
+
* @param {Object} options - 选项,含 decimals、decimal_separator、thousands_separator
|
|
465
|
+
* @returns {string} 格式化后的数字字符串
|
|
390
466
|
*/
|
|
391
|
-
Lang.prototype.
|
|
392
|
-
if (
|
|
393
|
-
|
|
394
|
-
this.store.config.file = file;
|
|
395
|
-
}
|
|
396
|
-
return this.store;
|
|
397
|
-
}
|
|
398
|
-
var target = file || this.file;
|
|
399
|
-
if (target) {
|
|
400
|
-
this.store = createStore(null, target);
|
|
401
|
-
this.file = target;
|
|
402
|
-
return this.store;
|
|
467
|
+
Lang.prototype.formatNumber = function (num, options) {
|
|
468
|
+
if (typeof num !== 'number' || isNaN(num)) {
|
|
469
|
+
throw new TypeError('num必须是有效的数字');
|
|
403
470
|
}
|
|
404
|
-
|
|
471
|
+
var opt = options || {};
|
|
472
|
+
var decimals = typeof opt.decimals === 'number' ? opt.decimals : 2;
|
|
473
|
+
var parts = num.toFixed(decimals).split('.');
|
|
474
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, opt.thousands_separator || ',');
|
|
475
|
+
return parts.join(opt.decimal_separator || '.');
|
|
405
476
|
};
|
|
406
477
|
|
|
407
478
|
/**
|
|
408
479
|
* 保存语言包到持久化文件
|
|
409
|
-
*
|
|
410
|
-
* @param {string} [file] 持久化文件路径,缺省时用实例的 file 配置
|
|
480
|
+
* 内存词条表即扁平 string→string 形态(数组值按标记编码),直接写入存储实例落盘
|
|
411
481
|
* @returns {boolean} 是否保存成功
|
|
412
482
|
*/
|
|
413
|
-
Lang.prototype.save = function (
|
|
414
|
-
|
|
415
|
-
if (!store) {
|
|
483
|
+
Lang.prototype.save = function () {
|
|
484
|
+
if (!this.store) {
|
|
416
485
|
this._log('error', '未配置持久化文件路径或存储实例,无法保存语言包');
|
|
417
486
|
return false;
|
|
418
487
|
}
|
|
419
|
-
var flat = flatten(this.config);
|
|
420
488
|
try {
|
|
421
|
-
store.clear();
|
|
422
|
-
for (var key in
|
|
423
|
-
|
|
489
|
+
this.store.clear();
|
|
490
|
+
for (var key in this.config) {
|
|
491
|
+
if (!isControlKey(key)) {
|
|
492
|
+
var str = encodeValue(this.config[key]);
|
|
493
|
+
if (str !== null) {
|
|
494
|
+
this.store.set(key, str);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
424
497
|
}
|
|
425
|
-
return store.save() !== false;
|
|
498
|
+
return this.store.save() !== false;
|
|
426
499
|
} catch (e) {
|
|
427
500
|
this._log('error', '语言包保存失败: ' + e.message);
|
|
428
501
|
return false;
|
|
@@ -431,58 +504,27 @@ Lang.prototype.save = function (file) {
|
|
|
431
504
|
|
|
432
505
|
/**
|
|
433
506
|
* 从持久化文件加载语言包
|
|
434
|
-
*
|
|
435
|
-
* @param {string} [file] 持久化文件路径,缺省时用实例的 file 配置
|
|
507
|
+
* 存储中的扁平键值与内存结构一致,直接合并进词条表(文件中的词条优先),数组编码值还原
|
|
436
508
|
* @returns {boolean} 是否加载成功
|
|
437
509
|
*/
|
|
438
|
-
Lang.prototype.load = function (
|
|
439
|
-
|
|
440
|
-
if (!store || typeof (store.load) !== 'function') {
|
|
510
|
+
Lang.prototype.load = function () {
|
|
511
|
+
if (!this.store) {
|
|
441
512
|
this._log('error', '未配置持久化文件路径或存储实例,无法加载语言包');
|
|
442
513
|
return false;
|
|
443
514
|
}
|
|
444
|
-
var ok = false;
|
|
445
515
|
try {
|
|
446
|
-
|
|
516
|
+
if (!this.store.load()) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
447
519
|
} catch (e) {
|
|
448
520
|
this._log('error', '语言包加载失败: ' + e.message);
|
|
449
521
|
return false;
|
|
450
522
|
}
|
|
451
|
-
|
|
452
|
-
|
|
523
|
+
var list = this.store.list();
|
|
524
|
+
for (var key in list) {
|
|
525
|
+
this.config[key] = decodeValue(list[key]);
|
|
453
526
|
}
|
|
454
|
-
assignDeep(this.config, unflatten(store.list()));
|
|
455
527
|
return true;
|
|
456
528
|
};
|
|
457
529
|
|
|
458
|
-
/**
|
|
459
|
-
* 检查语言是否支持
|
|
460
|
-
* @param {string} lang - 语言代码
|
|
461
|
-
* @returns {boolean} 是否支持
|
|
462
|
-
*/
|
|
463
|
-
Lang.prototype.isSupported = function (lang) {
|
|
464
|
-
if (!lang || typeof (lang) !== 'string' || isControlKey(lang)) {
|
|
465
|
-
return false;
|
|
466
|
-
}
|
|
467
|
-
return !!this.config[lang] || !!this.config[this._getBase(lang)];
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* 输出日志 - 内部方法,使用 console
|
|
472
|
-
* 配置 silent 为 true 时静默(供上层自带日志体系的场景使用,如 mm_tpl)
|
|
473
|
-
* @param {string} level - 日志级别
|
|
474
|
-
* @param {string} message - 日志消息
|
|
475
|
-
* @param {...any} args - 附加参数
|
|
476
|
-
* @private
|
|
477
|
-
*/
|
|
478
|
-
Lang.prototype._log = function (level, message, ...args) {
|
|
479
|
-
if (this.config.silent) {
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
var fn = console[level] || console.log;
|
|
483
|
-
if (typeof (fn) === 'function') {
|
|
484
|
-
fn.call(console, message, ...args);
|
|
485
|
-
}
|
|
486
|
-
};
|
|
487
|
-
|
|
488
530
|
module.exports = Lang;
|