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/langs.js
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
const KvStore = require('mm_kv_store').KvStore;
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 语言存储类,键和值都只能是字符串
|
|
5
|
-
* 由原 mm_langs 模块融合而来,作为多语言模块的数据侧:只负责键值管理与持久化,
|
|
6
|
-
*
|
|
7
|
-
* @class
|
|
8
|
-
* @augments {KvStore}
|
|
9
|
-
*/
|
|
10
|
-
class Langs extends KvStore {
|
|
11
|
-
/**
|
|
12
|
-
* 获取存储项名称,用于覆盖冲突提示
|
|
13
|
-
* @returns {string} 存储项名称
|
|
14
|
-
*/
|
|
15
|
-
_getName() {
|
|
16
|
-
return 'langs';
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* 校验值是否为字符串
|
|
21
|
-
* @param {*} value 值
|
|
22
|
-
* @returns {void}
|
|
23
|
-
* @throws {TypeError} 值不是字符串时抛出
|
|
24
|
-
*/
|
|
25
|
-
_checkValue(value) {
|
|
26
|
-
if (typeof value !== 'string') {
|
|
27
|
-
throw new TypeError('langs 的值必须是字符串');
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
module.exports = Langs;
|
|
1
|
+
const KvStore = require('mm_kv_store').KvStore;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 语言存储类,键和值都只能是字符串
|
|
5
|
+
* 由原 mm_langs 模块融合而来,作为多语言模块的数据侧:只负责键值管理与持久化,
|
|
6
|
+
* 不感知语言词条语义(Lang 侧内存词条同为扁平 string→string 形态,两侧无需结构转换)
|
|
7
|
+
* @class
|
|
8
|
+
* @augments {KvStore}
|
|
9
|
+
*/
|
|
10
|
+
class Langs extends KvStore {
|
|
11
|
+
/**
|
|
12
|
+
* 获取存储项名称,用于覆盖冲突提示
|
|
13
|
+
* @returns {string} 存储项名称
|
|
14
|
+
*/
|
|
15
|
+
_getName() {
|
|
16
|
+
return 'langs';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 校验值是否为字符串
|
|
21
|
+
* @param {*} value 值
|
|
22
|
+
* @returns {void}
|
|
23
|
+
* @throws {TypeError} 值不是字符串时抛出
|
|
24
|
+
*/
|
|
25
|
+
_checkValue(value) {
|
|
26
|
+
if (typeof value !== 'string') {
|
|
27
|
+
throw new TypeError('langs 的值必须是字符串');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = Langs;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const { reg } = require('mm_admin');
|
|
2
|
+
const Langs = require('./langs.js');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 语言存储实例管理器(数据侧)
|
|
6
|
+
* 独立成模块以便 lib/lang.js 引用而不产生循环依赖
|
|
7
|
+
* Lang 配置 file 时以文件路径为作用域键从管理器取实例,相同文件的 Lang 复用同一存储
|
|
8
|
+
* @type {Admin}
|
|
9
|
+
*/
|
|
10
|
+
const langs_admin = reg('lang.langs');
|
|
11
|
+
langs_admin.setClass(Langs);
|
|
12
|
+
|
|
13
|
+
module.exports = langs_admin;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
/** 模块能力层:懒加载工具实现,避免模块入口被 require 时加载全部实现 */
|
|
5
|
+
module.exports = new Proxy({}, {
|
|
6
|
+
get(_t, key) {
|
|
7
|
+
if (typeof key !== 'string') return undefined;
|
|
8
|
+
if (!fs.existsSync(path.join(__dirname, key, 'index.js'))) return undefined;
|
|
9
|
+
return require('./' + key);
|
|
10
|
+
},
|
|
11
|
+
has(_t, key) {
|
|
12
|
+
return typeof key === 'string' && fs.existsSync(path.join(__dirname, key, 'index.js'));
|
|
13
|
+
},
|
|
14
|
+
ownKeys() { return fs.readdirSync(__dirname, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name); },
|
|
15
|
+
getOwnPropertyDescriptor(_t, key) { return this.has(_t, key) ? { enumerable: true, configurable: true } : undefined; }
|
|
16
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* lang —— 多语言词条工具
|
|
4
|
+
* 复用模块 Lang 实例:层级键解析、回退链与格式化都由模块维护。
|
|
5
|
+
*/
|
|
6
|
+
const { langAdmin } = require('../../../index.js');
|
|
7
|
+
|
|
8
|
+
async function main(params, ctx, options) {
|
|
9
|
+
void options;
|
|
10
|
+
const p = params || {};
|
|
11
|
+
const action = p.action || 't';
|
|
12
|
+
const done = (data) => { ctx.output = data; return data; };
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const lang = langAdmin(p.scope || 'os');
|
|
16
|
+
|
|
17
|
+
if (action === 't') {
|
|
18
|
+
if (!p.key) throw new TypeError('key 不能为空');
|
|
19
|
+
const text = lang.t(p.key, p.params || {}, p.lang);
|
|
20
|
+
return done({ code: 0, msg: '成功', key: p.key, text: String(text) });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (action === 'get') {
|
|
24
|
+
return done({ code: 0, msg: '成功', lang: p.lang || '', value: lang.get(p.lang || undefined) });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (action === 'set') {
|
|
28
|
+
if (!p.key) throw new TypeError('key 不能为空');
|
|
29
|
+
if (typeof p.value !== 'string') throw new TypeError('value 必须是字符串');
|
|
30
|
+
lang.set(p.key, p.value, p.lang);
|
|
31
|
+
if (typeof lang.save === 'function') lang.save();
|
|
32
|
+
return done({ code: 0, msg: '写入成功', key: p.key });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (action === 'del') {
|
|
36
|
+
if (!p.key) throw new TypeError('key 不能为空');
|
|
37
|
+
const deleted = lang.del(p.key, p.lang);
|
|
38
|
+
if (typeof lang.save === 'function') lang.save();
|
|
39
|
+
return done({ code: 0, msg: deleted ? '删除成功' : '词条不存在', key: p.key, deleted: !!deleted });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (action === 'langs') {
|
|
43
|
+
const langs = lang.getLangs();
|
|
44
|
+
const list = Array.isArray(langs) ? langs : Object.keys(langs || {});
|
|
45
|
+
return done({ code: 0, msg: '成功', count: list.length, langs: list });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (action === 'save') {
|
|
49
|
+
lang.save();
|
|
50
|
+
return done({ code: 0, msg: '已落盘', ok: true });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return done({ code: 10001, msg: '不支持的操作: ' + action });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return done({ code: 10000, msg: '执行失败:' + error.message, error: { name: error.name, message: error.message } });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { main };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mm_lang",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "超级美眉多语言模块:融合 i18n 行为(Lang)与语言存储持久化(Langs),支持层级键、回退链、复数、格式化与 JSON 文件落盘",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"多语言",
|
|
19
19
|
"语言包"
|
|
20
20
|
],
|
|
21
|
-
"layer": "
|
|
22
|
-
"group": "
|
|
21
|
+
"layer": "infrastructure",
|
|
22
|
+
"group": "state",
|
|
23
23
|
"author": "邱文武",
|
|
24
24
|
"license": "ISC",
|
|
25
25
|
"scripts": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"example": "node example/index.js"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"
|
|
30
|
+
"mm_admin": "^1.1.0",
|
|
31
31
|
"mm_kv_store": "^1.0.1"
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"lib",
|
|
36
36
|
"example",
|
|
37
37
|
"README.md",
|
|
38
|
-
"package.json"
|
|
38
|
+
"package.json",
|
|
39
|
+
"tool"
|
|
39
40
|
]
|
|
40
41
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lang",
|
|
3
|
+
"title": "多语言词条",
|
|
4
|
+
"description": "基于 mm_lang 的多语言能力:按语言取词条(支持层级键与格式化)、写入/删除词条、列出支持语言、落盘。适用场景:生成多语言文案、补词条、核对某语言是否支持。工作流提示:t 按 key 取词,key 用层级点号(如 common.save);params 用于 {name} 之类占位。",
|
|
5
|
+
"category": "多语言",
|
|
6
|
+
"group": "i18n",
|
|
7
|
+
"type": "function",
|
|
8
|
+
"main": "./index.js",
|
|
9
|
+
"params": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"action": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "操作类型: t(取词条)/get(读整表)/set(写词条)/del(删词条)/langs(支持语言)/save(落盘)",
|
|
15
|
+
"enum": [
|
|
16
|
+
"t",
|
|
17
|
+
"get",
|
|
18
|
+
"set",
|
|
19
|
+
"del",
|
|
20
|
+
"langs",
|
|
21
|
+
"save"
|
|
22
|
+
],
|
|
23
|
+
"default": "t"
|
|
24
|
+
},
|
|
25
|
+
"key": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"description": "词条键(get/del/t 必填,支持 a.b.c 层级)"
|
|
28
|
+
},
|
|
29
|
+
"value": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"description": "词条文案(set 必填)"
|
|
32
|
+
},
|
|
33
|
+
"lang": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"description": "语言标识,缺省取实例默认语言"
|
|
36
|
+
},
|
|
37
|
+
"params": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"description": "占位参数(t 可选)"
|
|
40
|
+
},
|
|
41
|
+
"scope": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"description": "实例作用域,缺省 os",
|
|
44
|
+
"default": "os"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"required": [
|
|
48
|
+
"action"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"returns": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"properties": {
|
|
54
|
+
"code": {
|
|
55
|
+
"type": "number",
|
|
56
|
+
"description": "0 表示成功,1xxxx 表示业务/执行异常"
|
|
57
|
+
},
|
|
58
|
+
"msg": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "结果描述"
|
|
61
|
+
},
|
|
62
|
+
"text": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"description": "词条文案(t)"
|
|
65
|
+
},
|
|
66
|
+
"value": {
|
|
67
|
+
"description": "整表或单条值(get)"
|
|
68
|
+
},
|
|
69
|
+
"langs": {
|
|
70
|
+
"type": "array",
|
|
71
|
+
"description": "支持语言列表(langs)"
|
|
72
|
+
},
|
|
73
|
+
"count": {
|
|
74
|
+
"type": "number",
|
|
75
|
+
"description": "数量"
|
|
76
|
+
},
|
|
77
|
+
"error": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"description": "异常对象 { name, message }"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"handler": "manager"
|
|
84
|
+
}
|
package/lib/store.js
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 语言词条持久化适配层
|
|
3
|
-
* @description 承担"动作侧适配"职责:把 Lang 的嵌套词条字典与 Langs 的
|
|
4
|
-
* string→string 扁平键值互相转换。文件读写、键值校验等存储细节全部由 Langs 负责,
|
|
5
|
-
* 本文件不触碰 fs。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const Langs = require('./langs');
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* 非词条的控制键,不参与扁平化与持久化
|
|
12
|
-
* @type {Array<string>}
|
|
13
|
-
*/
|
|
14
|
-
const CONTROL_KEYS = ['default', 'fallback', 'locale', 'options', 'file', 'store', 'silent'];
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* 非字符串词条(复数数组等)的标记前缀
|
|
18
|
-
* 落盘后仍是字符串,满足 Langs 的 string→string 约束;读取时按前缀还原
|
|
19
|
-
* @type {string}
|
|
20
|
-
*/
|
|
21
|
-
const MARK = '\u0001';
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* 判断是否为控制键
|
|
25
|
-
* @param {string} key 键名
|
|
26
|
-
* @returns {boolean} 是控制键返回 true
|
|
27
|
-
*/
|
|
28
|
-
function isControlKey(key) {
|
|
29
|
-
return CONTROL_KEYS.indexOf(key) !== -1;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* 将词条值序列化为存储字符串
|
|
34
|
-
* @param {*} val 词条值
|
|
35
|
-
* @returns {string|null} 存储字符串,不可序列化的值返回 null
|
|
36
|
-
*/
|
|
37
|
-
function encodeValue(val) {
|
|
38
|
-
if (typeof (val) === 'string') {
|
|
39
|
-
return val;
|
|
40
|
-
}
|
|
41
|
-
if (Array.isArray(val)) {
|
|
42
|
-
return MARK + JSON.stringify(val);
|
|
43
|
-
}
|
|
44
|
-
if (typeof (val) === 'number' || typeof (val) === 'boolean') {
|
|
45
|
-
return String(val);
|
|
46
|
-
}
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* 将存储字符串还原为词条值
|
|
52
|
-
* @param {string} val 存储字符串
|
|
53
|
-
* @returns {*} 词条值
|
|
54
|
-
*/
|
|
55
|
-
function decodeValue(val) {
|
|
56
|
-
if (typeof (val) !== 'string') {
|
|
57
|
-
return val;
|
|
58
|
-
}
|
|
59
|
-
if (val.charAt(0) === MARK) {
|
|
60
|
-
try {
|
|
61
|
-
return JSON.parse(val.slice(1));
|
|
62
|
-
} catch (e) {
|
|
63
|
-
return val;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return val;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* 嵌套词条字典扁平化为 string→string 键值
|
|
71
|
-
* 键形如 `语言代码.层级键`,复数数组等非字符串值按 MARK 前缀编码
|
|
72
|
-
* @param {Object} dict 嵌套词条字典
|
|
73
|
-
* @param {string} [prefix] 键前缀
|
|
74
|
-
* @param {Object} [out] 输出容器
|
|
75
|
-
* @returns {Object} 扁平键值表
|
|
76
|
-
*/
|
|
77
|
-
function flatten(dict, prefix, out) {
|
|
78
|
-
var flat = out || {};
|
|
79
|
-
if (!dict || typeof (dict) !== 'object') {
|
|
80
|
-
return flat;
|
|
81
|
-
}
|
|
82
|
-
for (var key in dict) {
|
|
83
|
-
if (!Object.prototype.hasOwnProperty.call(dict, key)) {
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
if (!prefix && isControlKey(key)) {
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
var val = dict[key];
|
|
90
|
-
var path = prefix ? prefix + '.' + key : key;
|
|
91
|
-
if (val && typeof (val) === 'object' && !Array.isArray(val)) {
|
|
92
|
-
flatten(val, path, flat);
|
|
93
|
-
} else {
|
|
94
|
-
var str = encodeValue(val);
|
|
95
|
-
if (str !== null) {
|
|
96
|
-
flat[path] = str;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return flat;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* 按点号路径向对象写入值
|
|
105
|
-
* @param {Object} obj 目标对象
|
|
106
|
-
* @param {string} path 点号路径
|
|
107
|
-
* @param {*} val 值
|
|
108
|
-
* @returns {void}
|
|
109
|
-
*/
|
|
110
|
-
function setByPath(obj, path, val) {
|
|
111
|
-
var keys = path.split('.');
|
|
112
|
-
var cur = obj;
|
|
113
|
-
for (var i = 0; i < keys.length - 1; i++) {
|
|
114
|
-
if (!cur[keys[i]] || typeof (cur[keys[i]]) !== 'object') {
|
|
115
|
-
cur[keys[i]] = {};
|
|
116
|
-
}
|
|
117
|
-
cur = cur[keys[i]];
|
|
118
|
-
}
|
|
119
|
-
cur[keys[keys.length - 1]] = val;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* 扁平键值还原为嵌套词条字典
|
|
124
|
-
* @param {Object} flat 扁平键值表
|
|
125
|
-
* @returns {Object} 嵌套词条字典
|
|
126
|
-
*/
|
|
127
|
-
function unflatten(flat) {
|
|
128
|
-
var dict = {};
|
|
129
|
-
if (!flat || typeof (flat) !== 'object') {
|
|
130
|
-
return dict;
|
|
131
|
-
}
|
|
132
|
-
for (var path in flat) {
|
|
133
|
-
if (!Object.prototype.hasOwnProperty.call(flat, path)) {
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
setByPath(dict, path, decodeValue(flat[path]));
|
|
137
|
-
}
|
|
138
|
-
return dict;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* 深度合并词条字典,来源值覆盖目标值
|
|
143
|
-
* @param {Object} target 目标字典
|
|
144
|
-
* @param {Object} source 来源字典
|
|
145
|
-
* @returns {Object} 合并后的目标字典
|
|
146
|
-
*/
|
|
147
|
-
function assignDeep(target, source) {
|
|
148
|
-
for (var key in source) {
|
|
149
|
-
if (!Object.prototype.hasOwnProperty.call(source, key)) {
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
var val = source[key];
|
|
153
|
-
if (val && typeof (val) === 'object' && !Array.isArray(val)
|
|
154
|
-
&& target[key] && typeof (target[key]) === 'object' && !Array.isArray(target[key])) {
|
|
155
|
-
assignDeep(target[key], val);
|
|
156
|
-
} else {
|
|
157
|
-
target[key] = val;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return target;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* 创建语言存储实例
|
|
165
|
-
* 优先复用外部注入的实例(动作与数据解耦),其次按文件路径新建,都没有则返回 null
|
|
166
|
-
* @param {Langs} [injected] 注入的存储实例
|
|
167
|
-
* @param {string} [file] 持久化文件路径
|
|
168
|
-
* @returns {Langs|null} 存储实例
|
|
169
|
-
*/
|
|
170
|
-
function createStore(injected, file) {
|
|
171
|
-
if (injected && typeof (injected.set) === 'function' && typeof (injected.save) === 'function') {
|
|
172
|
-
return injected;
|
|
173
|
-
}
|
|
174
|
-
if (file) {
|
|
175
|
-
return new Langs({ file: file });
|
|
176
|
-
}
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
module.exports = {
|
|
181
|
-
CONTROL_KEYS,
|
|
182
|
-
MARK,
|
|
183
|
-
isControlKey,
|
|
184
|
-
encodeValue,
|
|
185
|
-
decodeValue,
|
|
186
|
-
flatten,
|
|
187
|
-
unflatten,
|
|
188
|
-
assignDeep,
|
|
189
|
-
createStore
|
|
190
|
-
};
|