mm_lang 0.0.0-stage → 1.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 +147 -2
- package/example/index.js +54 -0
- package/index.js +24 -0
- package/lib/lang.js +372 -0
- package/package.json +20 -4
- package/test.js +18 -0
package/README.md
CHANGED
|
@@ -1,3 +1,148 @@
|
|
|
1
|
-
#
|
|
1
|
+
# mm_lang
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
超级美眉多语言/语言包模块,提供 `Lang` 类作为国际化(i18n)核心能力:按语言字典取值、`a.b` 层级键解析、多级回退机制(当前语言 → 基础语言 → 默认语言)、复数形式、占位符参数替换,以及日期与数字的本地化格式化。行为对齐 mm_expand 的 Lang,但不依赖外部模块与全局变量。
|
|
4
|
+
|
|
5
|
+
## 项目信息
|
|
6
|
+
|
|
7
|
+
- **当前版本**: 1.0.0
|
|
8
|
+
- **更新日期**: 2026-10-04
|
|
9
|
+
- **Gitee地址**: https://gitee.com/qiuwenwu91/mm_lang
|
|
10
|
+
- **依赖**: 无第三方依赖(纯 Node.js 实现)
|
|
11
|
+
|
|
12
|
+
## 特点
|
|
13
|
+
|
|
14
|
+
- 零依赖,内置 `zh_cn` 与 `en` 两种语言的默认词条容器
|
|
15
|
+
- 支持 `a.b.c` 形式的层级键取值与写入
|
|
16
|
+
- 带回退机制的取值链:当前语言 → 基础语言(如 `en-US` 回退到 `en`)→ 默认语言
|
|
17
|
+
- 复数形式支持:`tc` 按数量选择词条数组索引,内置英文/中文复数规则,可通过 `registerPlural` 自定义
|
|
18
|
+
- 占位符替换:`get`/`t` 支持 `{0}`、`{1}` 等;`tc` 支持 `{count}` 及 `{1}`、`{2}` 等
|
|
19
|
+
- 附带日期格式化(`formatDate`)与数字格式化(`formatNumber`,可配置小数位数与千分位/小数分隔符)
|
|
20
|
+
|
|
21
|
+
## 安装
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install mm_lang
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 基本使用
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
const { Lang } = require('mm_lang');
|
|
31
|
+
|
|
32
|
+
const lang = new Lang({
|
|
33
|
+
zh_cn: {
|
|
34
|
+
common: { hello: '你好,{0}!' },
|
|
35
|
+
apple: ['一个苹果', '{count}个苹果']
|
|
36
|
+
},
|
|
37
|
+
en: {
|
|
38
|
+
common: { hello: 'Hello, {0}!' },
|
|
39
|
+
apple: ['one apple', '{count} apples']
|
|
40
|
+
},
|
|
41
|
+
default: 'zh_cn',
|
|
42
|
+
fallback: true
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
console.log(lang.get('common.hello', '美眉')); // 你好,美眉!
|
|
46
|
+
console.log(lang.setLang('en')); // en
|
|
47
|
+
console.log(lang.t('common.hello', 'MM')); // Hello, MM!
|
|
48
|
+
console.log(lang.tc('apple', 3)); // 3 apples
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## API 说明
|
|
52
|
+
|
|
53
|
+
### 构造 `new Lang(dict)`
|
|
54
|
+
|
|
55
|
+
`dict` 为语言包字典对象,各字段与默认值:
|
|
56
|
+
|
|
57
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
58
|
+
| --- | --- | --- | --- |
|
|
59
|
+
| `zh_cn` | `Object` | `{}` | 简体中文词条字典 |
|
|
60
|
+
| `en` | `Object` | `{}` | 英文词条字典 |
|
|
61
|
+
| `default` | `String` | `'zh_cn'` | 默认语言代码,同时作为初始当前语言 |
|
|
62
|
+
| `fallback` | `Boolean` | `true` | 是否启用回退机制 |
|
|
63
|
+
| `locale` | `Object` | `{ zh: 'zh_cn', en: 'en' }` | 完整语言代码到基础语言代码的映射(与默认值合并) |
|
|
64
|
+
|
|
65
|
+
实例属性:`config`(语言包配置)、`rules`(复数规则表)、`now`(当前语言代码)、`locale`(区域映射表)。
|
|
66
|
+
|
|
67
|
+
### 取值与翻译
|
|
68
|
+
|
|
69
|
+
| 方法 | 签名 | 返回值 | 说明 |
|
|
70
|
+
| --- | --- | --- | --- |
|
|
71
|
+
| get | `get(key, ...param)` | `String` | 获取翻译文本,支持 `a.b` 层级键;`param` 依次替换 `{0}`、`{1}` 等占位符;`key` 非字符串时抛 `TypeError` |
|
|
72
|
+
| t | `t(key, ...param)` | `String` | `get` 的别名 |
|
|
73
|
+
| tc | `tc(key, count, ...param)` | `String` | 翻译带复数形式的文本:词条值为数组时按当前语言复数规则取索引,先替换 `{count}`,再用 `param` 依次替换 `{1}`、`{2}` 等;`key` 非字符串或 `count` 非数字时抛 `TypeError` |
|
|
74
|
+
|
|
75
|
+
### 语言管理
|
|
76
|
+
|
|
77
|
+
| 方法 | 签名 | 返回值 | 说明 |
|
|
78
|
+
| --- | --- | --- | --- |
|
|
79
|
+
| setLang | `setLang(lang)` | `String` | 切换语言;找不到时先尝试基础语言映射,仍失败则输出错误日志并返回当前语言 |
|
|
80
|
+
| getLangs | `getLangs()` | `Array<String>` | 获取支持的语言代码列表(排除 `default`、`fallback`、`options` 等非词条键) |
|
|
81
|
+
| isSupported | `isSupported(lang)` | `Boolean` | 检查语言是否支持(含基础语言映射) |
|
|
82
|
+
| del | `del(lang)` | `Boolean` | 删除指定语言配置,删除成功返回 `true` |
|
|
83
|
+
| set | `set(key, val)` | `any` | 向当前语言写入键值,支持 `a.b` 层级键(中间层自动创建),返回设置的值 |
|
|
84
|
+
| registerPlural | `registerPlural(lang, rule)` | `Boolean` | 注册语言复数规则;`rule(count)` 需返回形式索引(数字),参数类型不符时抛 `TypeError` |
|
|
85
|
+
|
|
86
|
+
### 格式化
|
|
87
|
+
|
|
88
|
+
| 方法 | 签名 | 返回值 | 说明 |
|
|
89
|
+
| --- | --- | --- | --- |
|
|
90
|
+
| formatDate | `formatDate(dt, format)` | `String` | 格式化日期;`dt` 为 `Date` 或时间戳,`format` 默认 `YYYY-MM-DD hh:mm:ss`,支持 `YYYY/yyyy`、`MM`、`DD/dd`、`HH/hh`、`mm`、`ss` 标记;`dt` 为空或无效日期时抛 `TypeError` |
|
|
91
|
+
| formatNumber | `formatNumber(num, options)` | `String` | 格式化数字;`options` 支持 `decimals`(小数位数,默认 2)、`decimal_separator`(小数分隔符,默认 `.`)、`thousands_separator`(千分位分隔符,默认 `,`);`num` 非有效数字时抛 `TypeError` |
|
|
92
|
+
|
|
93
|
+
> 另有 `_getVal`、`_getFrom`、`_getBase`、`_set`、`_log` 等下划线前缀的内部私有方法,不建议外部调用。
|
|
94
|
+
|
|
95
|
+
## 使用示例
|
|
96
|
+
|
|
97
|
+
### 场景一:回退机制
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
const { Lang } = require('mm_lang');
|
|
101
|
+
|
|
102
|
+
const lang = new Lang({
|
|
103
|
+
zh_cn: { nav: { home: '首页' } },
|
|
104
|
+
en: { nav: { home: 'Home', about: 'About' } },
|
|
105
|
+
default: 'en',
|
|
106
|
+
fallback: true
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
lang.setLang('zh_cn');
|
|
110
|
+
console.log(lang.get('nav.about')); // 中文缺失,回退到默认语言 en → "About"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 场景二:自定义复数规则
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
const { Lang } = require('mm_lang');
|
|
117
|
+
|
|
118
|
+
const lang = new Lang({
|
|
119
|
+
ru: { apple: ['яблоко', 'яблока', 'яблок'] },
|
|
120
|
+
default: 'ru'
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// 俄语复数规则:按数量返回三种形式之一的索引
|
|
124
|
+
lang.registerPlural('ru', function (count) {
|
|
125
|
+
const n10 = count % 10, n100 = count % 100;
|
|
126
|
+
if (n10 === 1 && n100 !== 11) return 0;
|
|
127
|
+
if (n10 >= 2 && n10 <= 4 && (n100 < 12 || n100 > 14)) return 1;
|
|
128
|
+
return 2;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
console.log(lang.tc('apple', 1)); // яблоко
|
|
132
|
+
console.log(lang.tc('apple', 3)); // яблока
|
|
133
|
+
console.log(lang.tc('apple', 11)); // яблок
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 场景三:本地化格式化
|
|
137
|
+
|
|
138
|
+
```javascript
|
|
139
|
+
const { Lang } = require('mm_lang');
|
|
140
|
+
|
|
141
|
+
const lang = new Lang();
|
|
142
|
+
console.log(lang.formatDate(new Date(2026, 9, 4), 'YYYY年MM月DD日')); // 2026年10月04日
|
|
143
|
+
console.log(lang.formatNumber(1234567.891, {
|
|
144
|
+
decimals: 2,
|
|
145
|
+
decimal_separator: ',',
|
|
146
|
+
thousands_separator: '.'
|
|
147
|
+
})); // 1.234.567,89
|
|
148
|
+
```
|
package/example/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mm_lang 多语言/语言包模块示例
|
|
3
|
+
* 演示 Lang 类的翻译(get/t)、语言切换(setLang)与复数形式(tc)
|
|
4
|
+
* 运行:node example/index.js
|
|
5
|
+
*/
|
|
6
|
+
const { Lang } = require('../index.js');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 主函数
|
|
10
|
+
*/
|
|
11
|
+
function main() {
|
|
12
|
+
console.log('=== 语言包翻译 ===');
|
|
13
|
+
|
|
14
|
+
// 创建语言实例,字典为 语言代码 -> 词条对象
|
|
15
|
+
var lang = new Lang({
|
|
16
|
+
zh_cn: {
|
|
17
|
+
hello: '你好, {0}!',
|
|
18
|
+
order: {
|
|
19
|
+
count: ['共 {count} 个商品']
|
|
20
|
+
},
|
|
21
|
+
greet: ['你好', '你们好']
|
|
22
|
+
},
|
|
23
|
+
en: {
|
|
24
|
+
hello: 'Hello, {0}!',
|
|
25
|
+
order: {
|
|
26
|
+
count: ['{count} item', '{count} items']
|
|
27
|
+
},
|
|
28
|
+
greet: ['Hello', 'Hello everyone']
|
|
29
|
+
},
|
|
30
|
+
default: 'zh_cn'
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// 翻译并替换占位符
|
|
34
|
+
console.log('zh_cn hello ->', lang.get('hello', '张三'));
|
|
35
|
+
|
|
36
|
+
// 切换语言
|
|
37
|
+
lang.setLang('en');
|
|
38
|
+
console.log('en hello ->', lang.t('hello', 'Tom'));
|
|
39
|
+
|
|
40
|
+
// en 的复数形式按数量区分
|
|
41
|
+
console.log('en order.count(1) ->', lang.tc('order.count', 1));
|
|
42
|
+
console.log('en order.count(5) ->', lang.tc('order.count', 5));
|
|
43
|
+
|
|
44
|
+
// 切换回中文
|
|
45
|
+
lang.setLang('zh_cn');
|
|
46
|
+
console.log('zh_cn greet(1) ->', lang.tc('greet', 1));
|
|
47
|
+
console.log('支持的语言列表 ->', lang.getLangs().join(', '));
|
|
48
|
+
|
|
49
|
+
// 动态设置词条
|
|
50
|
+
lang.set('bye', '再见');
|
|
51
|
+
console.log('新词条 bye ->', lang.get('bye'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
main();
|
package/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const Lang = require('./lib/lang');
|
|
2
|
+
const Admin = require('mm_corejs').Admin;
|
|
3
|
+
|
|
4
|
+
let admin = new Admin();
|
|
5
|
+
admin.setClass(Lang);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 获取语言实例
|
|
9
|
+
* @param {string} scope 作用域
|
|
10
|
+
* @param {Object} config 语言配置
|
|
11
|
+
* @returns {Lang} 语言实例
|
|
12
|
+
*/
|
|
13
|
+
function langAdmin(scope, config) {
|
|
14
|
+
if (!scope) {
|
|
15
|
+
scope = 'os';
|
|
16
|
+
}
|
|
17
|
+
return admin.get(scope, config);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
Lang,
|
|
22
|
+
admin,
|
|
23
|
+
langAdmin
|
|
24
|
+
};
|
package/lib/lang.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 内置语言类
|
|
3
|
+
* 国际化(i18n)核心:按语言字典取值、层级键解析、回退机制、复数形式与格式化
|
|
4
|
+
* 行为对齐 mm_expand 的 Lang,但不依赖外部模块与全局变量
|
|
5
|
+
* @class
|
|
6
|
+
*/
|
|
7
|
+
class Lang {
|
|
8
|
+
/**
|
|
9
|
+
* 创建语言实例
|
|
10
|
+
* @param {Object} config - 语言包配置,键为语言代码,值可为词条对象;可含 default 与 fallback
|
|
11
|
+
*/
|
|
12
|
+
constructor(config = {}) {
|
|
13
|
+
/**
|
|
14
|
+
* 语言包配置,语言代码对应词条字典
|
|
15
|
+
* @type {Object}
|
|
16
|
+
*/
|
|
17
|
+
this.config = Object.assign({
|
|
18
|
+
zh_cn: {},
|
|
19
|
+
en: {},
|
|
20
|
+
default: 'zh_cn',
|
|
21
|
+
fallback: true
|
|
22
|
+
}, config);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 复数规则表,键为语言代码,值为按数量返回形式的索引的函数
|
|
26
|
+
* @type {Object}
|
|
27
|
+
*/
|
|
28
|
+
this.rules = {
|
|
29
|
+
en: function (count) {
|
|
30
|
+
return count === 1 ? 0 : 1;
|
|
31
|
+
},
|
|
32
|
+
zh_cn: function () {
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 当前语言代码
|
|
39
|
+
* @type {string}
|
|
40
|
+
*/
|
|
41
|
+
this.now = this.config.default || 'zh_cn';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 区域映射表,完整语言代码到基础语言代码
|
|
45
|
+
* @type {Object}
|
|
46
|
+
*/
|
|
47
|
+
this.locale = Object.assign({
|
|
48
|
+
zh: 'zh_cn',
|
|
49
|
+
en: 'en'
|
|
50
|
+
}, this.config.locale);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 获取翻译文本
|
|
56
|
+
* @param {string} key - 键,支持 a.b 形式的层级键
|
|
57
|
+
* @param {...any} param - 参数,依次替换 {0}、{1} 等占位符
|
|
58
|
+
* @returns {string} 翻译后的文本
|
|
59
|
+
*/
|
|
60
|
+
Lang.prototype.get = function (key, ...param) {
|
|
61
|
+
if (!key || typeof (key) !== 'string') {
|
|
62
|
+
throw new TypeError('key必须是字符串');
|
|
63
|
+
}
|
|
64
|
+
var val = this._getVal(key);
|
|
65
|
+
if (typeof (val) === 'string' && param.length > 0) {
|
|
66
|
+
for (var i = 0; i < param.length; i++) {
|
|
67
|
+
var reg = new RegExp('\\{' + i + '\\}', 'ig');
|
|
68
|
+
val = val.replace(reg, param[i]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return val;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 带回退机制获取语言值 - 内部方法
|
|
76
|
+
* 依次尝试:当前语言、基础语言、默认语言
|
|
77
|
+
* @param {string} key - 键
|
|
78
|
+
* @returns {any} 语言值
|
|
79
|
+
* @private
|
|
80
|
+
*/
|
|
81
|
+
Lang.prototype._getVal = function (key) {
|
|
82
|
+
var val = this._getFrom(this.now, key);
|
|
83
|
+
if (val === undefined && this.config.fallback) {
|
|
84
|
+
var lang = this._getBase(this.now);
|
|
85
|
+
if (lang && lang !== this.now) {
|
|
86
|
+
val = this._getFrom(lang, key);
|
|
87
|
+
}
|
|
88
|
+
if (val === undefined && this.config.default && this.config.default !== this.now) {
|
|
89
|
+
val = this._getFrom(this.config.default, key);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (val === undefined) {
|
|
93
|
+
this._log('error', '语言包错误: ' + key + ' 未定义');
|
|
94
|
+
}
|
|
95
|
+
return val;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 从指定语言中获取值 - 内部方法
|
|
100
|
+
* @param {string} lang - 语言代码
|
|
101
|
+
* @param {string} key - 键
|
|
102
|
+
* @returns {any} 值
|
|
103
|
+
* @private
|
|
104
|
+
*/
|
|
105
|
+
Lang.prototype._getFrom = function (lang, key) {
|
|
106
|
+
var lang_cg = this.config[lang];
|
|
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;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 获取基础语言代码 - 内部方法
|
|
126
|
+
* @param {string} lang - 完整语言代码
|
|
127
|
+
* @returns {string} 基础语言代码
|
|
128
|
+
* @private
|
|
129
|
+
*/
|
|
130
|
+
Lang.prototype._getBase = function (lang) {
|
|
131
|
+
if (this.locale[lang]) {
|
|
132
|
+
return this.locale[lang];
|
|
133
|
+
}
|
|
134
|
+
var parts = lang.split('-');
|
|
135
|
+
return parts.length > 1 ? parts[0] : lang;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 设置语言键值到当前语言
|
|
140
|
+
* @param {string} key - 键名,支持 a.b 形式的层级键
|
|
141
|
+
* @param {any} val - 值
|
|
142
|
+
* @returns {any} 返回设置的值
|
|
143
|
+
*/
|
|
144
|
+
Lang.prototype.set = function (key, val) {
|
|
145
|
+
if (!key || typeof (key) !== 'string') {
|
|
146
|
+
throw new TypeError('key必须是字符串');
|
|
147
|
+
}
|
|
148
|
+
return this._set(key, val);
|
|
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
|
+
}
|
|
181
|
+
return val;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 切换语言
|
|
186
|
+
* @param {string} lang - 语言代码
|
|
187
|
+
* @returns {string} 切换后的语言代码
|
|
188
|
+
*/
|
|
189
|
+
Lang.prototype.setLang = function (lang) {
|
|
190
|
+
if (!lang || typeof (lang) !== 'string') {
|
|
191
|
+
throw new TypeError('lang必须是字符串');
|
|
192
|
+
}
|
|
193
|
+
if (this.config[lang]) {
|
|
194
|
+
this.now = lang;
|
|
195
|
+
return this.now;
|
|
196
|
+
}
|
|
197
|
+
var base_lang = this._getBase(lang);
|
|
198
|
+
if (this.config[base_lang]) {
|
|
199
|
+
this.now = base_lang;
|
|
200
|
+
return this.now;
|
|
201
|
+
}
|
|
202
|
+
this._log('error', '语言未找到: ' + lang);
|
|
203
|
+
return this.now;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 删除语言配置
|
|
208
|
+
* @param {string} lang - 语言代码
|
|
209
|
+
* @returns {boolean} 是否删除成功
|
|
210
|
+
*/
|
|
211
|
+
Lang.prototype.del = function (lang) {
|
|
212
|
+
if (!lang || typeof (lang) !== 'string') {
|
|
213
|
+
throw new TypeError('lang必须是字符串');
|
|
214
|
+
}
|
|
215
|
+
if (this.config[lang]) {
|
|
216
|
+
delete this.config[lang];
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 翻译
|
|
224
|
+
* @param {string} key - 键
|
|
225
|
+
* @param {...any} param - 参数
|
|
226
|
+
* @returns {string} 翻译后的文本
|
|
227
|
+
*/
|
|
228
|
+
Lang.prototype.t = function (key, ...param) {
|
|
229
|
+
return this.get(key, ...param);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 翻译带复数形式的文本
|
|
234
|
+
* 词条值为数组时按复数规则取索引,先替换 {count},再依次替换 {1}、{2} 等占位符
|
|
235
|
+
* @param {string} key - 键
|
|
236
|
+
* @param {number} count - 数量,用于确定复数形式
|
|
237
|
+
* @param {...any} param - 额外参数
|
|
238
|
+
* @returns {string} 翻译后的文本
|
|
239
|
+
*/
|
|
240
|
+
Lang.prototype.tc = function (key, count, ...param) {
|
|
241
|
+
if (!key || typeof (key) !== 'string') {
|
|
242
|
+
throw new TypeError('key必须是字符串');
|
|
243
|
+
}
|
|
244
|
+
if (typeof (count) !== 'number') {
|
|
245
|
+
throw new TypeError('count必须是数字');
|
|
246
|
+
}
|
|
247
|
+
var lang = this.now;
|
|
248
|
+
var rule = this.rules[lang] || this.rules[this._getBase(lang)] || this.rules['en'];
|
|
249
|
+
var idx = rule(count);
|
|
250
|
+
var obj = this._getVal(key);
|
|
251
|
+
var val;
|
|
252
|
+
if (typeof (obj) === 'object' && obj !== null) {
|
|
253
|
+
val = obj[idx];
|
|
254
|
+
}
|
|
255
|
+
if (typeof (val) === 'string') {
|
|
256
|
+
val = val.replace(/\{count\}/ig, count);
|
|
257
|
+
for (var i = 0; i < param.length; i++) {
|
|
258
|
+
var reg = new RegExp('\\{' + (i + 1) + '\\}', 'ig');
|
|
259
|
+
val = val.replace(reg, param[i]);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return val;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 格式化日期
|
|
267
|
+
* @param {Date|number} dt - 日期对象或时间戳
|
|
268
|
+
* @param {string} format - 格式,默认 YYYY-MM-DD hh:mm:ss
|
|
269
|
+
* @returns {string} 格式化后的日期字符串
|
|
270
|
+
*/
|
|
271
|
+
Lang.prototype.formatDate = function (dt, format) {
|
|
272
|
+
if (!dt) {
|
|
273
|
+
throw new TypeError('date不能为空');
|
|
274
|
+
}
|
|
275
|
+
var fmt = format || 'YYYY-MM-DD hh:mm:ss';
|
|
276
|
+
var d = new Date(dt);
|
|
277
|
+
if (isNaN(d.getTime())) {
|
|
278
|
+
throw new TypeError('无效的日期');
|
|
279
|
+
}
|
|
280
|
+
var year = d.getFullYear();
|
|
281
|
+
var month = String(d.getMonth() + 1).padStart(2, '0');
|
|
282
|
+
var day = String(d.getDate()).padStart(2, '0');
|
|
283
|
+
var hours = String(d.getHours()).padStart(2, '0');
|
|
284
|
+
var minutes = String(d.getMinutes()).padStart(2, '0');
|
|
285
|
+
var seconds = String(d.getSeconds()).padStart(2, '0');
|
|
286
|
+
return fmt
|
|
287
|
+
.replace('YYYY', year)
|
|
288
|
+
.replace('yyyy', year)
|
|
289
|
+
.replace('MM', month)
|
|
290
|
+
.replace('DD', day)
|
|
291
|
+
.replace('dd', day)
|
|
292
|
+
.replace('HH', hours)
|
|
293
|
+
.replace('hh', hours)
|
|
294
|
+
.replace('mm', minutes)
|
|
295
|
+
.replace('ss', seconds);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 格式化数字
|
|
300
|
+
* @param {number} num - 要格式化的数字
|
|
301
|
+
* @param {Object} options - 选项,含 decimals、decimal_separator、thousands_separator
|
|
302
|
+
* @returns {string} 格式化后的数字字符串
|
|
303
|
+
*/
|
|
304
|
+
Lang.prototype.formatNumber = function (num, options) {
|
|
305
|
+
if (typeof (num) !== 'number' || isNaN(num)) {
|
|
306
|
+
throw new TypeError('num必须是有效的数字');
|
|
307
|
+
}
|
|
308
|
+
var opt = options || {};
|
|
309
|
+
var decimals = typeof (opt.decimals) === 'number' ? opt.decimals : 2;
|
|
310
|
+
var decimal_sepa = opt.decimal_separator || '.';
|
|
311
|
+
var thou_sepa = opt.thousands_separator || ',';
|
|
312
|
+
var parts = num.toFixed(decimals).split('.');
|
|
313
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thou_sepa);
|
|
314
|
+
return parts.join(decimal_sepa);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 注册语言复数规则
|
|
319
|
+
* @param {string} lang - 语言代码
|
|
320
|
+
* @param {Function} rule - 复数规则函数,按数量返回形式的索引
|
|
321
|
+
* @returns {boolean} 是否注册成功
|
|
322
|
+
*/
|
|
323
|
+
Lang.prototype.registerPlural = function (lang, rule) {
|
|
324
|
+
if (!lang || typeof (lang) !== 'string') {
|
|
325
|
+
throw new TypeError('lang必须是字符串');
|
|
326
|
+
}
|
|
327
|
+
if (typeof (rule) !== 'function') {
|
|
328
|
+
throw new TypeError('rule必须是函数');
|
|
329
|
+
}
|
|
330
|
+
this.rules[lang] = rule;
|
|
331
|
+
return true;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* 获取支持的语言列表
|
|
336
|
+
* @returns {Array<string>} 语言代码数组
|
|
337
|
+
*/
|
|
338
|
+
Lang.prototype.getLangs = function () {
|
|
339
|
+
var langs = [];
|
|
340
|
+
for (var key in this.config) {
|
|
341
|
+
if (key !== 'default' && key !== 'fallback' && key !== 'options'
|
|
342
|
+
&& typeof (this.config[key]) === 'object') {
|
|
343
|
+
langs.push(key);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return langs;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* 检查语言是否支持
|
|
351
|
+
* @param {string} lang - 语言代码
|
|
352
|
+
* @returns {boolean} 是否支持
|
|
353
|
+
*/
|
|
354
|
+
Lang.prototype.isSupported = function (lang) {
|
|
355
|
+
return !!this.config[lang] || !!this.config[this._getBase(lang)];
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* 输出日志 - 内部方法,使用 console
|
|
360
|
+
* @param {string} level - 日志级别
|
|
361
|
+
* @param {string} message - 日志消息
|
|
362
|
+
* @param {...any} args - 附加参数
|
|
363
|
+
* @private
|
|
364
|
+
*/
|
|
365
|
+
Lang.prototype._log = function (level, message, ...args) {
|
|
366
|
+
var fn = console[level] || console.log;
|
|
367
|
+
if (typeof (fn) === 'function') {
|
|
368
|
+
fn.call(console, message, ...args);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
module.exports = Lang;
|
package/package.json
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mm_lang",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "超级美眉多语言/语言包模块",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://gitee.com/qiuwenwu91/mm_lang.git"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"mm_mod",
|
|
12
|
+
"mm_infra",
|
|
13
|
+
"mm_common"
|
|
14
|
+
],
|
|
15
|
+
"layer": "infra",
|
|
16
|
+
"group": "common",
|
|
17
|
+
"author": "邱文武",
|
|
18
|
+
"license": "ISC",
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node test.js"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/test.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
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 模块测试通过');
|