mm_lang 1.0.1 → 3.0.0
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 +209 -148
- package/example/index.js +7 -15
- package/index.js +48 -24
- package/lib/lang.js +259 -69
- package/lib/langs.js +32 -0
- package/lib/langs_admin.js +13 -0
- package/package.json +24 -6
- package/test.js +0 -18
package/README.md
CHANGED
|
@@ -1,148 +1,209 @@
|
|
|
1
|
-
# mm_lang
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
console.log(lang.get('common.hello', '美眉'));
|
|
46
|
-
|
|
47
|
-
console.log(lang.t('common.hello', 'MM'));
|
|
48
|
-
console.log(lang.tc('apple', 3));
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
|
83
|
-
|
|
|
84
|
-
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
|
91
|
-
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
1
|
+
# mm_lang
|
|
2
|
+
|
|
3
|
+
超级美眉多语言模块。融合了原 `mm_lang`(i18n 行为)与 `mm_langs`(语言存储)两个模块:`Lang` 负责翻译行为,`Langs` 负责键值存储与 JSON 文件持久化,两者组合实现"动作与数据分离"。
|
|
4
|
+
|
|
5
|
+
> **迁移提示**:`mm_langs` 已合并至本模块,其 `Langs` 类与 `langsAdmin` 从 `mm_lang` 直接导出,API 与报错文案保持不变。原 `require('mm_langs')` 改为 `require('mm_lang')` 即可。
|
|
6
|
+
|
|
7
|
+
## 项目信息
|
|
8
|
+
|
|
9
|
+
- **当前版本**: 2.0.0
|
|
10
|
+
- **更新日期**: 2026-10-30
|
|
11
|
+
- **Gitee地址**: https://gitee.com/qiuwenwu91/mm_lang
|
|
12
|
+
- **依赖**: `mm_corejs`(管理器)、`mm_kv_store`(键值存储基类)
|
|
13
|
+
|
|
14
|
+
## 模块结构
|
|
15
|
+
|
|
16
|
+
| 文件 | 职责 |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `lib/lang.js` | 行为侧:扁平词条表(`语言.层级键`→文本)上的取词、回退链、复数、格式化,只接受扁平入参,**不做文件 IO** |
|
|
19
|
+
| `lib/langs.js` | 数据侧:string→string 键值、JSON 文件 save/load、值类型校验,**不感知语言词条语义** |
|
|
20
|
+
| `lib/langs_admin.js` | 数据侧管理器:Langs 实例按作用域(文件路径)复用,供 Lang 与 langsAdmin 共用 |
|
|
21
|
+
|
|
22
|
+
## 特点
|
|
23
|
+
|
|
24
|
+
- **行为侧**:零文件 IO,`zh_cn`/`en` 内置复数规则,`a.b.c` 层级键,回退链(当前语言 → 基础语言 → 默认语言),占位符替换,日期/数字本地化格式化
|
|
25
|
+
- **数据侧**:键和值都只能是字符串,支持覆盖、删除、判断存在、取副本、清空、JSON 文件持久化
|
|
26
|
+
- **组合使用**:`Lang` 配置 `file` 即获得持久化能力;也可注入自定义 `store` 实例替换存储后端
|
|
27
|
+
|
|
28
|
+
## 安装
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install mm_lang
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 快速开始
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
const { Lang, Langs, langAdmin } = require('mm_lang');
|
|
38
|
+
|
|
39
|
+
// 纯内存翻译
|
|
40
|
+
const lang = new Lang({
|
|
41
|
+
zh_cn: { common: { hello: '你好,{0}!' }, apple: ['一个苹果', '{count}个苹果'] },
|
|
42
|
+
en: { common: { hello: 'Hello, {0}!' }, apple: ['one apple', '{count} apples'] },
|
|
43
|
+
default: 'zh_cn'
|
|
44
|
+
});
|
|
45
|
+
console.log(lang.get('common.hello', '美眉')); // 你好,美眉!
|
|
46
|
+
lang.setLang('en');
|
|
47
|
+
console.log(lang.t('common.hello', 'MM')); // Hello, MM!
|
|
48
|
+
console.log(lang.tc('apple', 3)); // 3 apples
|
|
49
|
+
|
|
50
|
+
// 带持久化:配置 file 后自动创建 Langs 存储实例
|
|
51
|
+
const persisted = langAdmin('shop', { file: './data/lang.json', default: 'zh_cn' });
|
|
52
|
+
persisted.load(); // 启动时从文件恢复词条
|
|
53
|
+
persisted.set('nav.home', '首页');
|
|
54
|
+
persisted.save(); // 内存词条表即扁平形态,直接落盘(相同 file 复用同一存储实例)
|
|
55
|
+
|
|
56
|
+
// 只用存储(原 mm_langs 用法)
|
|
57
|
+
const langs = new Langs({ file: './data/map.json' });
|
|
58
|
+
langs.set('en', 'english').save();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## API 说明
|
|
62
|
+
|
|
63
|
+
### 行为侧 `Lang`
|
|
64
|
+
|
|
65
|
+
#### 构造 `new Lang(config)`
|
|
66
|
+
|
|
67
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
68
|
+
| --- | --- | --- | --- |
|
|
69
|
+
| `zh_cn` | `Object` | `{}` | 简体中文词条字典 |
|
|
70
|
+
| `en` | `Object` | `{}` | 英文词条字典 |
|
|
71
|
+
| `default` | `String` | `'zh_cn'` | 默认语言代码,同时作为初始当前语言 |
|
|
72
|
+
| `fallback` | `Boolean` | `true` | 是否启用回退机制 |
|
|
73
|
+
| `locale` | `Object` | `{ zh: 'zh_cn', en: 'en' }` | 完整语言代码到基础语言代码的映射 |
|
|
74
|
+
| `file` | `String` | `''` | 持久化文件路径,配置后自动创建 Langs 存储实例 |
|
|
75
|
+
| `store` | `Langs` | `undefined` | 注入的存储实例(鸭子类型),优先于 `file` |
|
|
76
|
+
| `silent` | `Boolean` | `false` | 是否静默缺失词条/存储异常日志(上层自带日志体系时开启,如 mm_tpl) |
|
|
77
|
+
|
|
78
|
+
实例属性:`config`(词条字典)、`rules`(复数规则)、`now`(当前语言)、`locale`(区域映射)、`file`(持久化路径)、`store`(存储实例或 `null`)。
|
|
79
|
+
|
|
80
|
+
#### 取值与翻译
|
|
81
|
+
|
|
82
|
+
| 方法 | 签名 | 返回值 | 说明 |
|
|
83
|
+
| --- | --- | --- | --- |
|
|
84
|
+
| get | `get(key, ...param)` | `String` | 取翻译文本,支持 `a.b` 层级键;`param` 依次替换 `{0}`、`{1}` 等 |
|
|
85
|
+
| t | `t(key, ...param)` | `String` | `get` 的别名 |
|
|
86
|
+
| tc | `tc(key, count, ...param)` | `String` | 复数形式取词:数组词条按当前语言规则取索引,先替换 `{count}`,再替换 `{1}`、`{2}` 等 |
|
|
87
|
+
|
|
88
|
+
#### 语言管理
|
|
89
|
+
|
|
90
|
+
| 方法 | 签名 | 返回值 | 说明 |
|
|
91
|
+
| --- | --- | --- | --- |
|
|
92
|
+
| setLang | `setLang(lang)` | `String` | 切换语言;找不到时先尝试基础语言映射,仍失败记日志并保持当前语言 |
|
|
93
|
+
| getLangs | `getLangs()` | `Array<String>` | 支持的语言列表(取词条键首段去重,无词条的语言不会出现) |
|
|
94
|
+
| isSupported | `isSupported(lang)` | `Boolean` | 语言是否支持(含基础语言映射,控制键恒为 false) |
|
|
95
|
+
| del | `del(lang)` | `Boolean` | 删除指定语言的全部词条 |
|
|
96
|
+
| hasLang | `hasLang(lang)` | `Boolean` | 某语言是否存在词条 |
|
|
97
|
+
| getDict | `getDict(lang)` | `Object|null` | 导出某语言词条为嵌套字典(形态与语言包文件一致),无词条返回 `null` |
|
|
98
|
+
| set | `set(key, val)` | `any` | 向当前语言写入词条,支持 `a.b` 层级键 |
|
|
99
|
+
| registerPlural | `registerPlural(lang, rule)` | `Boolean` | 注册复数规则,`rule(count)` 返回形式索引 |
|
|
100
|
+
|
|
101
|
+
#### 持久化
|
|
102
|
+
|
|
103
|
+
| 方法 | 签名 | 返回值 | 说明 |
|
|
104
|
+
| --- | --- | --- | --- |
|
|
105
|
+
| save | `save(file?)` | `Boolean` | 词条扁平化后经存储实例落盘;无存储实例返回 `false`(不抛异常) |
|
|
106
|
+
| load | `load(file?)` | `Boolean` | 从存储实例读取扁平键值直接合并进 `config`(结构一致无需转换),文件中的词条优先;失败返回 `false` 且不改动内存词条 |
|
|
107
|
+
|
|
108
|
+
落盘格式为 `语言代码.层级键 → 文本` 的 string→string 键值,例如:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"zh_cn.nav.home": "首页",
|
|
113
|
+
"zh_cn.apple": "\u0001[\"一个苹果\",\"{count}个苹果\"]"
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
> 复数等数组词条以 `\u0001` 前缀 + JSON 文本编码,仍满足存储的 string→string 约束,加载时自动还原为数组。`default`、`fallback`、`locale` 等控制键不参与持久化。
|
|
118
|
+
|
|
119
|
+
#### 格式化
|
|
120
|
+
|
|
121
|
+
| 方法 | 签名 | 返回值 | 说明 |
|
|
122
|
+
| --- | --- | --- | --- |
|
|
123
|
+
| formatDate | `formatDate(dt, format)` | `String` | 默认格式 `YYYY-MM-DD hh:mm:ss`,支持 `YYYY/yyyy`、`MM`、`DD/dd`、`HH/hh`、`mm`、`ss` |
|
|
124
|
+
| formatNumber | `formatNumber(num, options)` | `String` | `options` 支持 `decimals`(默认 2)、`decimal_separator`、`thousands_separator` |
|
|
125
|
+
|
|
126
|
+
### 数据侧 `Langs`
|
|
127
|
+
|
|
128
|
+
| 方法 | 说明 | 参数 | 返回值 |
|
|
129
|
+
|---|---|---|---|
|
|
130
|
+
| set | 设置键值(值必须为字符串) | key, value | Langs |
|
|
131
|
+
| get | 获取值 | key | string\|undefined |
|
|
132
|
+
| del | 删除键 | key | boolean |
|
|
133
|
+
| has | 判断键是否存在 | key | boolean |
|
|
134
|
+
| list | 获取键值表副本 | - | object |
|
|
135
|
+
| clear | 清空所有键 | - | Langs |
|
|
136
|
+
| save | 保存到持久化文件 | - | boolean |
|
|
137
|
+
| load | 从持久化文件加载(逐项校验值类型) | - | boolean |
|
|
138
|
+
|
|
139
|
+
### 管理器
|
|
140
|
+
|
|
141
|
+
| 方法 | 说明 |
|
|
142
|
+
|---|---|
|
|
143
|
+
| langAdmin(scope, config) | 按作用域获取 `Lang` 实例(相同作用域返回同一实例),缺省 `os` |
|
|
144
|
+
| langsAdmin(scope, config) | 按作用域获取 `Langs` 实例,缺省 `os` |
|
|
145
|
+
| admin / langs_admin | 对应的 `Admin` 实例 |
|
|
146
|
+
|
|
147
|
+
## 使用示例
|
|
148
|
+
|
|
149
|
+
### 场景一:回退机制
|
|
150
|
+
|
|
151
|
+
```javascript
|
|
152
|
+
const { Lang } = require('mm_lang');
|
|
153
|
+
|
|
154
|
+
const lang = new Lang({
|
|
155
|
+
zh_cn: { nav: { home: '首页' } },
|
|
156
|
+
en: { nav: { home: 'Home', about: 'About' } },
|
|
157
|
+
default: 'en'
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
lang.setLang('zh_cn');
|
|
161
|
+
console.log(lang.get('nav.about')); // 中文缺失,回退到默认语言 en → "About"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 场景二:自定义复数规则
|
|
165
|
+
|
|
166
|
+
```javascript
|
|
167
|
+
const { Lang } = require('mm_lang');
|
|
168
|
+
|
|
169
|
+
const lang = new Lang({ ru: { apple: ['яблоко', 'яблока', 'яблок'] }, default: 'ru' });
|
|
170
|
+
lang.registerPlural('ru', function (count) {
|
|
171
|
+
const n10 = count % 10, n100 = count % 100;
|
|
172
|
+
if (n10 === 1 && n100 !== 11) return 0;
|
|
173
|
+
if (n10 >= 2 && n10 <= 4 && (n100 < 12 || n100 > 14)) return 1;
|
|
174
|
+
return 2;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
console.log(lang.tc('apple', 1)); // яблоко
|
|
178
|
+
console.log(lang.tc('apple', 3)); // яблока
|
|
179
|
+
console.log(lang.tc('apple', 11)); // яблок
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 场景三:注入自定义存储
|
|
183
|
+
|
|
184
|
+
```javascript
|
|
185
|
+
const { Lang, Langs } = require('mm_lang');
|
|
186
|
+
|
|
187
|
+
// 任何具备 set/get/list/save/load 的对象都可作为存储
|
|
188
|
+
const store = new Langs({ file: './data/custom.json' });
|
|
189
|
+
const lang = new Lang({ store, zh_cn: { hello: '你好' } });
|
|
190
|
+
lang.save();
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## 目录结构
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
mm_lang/
|
|
197
|
+
├── index.js 入口:导出 Lang / Langs / admin / langs_admin / langAdmin / langsAdmin
|
|
198
|
+
├── lib/
|
|
199
|
+
│ ├── lang.js 行为侧
|
|
200
|
+
│ └── langs.js 数据侧
|
|
201
|
+
├── example/
|
|
202
|
+
│ └── index.js
|
|
203
|
+
├── test.js
|
|
204
|
+
└── package.json
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## 许可证
|
|
208
|
+
|
|
209
|
+
ISC
|
package/example/index.js
CHANGED
|
@@ -11,22 +11,14 @@ const { Lang } = require('../index.js');
|
|
|
11
11
|
function main() {
|
|
12
12
|
console.log('=== 语言包翻译 ===');
|
|
13
13
|
|
|
14
|
-
//
|
|
14
|
+
// 创建语言实例,词条表为扁平结构:键为 语言.层级键,值为文本或复数数组
|
|
15
15
|
var lang = new Lang({
|
|
16
|
-
zh_cn: {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
},
|
|
23
|
-
en: {
|
|
24
|
-
hello: 'Hello, {0}!',
|
|
25
|
-
order: {
|
|
26
|
-
count: ['{count} item', '{count} items']
|
|
27
|
-
},
|
|
28
|
-
greet: ['Hello', 'Hello everyone']
|
|
29
|
-
},
|
|
16
|
+
'zh_cn.hello': '你好, {0}!',
|
|
17
|
+
'zh_cn.order.count': ['共 {count} 个商品'],
|
|
18
|
+
'zh_cn.greet': ['你好', '你们好'],
|
|
19
|
+
'en.hello': 'Hello, {0}!',
|
|
20
|
+
'en.order.count': ['{count} item', '{count} items'],
|
|
21
|
+
'en.greet': ['Hello', 'Hello everyone'],
|
|
30
22
|
default: 'zh_cn'
|
|
31
23
|
});
|
|
32
24
|
|
package/index.js
CHANGED
|
@@ -1,24 +1,48 @@
|
|
|
1
|
-
const Lang = require('./lib/lang');
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
1
|
+
const Lang = require('./lib/lang');
|
|
2
|
+
const Langs = require('./lib/langs');
|
|
3
|
+
const langs_admin = require('./lib/langs_admin');
|
|
4
|
+
const Admin = require('mm_corejs').Admin;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 语言实例管理器(行为侧)
|
|
8
|
+
* @type {Admin}
|
|
9
|
+
*/
|
|
10
|
+
let admin = new Admin();
|
|
11
|
+
admin.setClass(Lang);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 获取语言实例
|
|
15
|
+
* @param {string} scope 作用域
|
|
16
|
+
* @param {Object} config 语言配置,可含 file 持久化路径与 store 存储实例
|
|
17
|
+
* @returns {Lang} 语言实例
|
|
18
|
+
*/
|
|
19
|
+
function langAdmin(scope, config) {
|
|
20
|
+
if (!scope) {
|
|
21
|
+
scope = 'os';
|
|
22
|
+
}
|
|
23
|
+
return admin.get(scope, config);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 获取语言存储实例,不存在时创建
|
|
28
|
+
* 与 Lang 内部使用同一 langs_admin 管理器:以文件路径为作用域时,
|
|
29
|
+
* 配置了相同 file 的 Lang 与 langsAdmin 取到的是同一个存储实例
|
|
30
|
+
* @param {string} [scope] 作用域名称,缺省时使用 os
|
|
31
|
+
* @param {Object} [config] 配置对象,含 file 持久化文件路径
|
|
32
|
+
* @returns {Langs} 语言存储实例
|
|
33
|
+
*/
|
|
34
|
+
function langsAdmin(scope, config) {
|
|
35
|
+
if (!scope) {
|
|
36
|
+
scope = 'os';
|
|
37
|
+
}
|
|
38
|
+
return langs_admin.get(scope, config);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
Lang,
|
|
43
|
+
Langs,
|
|
44
|
+
admin,
|
|
45
|
+
langs_admin,
|
|
46
|
+
langAdmin,
|
|
47
|
+
langsAdmin
|
|
48
|
+
};
|
package/lib/lang.js
CHANGED
|
@@ -1,25 +1,126 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 内置语言类
|
|
3
|
-
* 国际化(i18n)
|
|
4
|
-
*
|
|
3
|
+
* 国际化(i18n)核心:扁平词条表上的取词、回退、复数与格式化
|
|
4
|
+
* 词条表与持久化同构:内存即 `语言代码.层级键 → 文本` 的扁平形态,save/load 无需转换
|
|
5
|
+
* 只接受扁平入参(键为 `语言.层级键`,或 default/fallback/locale/silent/file/store 控制键),
|
|
6
|
+
* 嵌套语言包由调用方展开(mm_tpl 在加载语言包文件后自行扁平化)
|
|
7
|
+
* 持久化通过 langsAdmin 管理的 Langs 存储实例(this.store)完成,本类不做文件 IO
|
|
5
8
|
* @class
|
|
6
9
|
*/
|
|
10
|
+
|
|
11
|
+
const langs_admin = require('./langs_admin.js');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 非词条的控制键,不参与取词、语言列表与持久化
|
|
15
|
+
* 词条键必含点号(`语言.键`),控制键均不含点号,二者天然可分
|
|
16
|
+
* @type {Array<string>}
|
|
17
|
+
*/
|
|
18
|
+
const CONTROL_KEYS = ['default', 'fallback', 'locale', 'options', 'file', 'store', 'silent'];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 非字符串词条(复数数组等)落盘时的标记前缀
|
|
22
|
+
* 落盘后仍是字符串,满足 Langs 的 string→string 约束;读取时按前缀还原
|
|
23
|
+
* @type {string}
|
|
24
|
+
*/
|
|
25
|
+
const MARK = '\u0001';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 判断是否为控制键
|
|
29
|
+
* @param {string} key 键名
|
|
30
|
+
* @returns {boolean} 是控制键返回 true
|
|
31
|
+
*/
|
|
32
|
+
function isControlKey(key) {
|
|
33
|
+
return CONTROL_KEYS.indexOf(key) !== -1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 将词条值编码为可落盘的字符串
|
|
38
|
+
* @param {*} val 词条值
|
|
39
|
+
* @returns {string|null} 存储字符串,不可序列化的值返回 null
|
|
40
|
+
*/
|
|
41
|
+
function encodeValue(val) {
|
|
42
|
+
if (typeof (val) === 'string') {
|
|
43
|
+
return val;
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(val)) {
|
|
46
|
+
return MARK + JSON.stringify(val);
|
|
47
|
+
}
|
|
48
|
+
if (typeof (val) === 'number' || typeof (val) === 'boolean') {
|
|
49
|
+
return String(val);
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 将落盘字符串还原为词条值
|
|
56
|
+
* @param {string} val 存储字符串
|
|
57
|
+
* @returns {*} 词条值
|
|
58
|
+
*/
|
|
59
|
+
function decodeValue(val) {
|
|
60
|
+
if (typeof (val) !== 'string') {
|
|
61
|
+
return val;
|
|
62
|
+
}
|
|
63
|
+
if (val.charAt(0) === MARK) {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(val.slice(1));
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return val;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return val;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 按点号路径向对象写入值(仅 getDict 导出单语言词条时使用)
|
|
75
|
+
* @param {Object} obj 目标对象
|
|
76
|
+
* @param {string} path 点号路径
|
|
77
|
+
* @param {*} val 值
|
|
78
|
+
* @returns {void}
|
|
79
|
+
*/
|
|
80
|
+
function setByPath(obj, path, val) {
|
|
81
|
+
var keys = path.split('.');
|
|
82
|
+
var cur = obj;
|
|
83
|
+
for (var i = 0; i < keys.length - 1; i++) {
|
|
84
|
+
if (!cur[keys[i]] || typeof (cur[keys[i]]) !== 'object' || Array.isArray(cur[keys[i]])) {
|
|
85
|
+
cur[keys[i]] = {};
|
|
86
|
+
}
|
|
87
|
+
cur = cur[keys[i]];
|
|
88
|
+
}
|
|
89
|
+
cur[keys[keys.length - 1]] = val;
|
|
90
|
+
}
|
|
91
|
+
|
|
7
92
|
class Lang {
|
|
8
93
|
/**
|
|
9
94
|
* 创建语言实例
|
|
10
|
-
* @param {Object} config -
|
|
95
|
+
* @param {Object} config - 扁平词条表:键为 `语言.层级键`,值为字符串/数字/布尔/数组;
|
|
96
|
+
* 可含控制键 default、fallback、locale、silent 及存储配置 file、store
|
|
11
97
|
*/
|
|
12
98
|
constructor(config = {}) {
|
|
99
|
+
var cg = Object.assign({}, config);
|
|
100
|
+
var file = cg.file || '';
|
|
101
|
+
var injected = cg.store;
|
|
102
|
+
delete cg.file;
|
|
103
|
+
delete cg.store;
|
|
104
|
+
|
|
13
105
|
/**
|
|
14
|
-
*
|
|
106
|
+
* 扁平词条表,键为 `语言代码.层级键`,与持久化格式一致;控制键并存于同一层
|
|
15
107
|
* @type {Object}
|
|
16
108
|
*/
|
|
17
109
|
this.config = Object.assign({
|
|
18
|
-
zh_cn: {},
|
|
19
|
-
en: {},
|
|
20
110
|
default: 'zh_cn',
|
|
21
111
|
fallback: true
|
|
22
|
-
},
|
|
112
|
+
}, cg);
|
|
113
|
+
|
|
114
|
+
// 词条值不能是嵌套对象,嵌套语言包需由调用方展开为扁平键
|
|
115
|
+
for (var key in this.config) {
|
|
116
|
+
if (!Object.prototype.hasOwnProperty.call(this.config, key) || isControlKey(key)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
var val = this.config[key];
|
|
120
|
+
if (val && typeof (val) === 'object' && !Array.isArray(val)) {
|
|
121
|
+
throw new TypeError('词条 "' + key + '" 的值不能是对象,请使用扁平键(语言.层级键)');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
23
124
|
|
|
24
125
|
/**
|
|
25
126
|
* 复数规则表,键为语言代码,值为按数量返回形式的索引的函数
|
|
@@ -48,6 +149,19 @@ class Lang {
|
|
|
48
149
|
zh: 'zh_cn',
|
|
49
150
|
en: 'en'
|
|
50
151
|
}, this.config.locale);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 持久化文件路径
|
|
155
|
+
* @type {string}
|
|
156
|
+
*/
|
|
157
|
+
this.file = file;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 语言存储实例(数据侧):优先用注入实例,否则按文件路径经 langsAdmin 取共享实例,
|
|
161
|
+
* 两者都无则为 null(纯内存)
|
|
162
|
+
* @type {Langs|null}
|
|
163
|
+
*/
|
|
164
|
+
this.store = injected || (file ? langs_admin.get(file, { file: file }) : null);
|
|
51
165
|
}
|
|
52
166
|
}
|
|
53
167
|
|
|
@@ -72,7 +186,7 @@ Lang.prototype.get = function (key, ...param) {
|
|
|
72
186
|
};
|
|
73
187
|
|
|
74
188
|
/**
|
|
75
|
-
*
|
|
189
|
+
* 带回退机制获取词条 - 内部方法
|
|
76
190
|
* 依次尝试:当前语言、基础语言、默认语言
|
|
77
191
|
* @param {string} key - 键
|
|
78
192
|
* @returns {any} 语言值
|
|
@@ -96,29 +210,14 @@ Lang.prototype._getVal = function (key) {
|
|
|
96
210
|
};
|
|
97
211
|
|
|
98
212
|
/**
|
|
99
|
-
*
|
|
213
|
+
* 从指定语言中获取扁平词条 - 内部方法
|
|
100
214
|
* @param {string} lang - 语言代码
|
|
101
215
|
* @param {string} key - 键
|
|
102
216
|
* @returns {any} 值
|
|
103
217
|
* @private
|
|
104
218
|
*/
|
|
105
219
|
Lang.prototype._getFrom = function (lang, key) {
|
|
106
|
-
|
|
107
|
-
if (!lang_cg) {
|
|
108
|
-
return undefined;
|
|
109
|
-
}
|
|
110
|
-
var arr = key.split('.');
|
|
111
|
-
var val = lang_cg;
|
|
112
|
-
for (var i = 0; i < arr.length; i++) {
|
|
113
|
-
if (val === undefined) {
|
|
114
|
-
break;
|
|
115
|
-
}
|
|
116
|
-
val = val[arr[i]];
|
|
117
|
-
if (val === undefined || typeof (val) === 'string') {
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return val;
|
|
220
|
+
return this.config[lang + '.' + key];
|
|
122
221
|
};
|
|
123
222
|
|
|
124
223
|
/**
|
|
@@ -145,44 +244,13 @@ Lang.prototype.set = function (key, val) {
|
|
|
145
244
|
if (!key || typeof (key) !== 'string') {
|
|
146
245
|
throw new TypeError('key必须是字符串');
|
|
147
246
|
}
|
|
148
|
-
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* 设置语言键值 - 内部方法
|
|
153
|
-
* @param {string} key - 键名
|
|
154
|
-
* @param {any} val - 值
|
|
155
|
-
* @returns {any} 返回设置的值
|
|
156
|
-
* @private
|
|
157
|
-
*/
|
|
158
|
-
Lang.prototype._set = function (key, val) {
|
|
159
|
-
var lang = this.config[this.now];
|
|
160
|
-
if (!lang) {
|
|
161
|
-
this._log('error', '当前语言未初始化: ' + this.now);
|
|
162
|
-
return val;
|
|
163
|
-
}
|
|
164
|
-
var arr = key.split('.');
|
|
165
|
-
var obj = lang;
|
|
166
|
-
for (var i = 0; i < arr.length; i++) {
|
|
167
|
-
var k = arr[i];
|
|
168
|
-
var v = obj[k];
|
|
169
|
-
if (i === arr.length - 1) {
|
|
170
|
-
obj[k] = val;
|
|
171
|
-
} else if (v === undefined) {
|
|
172
|
-
obj[k] = {};
|
|
173
|
-
obj = obj[k];
|
|
174
|
-
} else if (typeof (v) === 'object' && !Array.isArray(v)) {
|
|
175
|
-
obj = v;
|
|
176
|
-
} else {
|
|
177
|
-
obj[k] = {};
|
|
178
|
-
obj = obj[k];
|
|
179
|
-
}
|
|
180
|
-
}
|
|
247
|
+
this.config[this.now + '.' + key] = val;
|
|
181
248
|
return val;
|
|
182
249
|
};
|
|
183
250
|
|
|
184
251
|
/**
|
|
185
252
|
* 切换语言
|
|
253
|
+
* 扁平结构下语言是否存在以是否有该语言前缀的词条为准
|
|
186
254
|
* @param {string} lang - 语言代码
|
|
187
255
|
* @returns {string} 切换后的语言代码
|
|
188
256
|
*/
|
|
@@ -190,12 +258,16 @@ Lang.prototype.setLang = function (lang) {
|
|
|
190
258
|
if (!lang || typeof (lang) !== 'string') {
|
|
191
259
|
throw new TypeError('lang必须是字符串');
|
|
192
260
|
}
|
|
193
|
-
if (
|
|
261
|
+
if (isControlKey(lang)) {
|
|
262
|
+
this._log('error', '语言未找到: ' + lang);
|
|
263
|
+
return this.now;
|
|
264
|
+
}
|
|
265
|
+
if (this.hasLang(lang)) {
|
|
194
266
|
this.now = lang;
|
|
195
267
|
return this.now;
|
|
196
268
|
}
|
|
197
269
|
var base_lang = this._getBase(lang);
|
|
198
|
-
if (this.
|
|
270
|
+
if (base_lang !== lang && this.hasLang(base_lang)) {
|
|
199
271
|
this.now = base_lang;
|
|
200
272
|
return this.now;
|
|
201
273
|
}
|
|
@@ -204,7 +276,25 @@ Lang.prototype.setLang = function (lang) {
|
|
|
204
276
|
};
|
|
205
277
|
|
|
206
278
|
/**
|
|
207
|
-
*
|
|
279
|
+
* 判断某语言是否存在词条
|
|
280
|
+
* @param {string} lang - 语言代码
|
|
281
|
+
* @returns {boolean} 存在该语言的词条时返回 true
|
|
282
|
+
*/
|
|
283
|
+
Lang.prototype.hasLang = function (lang) {
|
|
284
|
+
if (!lang || typeof (lang) !== 'string') {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
var prefix = lang + '.';
|
|
288
|
+
for (var key in this.config) {
|
|
289
|
+
if (key.indexOf(prefix) === 0) {
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 删除某语言的全部词条
|
|
208
298
|
* @param {string} lang - 语言代码
|
|
209
299
|
* @returns {boolean} 是否删除成功
|
|
210
300
|
*/
|
|
@@ -212,11 +302,15 @@ Lang.prototype.del = function (lang) {
|
|
|
212
302
|
if (!lang || typeof (lang) !== 'string') {
|
|
213
303
|
throw new TypeError('lang必须是字符串');
|
|
214
304
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
305
|
+
var prefix = lang + '.';
|
|
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
|
+
}
|
|
218
312
|
}
|
|
219
|
-
return
|
|
313
|
+
return removed;
|
|
220
314
|
};
|
|
221
315
|
|
|
222
316
|
/**
|
|
@@ -333,36 +427,132 @@ Lang.prototype.registerPlural = function (lang, rule) {
|
|
|
333
427
|
|
|
334
428
|
/**
|
|
335
429
|
* 获取支持的语言列表
|
|
430
|
+
* 取词条键的首段去重,无词条的语言不会出现(空语言字典天然不可见)
|
|
336
431
|
* @returns {Array<string>} 语言代码数组
|
|
337
432
|
*/
|
|
338
433
|
Lang.prototype.getLangs = function () {
|
|
339
434
|
var langs = [];
|
|
340
435
|
for (var key in this.config) {
|
|
341
|
-
if (key
|
|
342
|
-
|
|
343
|
-
|
|
436
|
+
if (isControlKey(key)) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
var dot = key.indexOf('.');
|
|
440
|
+
if (dot === -1) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
var lang = key.slice(0, dot);
|
|
444
|
+
if (langs.indexOf(lang) === -1) {
|
|
445
|
+
langs.push(lang);
|
|
344
446
|
}
|
|
345
447
|
}
|
|
346
448
|
return langs;
|
|
347
449
|
};
|
|
348
450
|
|
|
451
|
+
/**
|
|
452
|
+
* 导出某语言的全部词条
|
|
453
|
+
* 扁平词条按前缀取出并还原为嵌套字典,形态与语言包文件一致
|
|
454
|
+
* @param {string} lang - 语言代码
|
|
455
|
+
* @returns {Object|null} 嵌套词条字典,该语言无词条时返回 null
|
|
456
|
+
*/
|
|
457
|
+
Lang.prototype.getDict = function (lang) {
|
|
458
|
+
if (!lang || typeof (lang) !== 'string' || isControlKey(lang)) {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
var prefix = lang + '.';
|
|
462
|
+
var dict = null;
|
|
463
|
+
for (var key in this.config) {
|
|
464
|
+
if (key.indexOf(prefix) !== 0) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (!dict) {
|
|
468
|
+
dict = {};
|
|
469
|
+
}
|
|
470
|
+
setByPath(dict, key.slice(prefix.length), this.config[key]);
|
|
471
|
+
}
|
|
472
|
+
return dict;
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* 保存语言包到持久化文件
|
|
477
|
+
* 内存词条表即扁平 string→string 形态(数组值按标记编码),直接写入存储实例落盘
|
|
478
|
+
* @returns {boolean} 是否保存成功
|
|
479
|
+
*/
|
|
480
|
+
Lang.prototype.save = function () {
|
|
481
|
+
if (!this.store) {
|
|
482
|
+
this._log('error', '未配置持久化文件路径或存储实例,无法保存语言包');
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
this.store.clear();
|
|
487
|
+
for (var key in this.config) {
|
|
488
|
+
if (!Object.prototype.hasOwnProperty.call(this.config, key) || isControlKey(key)) {
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
var str = encodeValue(this.config[key]);
|
|
492
|
+
if (str !== null) {
|
|
493
|
+
this.store.set(key, str);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return this.store.save() !== false;
|
|
497
|
+
} catch (e) {
|
|
498
|
+
this._log('error', '语言包保存失败: ' + e.message);
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* 从持久化文件加载语言包
|
|
505
|
+
* 存储中的扁平键值与内存结构一致,直接合并进词条表(文件中的词条优先),数组编码值还原
|
|
506
|
+
* @returns {boolean} 是否加载成功
|
|
507
|
+
*/
|
|
508
|
+
Lang.prototype.load = function () {
|
|
509
|
+
if (!this.store) {
|
|
510
|
+
this._log('error', '未配置持久化文件路径或存储实例,无法加载语言包');
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
var ok = false;
|
|
514
|
+
try {
|
|
515
|
+
ok = this.store.load();
|
|
516
|
+
} catch (e) {
|
|
517
|
+
this._log('error', '语言包加载失败: ' + e.message);
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
if (!ok) {
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
var list = this.store.list();
|
|
524
|
+
for (var key in list) {
|
|
525
|
+
if (Object.prototype.hasOwnProperty.call(list, key)) {
|
|
526
|
+
this.config[key] = decodeValue(list[key]);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return true;
|
|
530
|
+
};
|
|
531
|
+
|
|
349
532
|
/**
|
|
350
533
|
* 检查语言是否支持
|
|
351
534
|
* @param {string} lang - 语言代码
|
|
352
535
|
* @returns {boolean} 是否支持
|
|
353
536
|
*/
|
|
354
537
|
Lang.prototype.isSupported = function (lang) {
|
|
355
|
-
|
|
538
|
+
if (!lang || typeof (lang) !== 'string' || isControlKey(lang)) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
return this.hasLang(lang) || this.hasLang(this._getBase(lang));
|
|
356
542
|
};
|
|
357
543
|
|
|
358
544
|
/**
|
|
359
545
|
* 输出日志 - 内部方法,使用 console
|
|
546
|
+
* 配置 silent 为 true 时静默(供上层自带日志体系的场景使用,如 mm_tpl)
|
|
360
547
|
* @param {string} level - 日志级别
|
|
361
548
|
* @param {string} message - 日志消息
|
|
362
549
|
* @param {...any} args - 附加参数
|
|
363
550
|
* @private
|
|
364
551
|
*/
|
|
365
552
|
Lang.prototype._log = function (level, message, ...args) {
|
|
553
|
+
if (this.config.silent) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
366
556
|
var fn = console[level] || console.log;
|
|
367
557
|
if (typeof (fn) === 'function') {
|
|
368
558
|
fn.call(console, message, ...args);
|
package/lib/langs.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
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 Admin = require('mm_corejs').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 = new Admin();
|
|
11
|
+
langs_admin.setClass(Langs);
|
|
12
|
+
|
|
13
|
+
module.exports = langs_admin;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mm_lang",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "超级美眉多语言模块:融合 i18n 行为(Lang)与语言存储持久化(Langs),支持层级键、回退链、复数、格式化与 JSON 文件落盘",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -10,13 +10,31 @@
|
|
|
10
10
|
"keywords": [
|
|
11
11
|
"mm_mod",
|
|
12
12
|
"mm_infra",
|
|
13
|
-
"
|
|
13
|
+
"mm_data",
|
|
14
|
+
"lang",
|
|
15
|
+
"i18n",
|
|
16
|
+
"locale",
|
|
17
|
+
"langs",
|
|
18
|
+
"多语言",
|
|
19
|
+
"语言包"
|
|
14
20
|
],
|
|
15
21
|
"layer": "infra",
|
|
16
|
-
"group": "
|
|
22
|
+
"group": "data",
|
|
17
23
|
"author": "邱文武",
|
|
18
24
|
"license": "ISC",
|
|
19
25
|
"scripts": {
|
|
20
|
-
"test": "node test.js"
|
|
21
|
-
|
|
26
|
+
"test": "node test.js",
|
|
27
|
+
"example": "node example/index.js"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"mm_corejs": "^1.1.6",
|
|
31
|
+
"mm_kv_store": "^1.0.1"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"index.js",
|
|
35
|
+
"lib",
|
|
36
|
+
"example",
|
|
37
|
+
"README.md",
|
|
38
|
+
"package.json"
|
|
39
|
+
]
|
|
22
40
|
}
|
package/test.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const assert = require('assert');
|
|
3
|
-
const mod = require('./index.js');
|
|
4
|
-
|
|
5
|
-
// 验证导出结构
|
|
6
|
-
assert.ok(mod.Lang, '应导出 Lang');
|
|
7
|
-
assert.ok(mod.admin, '应导出 admin');
|
|
8
|
-
assert.strictEqual(typeof mod.langAdmin, 'function', '应导出 langAdmin 函数');
|
|
9
|
-
|
|
10
|
-
// 验证通过工厂函数获取实例
|
|
11
|
-
const inst = mod.langAdmin('test');
|
|
12
|
-
assert.ok(inst instanceof mod.Lang, 'langAdmin 返回的实例应为 Lang 类型');
|
|
13
|
-
|
|
14
|
-
// 验证通过 admin 获取实例
|
|
15
|
-
const inst2 = mod.admin.get('test2');
|
|
16
|
-
assert.ok(inst2 instanceof mod.Lang, 'admin.get 返回的实例应为 Lang 类型');
|
|
17
|
-
|
|
18
|
-
console.log('mm_lang 模块测试通过');
|