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