@kne/fastify-intl-admin 0.1.0 → 0.1.2
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/libs/controllers/lang-lib.js +57 -13
- package/libs/controllers/lang-type.js +40 -9
- package/libs/schema/lang-lib.js +137 -58
- package/libs/schema/lang-type.js +60 -26
- package/libs/schema/response.js +92 -0
- package/package.json +1 -1
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
const fp = require('fastify-plugin');
|
|
2
|
+
const pick = require('lodash/pick');
|
|
2
3
|
const schema = require('../schema/lang-lib');
|
|
4
|
+
const { empty } = require('../schema/response');
|
|
5
|
+
|
|
6
|
+
const TAG = '国际化管理后台-语言库';
|
|
7
|
+
const RUNTIME_TAG = '国际化-运行时';
|
|
8
|
+
|
|
9
|
+
// save 只透传客户端可修改的字段,避免 userId、deletedAt 等未声明字段写进数据库
|
|
10
|
+
const SAVE_FIELDS = ['id', 'target', 'status'];
|
|
3
11
|
|
|
4
12
|
module.exports = fp(async (fastify, options) => {
|
|
5
13
|
const { services } = fastify[options.name];
|
|
@@ -10,8 +18,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
10
18
|
{
|
|
11
19
|
onRequest: getAuth('langLib'),
|
|
12
20
|
schema: {
|
|
21
|
+
tags: [TAG],
|
|
13
22
|
summary: '语言库词条列表',
|
|
14
|
-
|
|
23
|
+
description: '分页查询语言词条,每条附带默认语言源文(sourceTarget)便于对照翻译',
|
|
24
|
+
query: schema.listQuery,
|
|
25
|
+
response: schema.listResponse
|
|
15
26
|
}
|
|
16
27
|
},
|
|
17
28
|
async request => services.langLib.list(request.userInfo || {}, request.query)
|
|
@@ -22,7 +33,10 @@ module.exports = fp(async (fastify, options) => {
|
|
|
22
33
|
{
|
|
23
34
|
onRequest: getAuth('langLib'),
|
|
24
35
|
schema: {
|
|
25
|
-
|
|
36
|
+
tags: [TAG],
|
|
37
|
+
summary: '语言库命名空间列表',
|
|
38
|
+
description: '列出语言库中已有词条的全部命名空间(去重)',
|
|
39
|
+
response: schema.namespacesResponse
|
|
26
40
|
}
|
|
27
41
|
},
|
|
28
42
|
async request => services.langLib.namespaces(request.userInfo || {}, request.query)
|
|
@@ -33,8 +47,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
33
47
|
{
|
|
34
48
|
onRequest: getAuth('langLib:manage'),
|
|
35
49
|
schema: {
|
|
50
|
+
tags: [TAG],
|
|
36
51
|
summary: '批量添加语言词条',
|
|
37
|
-
|
|
52
|
+
description: '为同一命名空间 + 编码一次创建多种语言的词条。手动模式用 targets 填译文,均直接审核通过;aiGenerate 模式由 generateTargets 生成非默认语言译文,默认语言词条直接通过,其他语言为待审核',
|
|
53
|
+
body: schema.addPackageBody,
|
|
54
|
+
response: schema.addPackageResponse
|
|
38
55
|
}
|
|
39
56
|
},
|
|
40
57
|
async request => services.langLib.addPackage(request.userInfo || {}, request.body)
|
|
@@ -45,12 +62,15 @@ module.exports = fp(async (fastify, options) => {
|
|
|
45
62
|
{
|
|
46
63
|
onRequest: getAuth('langLib:manage'),
|
|
47
64
|
schema: {
|
|
65
|
+
tags: [TAG],
|
|
48
66
|
summary: '保存语言词条',
|
|
49
|
-
|
|
67
|
+
description: '修改词条译文与状态;命名空间、编码、语言不可修改。译文变化时默认语言词条直接审核通过,其他语言重置为待审核并清空审核备注;未声明的字段(如 userId、reviewStatus)会被忽略',
|
|
68
|
+
body: schema.saveBody,
|
|
69
|
+
response: empty('保存成功,返回空对象')
|
|
50
70
|
}
|
|
51
71
|
},
|
|
52
72
|
async request => {
|
|
53
|
-
await services.langLib.save(request.userInfo || {}, request.body);
|
|
73
|
+
await services.langLib.save(request.userInfo || {}, pick(request.body, SAVE_FIELDS));
|
|
54
74
|
return {};
|
|
55
75
|
}
|
|
56
76
|
);
|
|
@@ -60,8 +80,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
60
80
|
{
|
|
61
81
|
onRequest: getAuth('langLib:manage'),
|
|
62
82
|
schema: {
|
|
83
|
+
tags: [TAG],
|
|
63
84
|
summary: '删除语言词条',
|
|
64
|
-
|
|
85
|
+
description: '按 id 或 ids 删除词条,均不存在时报错',
|
|
86
|
+
body: schema.removeBody,
|
|
87
|
+
response: empty('删除成功,返回空对象')
|
|
65
88
|
}
|
|
66
89
|
},
|
|
67
90
|
async request => {
|
|
@@ -75,8 +98,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
75
98
|
{
|
|
76
99
|
onRequest: getAuth('langLib:manage'),
|
|
77
100
|
schema: {
|
|
101
|
+
tags: [TAG],
|
|
78
102
|
summary: '设置语言词条状态',
|
|
79
|
-
|
|
103
|
+
description: '按 id 或 ids 批量启用或禁用词条;禁用的词条不出现在运行时语言包中',
|
|
104
|
+
body: schema.setStatusBody,
|
|
105
|
+
response: empty('设置成功,返回空对象')
|
|
80
106
|
}
|
|
81
107
|
},
|
|
82
108
|
async request => {
|
|
@@ -90,8 +116,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
90
116
|
{
|
|
91
117
|
onRequest: getAuth('langLib:manage'),
|
|
92
118
|
schema: {
|
|
119
|
+
tags: [TAG],
|
|
93
120
|
summary: '审核语言词条',
|
|
94
|
-
|
|
121
|
+
description: '按 id 或 ids 批量审核词条;只有审核通过的词条会出现在运行时语言包中',
|
|
122
|
+
body: schema.reviewBody,
|
|
123
|
+
response: empty('审核成功,返回空对象')
|
|
95
124
|
}
|
|
96
125
|
},
|
|
97
126
|
async request => {
|
|
@@ -105,8 +134,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
105
134
|
{
|
|
106
135
|
onRequest: getAuth('langLib:manage'),
|
|
107
136
|
schema: {
|
|
137
|
+
tags: [TAG],
|
|
108
138
|
summary: '重新生成待审核词条译文',
|
|
109
|
-
|
|
139
|
+
description: '对待审核或已通过的非默认语言词条,按默认语言源文重新调用 generateTargets 生成译文,结果重置为待审核;未配置 generateTargets、缺少源文或包含默认语言词条时报错',
|
|
140
|
+
body: schema.regenerateBody,
|
|
141
|
+
response: empty('重新生成成功,返回空对象')
|
|
110
142
|
}
|
|
111
143
|
},
|
|
112
144
|
async request => {
|
|
@@ -120,8 +152,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
120
152
|
{
|
|
121
153
|
onRequest: getAuth('langLib:manage'),
|
|
122
154
|
schema: {
|
|
155
|
+
tags: [TAG],
|
|
123
156
|
summary: '复制词条到新命名空间',
|
|
124
|
-
|
|
157
|
+
description: '把选中的词条(含译文、审核状态、启用状态)复制到目标命名空间;目标下已存在同编码同语言的词条会被跳过,全部跳过时报错',
|
|
158
|
+
body: schema.copyToNamespaceBody,
|
|
159
|
+
response: schema.copyToNamespaceResponse
|
|
125
160
|
}
|
|
126
161
|
},
|
|
127
162
|
async request => services.langLib.copyToNamespace(request.userInfo || {}, request.body)
|
|
@@ -132,8 +167,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
132
167
|
{
|
|
133
168
|
// 运行时语言包:允许未登录访问
|
|
134
169
|
schema: {
|
|
170
|
+
tags: [RUNTIME_TAG],
|
|
135
171
|
summary: '运行时语言包(扁平 code→文案,无需登录)',
|
|
136
|
-
|
|
172
|
+
description: '前端运行时拉取语言包,返回 { code: 译文 },可直接给 @kne/react-intl 使用;只包含已启用且审核通过的词条,无需登录',
|
|
173
|
+
query: schema.messagesQuery,
|
|
174
|
+
response: schema.messagesResponse
|
|
137
175
|
}
|
|
138
176
|
},
|
|
139
177
|
async request => services.langLib.messages(request.userInfo || {}, request.query)
|
|
@@ -144,8 +182,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
144
182
|
{
|
|
145
183
|
onRequest: getAuth('langLib'),
|
|
146
184
|
schema: {
|
|
185
|
+
tags: [TAG],
|
|
147
186
|
summary: '导出语言库 i18n 文件',
|
|
148
|
-
|
|
187
|
+
description: '按命名空间 + 语言分组导出 i18n 文件内容(JSON 返回文件名与正文,不是文件下载),包含全部状态的词条',
|
|
188
|
+
query: schema.exportQuery,
|
|
189
|
+
response: schema.exportResponse
|
|
149
190
|
}
|
|
150
191
|
},
|
|
151
192
|
async request => services.langLib.exportI18n(request.userInfo || {}, request.query)
|
|
@@ -156,8 +197,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
156
197
|
{
|
|
157
198
|
onRequest: getAuth('langLib:manage'),
|
|
158
199
|
schema: {
|
|
200
|
+
tags: [TAG],
|
|
159
201
|
summary: '导入语言库 i18n 文件',
|
|
160
|
-
|
|
202
|
+
description: '导入一个或多个 i18n 文件;已存在的词条覆盖译文,新词条直接创建,导入的词条均为启用且审核通过',
|
|
203
|
+
body: schema.importBody,
|
|
204
|
+
response: schema.importResponse
|
|
161
205
|
}
|
|
162
206
|
},
|
|
163
207
|
async request => services.langLib.importI18n(request.userInfo || {}, request.body)
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
const fp = require('fastify-plugin');
|
|
2
|
+
const pick = require('lodash/pick');
|
|
2
3
|
const schema = require('../schema/lang-type');
|
|
4
|
+
const { empty } = require('../schema/response');
|
|
5
|
+
|
|
6
|
+
const TAG = '国际化管理后台-语言类型';
|
|
7
|
+
|
|
8
|
+
// save 只透传客户端可修改的字段,避免 userId、deletedAt 等未声明字段写进数据库
|
|
9
|
+
const SAVE_FIELDS = ['id', 'name', 'params', 'description', 'status'];
|
|
3
10
|
|
|
4
11
|
module.exports = fp(async (fastify, options) => {
|
|
5
12
|
const { services } = fastify[options.name];
|
|
@@ -10,8 +17,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
10
17
|
{
|
|
11
18
|
onRequest: getAuth('langType'),
|
|
12
19
|
schema: {
|
|
20
|
+
tags: [TAG],
|
|
13
21
|
summary: '语言类型列表',
|
|
14
|
-
|
|
22
|
+
description: '分页查询语言类型,附带每种语言的词条数与默认语言词条数,可用于展示翻译完成度',
|
|
23
|
+
query: schema.listQuery,
|
|
24
|
+
response: schema.listResponse
|
|
15
25
|
}
|
|
16
26
|
},
|
|
17
27
|
async request => services.langType.list(request.userInfo || {}, request.query)
|
|
@@ -22,8 +32,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
22
32
|
{
|
|
23
33
|
onRequest: getAuth('langType:manage'),
|
|
24
34
|
schema: {
|
|
35
|
+
tags: [TAG],
|
|
25
36
|
summary: '创建语言类型',
|
|
26
|
-
|
|
37
|
+
description: '新增一种语言;语言编码唯一且创建后不可修改,系统中第一条语言自动成为默认语言',
|
|
38
|
+
body: schema.createBody,
|
|
39
|
+
response: schema.createResponse
|
|
27
40
|
}
|
|
28
41
|
},
|
|
29
42
|
async request => services.langType.create(request.userInfo || {}, request.body)
|
|
@@ -34,12 +47,15 @@ module.exports = fp(async (fastify, options) => {
|
|
|
34
47
|
{
|
|
35
48
|
onRequest: getAuth('langType:manage'),
|
|
36
49
|
schema: {
|
|
50
|
+
tags: [TAG],
|
|
37
51
|
summary: '保存语言类型',
|
|
38
|
-
|
|
52
|
+
description: '修改语言名称、参数提示、描述、状态;语言编码、默认标记和排序不能通过此接口修改,未声明的字段(如 userId)会被忽略',
|
|
53
|
+
body: schema.saveBody,
|
|
54
|
+
response: empty('保存成功,返回空对象')
|
|
39
55
|
}
|
|
40
56
|
},
|
|
41
57
|
async request => {
|
|
42
|
-
await services.langType.save(request.userInfo || {}, request.body);
|
|
58
|
+
await services.langType.save(request.userInfo || {}, pick(request.body, SAVE_FIELDS));
|
|
43
59
|
return {};
|
|
44
60
|
}
|
|
45
61
|
);
|
|
@@ -49,8 +65,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
49
65
|
{
|
|
50
66
|
onRequest: getAuth('langType:manage'),
|
|
51
67
|
schema: {
|
|
68
|
+
tags: [TAG],
|
|
52
69
|
summary: '删除语言类型',
|
|
53
|
-
|
|
70
|
+
description: '删除语言类型;语言库中仍有该语言的词条时报错。删除的是默认语言时,最早创建的剩余语言自动成为默认',
|
|
71
|
+
body: schema.removeBody,
|
|
72
|
+
response: empty('删除成功,返回空对象')
|
|
54
73
|
}
|
|
55
74
|
},
|
|
56
75
|
async request => {
|
|
@@ -64,8 +83,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
64
83
|
{
|
|
65
84
|
onRequest: getAuth('langType:manage'),
|
|
66
85
|
schema: {
|
|
86
|
+
tags: [TAG],
|
|
67
87
|
summary: '设置语言类型状态',
|
|
68
|
-
|
|
88
|
+
description: '启用或禁用语言;禁用的语言不能新增词条,也不能补译',
|
|
89
|
+
body: schema.setStatusBody,
|
|
90
|
+
response: empty('设置成功,返回空对象')
|
|
69
91
|
}
|
|
70
92
|
},
|
|
71
93
|
async request => {
|
|
@@ -79,8 +101,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
79
101
|
{
|
|
80
102
|
onRequest: getAuth('langType:manage'),
|
|
81
103
|
schema: {
|
|
104
|
+
tags: [TAG],
|
|
82
105
|
summary: '设置默认语言',
|
|
83
|
-
|
|
106
|
+
description: '把指定语言设为默认语言,并清除其他语言的默认标记;默认语言是 AI 翻译与补译的源语言',
|
|
107
|
+
body: schema.setDefaultBody,
|
|
108
|
+
response: empty('设置成功,返回空对象')
|
|
84
109
|
}
|
|
85
110
|
},
|
|
86
111
|
async request => {
|
|
@@ -94,8 +119,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
94
119
|
{
|
|
95
120
|
onRequest: getAuth('langType:manage'),
|
|
96
121
|
schema: {
|
|
122
|
+
tags: [TAG],
|
|
97
123
|
summary: '语言类型上移/下移',
|
|
98
|
-
|
|
124
|
+
description: '与相邻语言交换排序位置;已在首位上移或末位下移时不做修改',
|
|
125
|
+
body: schema.moveBody,
|
|
126
|
+
response: schema.moveResponse
|
|
99
127
|
}
|
|
100
128
|
},
|
|
101
129
|
async request => services.langType.move(request.userInfo || {}, request.body)
|
|
@@ -106,8 +134,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
106
134
|
{
|
|
107
135
|
onRequest: getAuth('langType:manage'),
|
|
108
136
|
schema: {
|
|
137
|
+
tags: [TAG],
|
|
109
138
|
summary: '补译目标语言缺失词条',
|
|
110
|
-
|
|
139
|
+
description: '为目标语言补齐默认语言已有但目标语言缺失的词条:非 global 命名空间优先复用 global 同编码的目标语言译文(直接审核通过),其余一次性调用 generateTargets 生成译文(待审核)。未配置 generateTargets 且需要 AI 翻译时报错',
|
|
140
|
+
body: schema.translateRemainingBody,
|
|
141
|
+
response: schema.translateRemainingResponse
|
|
111
142
|
}
|
|
112
143
|
},
|
|
113
144
|
async request => services.langType.translateRemaining(request.userInfo || {}, request.body)
|
package/libs/schema/lang-lib.js
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
|
+
const { obj, arr, str, num, success, langLib, langLibListItem, importResultFields } = require('./response');
|
|
2
|
+
|
|
3
|
+
const ID = { description: '单个词条 ID;与 ids 二选一,同时传时以非空的 ids 为准' };
|
|
4
|
+
const IDS = {
|
|
5
|
+
type: 'array',
|
|
6
|
+
items: { description: '词条 ID' },
|
|
7
|
+
description: '批量操作的词条 ID 列表;非空时忽略 id'
|
|
8
|
+
};
|
|
9
|
+
|
|
1
10
|
const listQuery = {
|
|
2
11
|
type: 'object',
|
|
3
12
|
properties: {
|
|
4
|
-
filter: { type: 'object', default: {} },
|
|
5
|
-
keyword: { type: 'string' },
|
|
6
|
-
namespace: { type: 'string' },
|
|
7
|
-
locale: { type: 'string' },
|
|
8
|
-
reviewStatus: { type: 'string', enum: ['approved', 'pending', 'rejected'] },
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
filter: { type: 'object', default: {}, description: '筛选条件对象,可含 keyword、namespace、locale、reviewStatus、status、code;与顶层同名字段同时传时以 filter 为准' },
|
|
14
|
+
keyword: { type: 'string', description: '关键字,模糊匹配命名空间、词条编码、译文' },
|
|
15
|
+
namespace: { type: 'string', description: '按命名空间精确筛选' },
|
|
16
|
+
locale: { type: 'string', description: '按语言编码精确筛选' },
|
|
17
|
+
reviewStatus: { type: 'string', enum: ['approved', 'pending', 'rejected'], description: '按审核状态筛选:approved 已通过,pending 待审核,rejected 已驳回' },
|
|
18
|
+
status: { description: '按状态筛选:open 启用,closed 禁用' },
|
|
19
|
+
code: { description: '按词条编码精确筛选' },
|
|
20
|
+
perPage: { type: 'number', default: 20, description: '每页条数,非正数按 20 处理' },
|
|
21
|
+
currentPage: { type: 'number', default: 1, description: '页码,从 1 开始,非正数按 1 处理' }
|
|
11
22
|
}
|
|
12
23
|
};
|
|
13
24
|
|
|
@@ -15,17 +26,22 @@ const addPackageBody = {
|
|
|
15
26
|
type: 'object',
|
|
16
27
|
required: ['namespace', 'code', 'locales'],
|
|
17
28
|
properties: {
|
|
18
|
-
namespace: { type: 'string' },
|
|
19
|
-
code: { type: 'string' },
|
|
29
|
+
namespace: { type: 'string', description: '命名空间,去除首尾空白后不能为空' },
|
|
30
|
+
code: { type: 'string', description: '词条编码,去除首尾空白后不能为空' },
|
|
20
31
|
locales: {
|
|
21
32
|
type: 'array',
|
|
22
33
|
minItems: 1,
|
|
23
|
-
items: { type: 'string' }
|
|
34
|
+
items: { type: 'string', description: '语言编码' },
|
|
35
|
+
description: '要创建词条的语言编码列表,须均为已启用的语言,且同命名空间同编码下不能已存在'
|
|
24
36
|
},
|
|
25
|
-
aiGenerate: { type: 'boolean', default: false },
|
|
26
|
-
sourceTarget: { type: 'string' },
|
|
27
|
-
defaultLocale: { type: 'string' },
|
|
28
|
-
targets: {
|
|
37
|
+
aiGenerate: { type: 'boolean', default: false, description: '是否调用 generateTargets 用 AI 生成非默认语言译文;AI 生成的词条为待审核' },
|
|
38
|
+
sourceTarget: { type: 'string', description: '默认语言源文;aiGenerate 为 true 时必填,作为默认语言译文并作为 AI 翻译源' },
|
|
39
|
+
defaultLocale: { type: 'string', description: '默认语言编码;不传使用系统默认语言' },
|
|
40
|
+
targets: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
additionalProperties: { type: 'string' },
|
|
43
|
+
description: '手动填写的译文,键为语言编码、值为译文;aiGenerate 为 true 时忽略'
|
|
44
|
+
}
|
|
29
45
|
}
|
|
30
46
|
};
|
|
31
47
|
|
|
@@ -33,23 +49,20 @@ const saveBody = {
|
|
|
33
49
|
type: 'object',
|
|
34
50
|
required: ['id'],
|
|
35
51
|
properties: {
|
|
36
|
-
id: {},
|
|
37
|
-
target: { type: 'string' },
|
|
38
|
-
namespace: { type: 'string' },
|
|
39
|
-
code: { type: 'string' },
|
|
40
|
-
locale: {},
|
|
41
|
-
status: { type: 'string', enum: ['open', 'closed'] }
|
|
52
|
+
id: { description: '词条 ID' },
|
|
53
|
+
target: { type: 'string', description: '译文;不传保持原值。译文变化时默认语言词条直接审核通过,其他语言重置为待审核' },
|
|
54
|
+
namespace: { type: 'string', description: '命名空间,不可修改,传入会被忽略' },
|
|
55
|
+
code: { type: 'string', description: '词条编码,不可修改,传入会被忽略' },
|
|
56
|
+
locale: { description: '语言编码,不可修改,传入会被忽略' },
|
|
57
|
+
status: { type: 'string', enum: ['open', 'closed'], description: '状态:open 启用,closed 禁用;不传保持原值' }
|
|
42
58
|
}
|
|
43
59
|
};
|
|
44
60
|
|
|
45
61
|
const removeBody = {
|
|
46
62
|
type: 'object',
|
|
47
63
|
properties: {
|
|
48
|
-
id:
|
|
49
|
-
ids:
|
|
50
|
-
type: 'array',
|
|
51
|
-
items: {}
|
|
52
|
-
}
|
|
64
|
+
id: ID,
|
|
65
|
+
ids: IDS
|
|
53
66
|
}
|
|
54
67
|
};
|
|
55
68
|
|
|
@@ -57,12 +70,9 @@ const setStatusBody = {
|
|
|
57
70
|
type: 'object',
|
|
58
71
|
required: ['status'],
|
|
59
72
|
properties: {
|
|
60
|
-
id:
|
|
61
|
-
ids:
|
|
62
|
-
|
|
63
|
-
items: {}
|
|
64
|
-
},
|
|
65
|
-
status: { type: 'string', enum: ['open', 'closed'] }
|
|
73
|
+
id: ID,
|
|
74
|
+
ids: IDS,
|
|
75
|
+
status: { type: 'string', enum: ['open', 'closed'], description: '目标状态:open 启用,closed 禁用' }
|
|
66
76
|
}
|
|
67
77
|
};
|
|
68
78
|
|
|
@@ -70,24 +80,18 @@ const reviewBody = {
|
|
|
70
80
|
type: 'object',
|
|
71
81
|
required: ['reviewStatus'],
|
|
72
82
|
properties: {
|
|
73
|
-
id:
|
|
74
|
-
ids:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
},
|
|
78
|
-
reviewStatus: { type: 'string', enum: ['approved', 'rejected'] },
|
|
79
|
-
reviewRemark: { type: 'string', maxLength: 500 }
|
|
83
|
+
id: ID,
|
|
84
|
+
ids: IDS,
|
|
85
|
+
reviewStatus: { type: 'string', enum: ['approved', 'rejected'], description: '审核结果:approved 通过,rejected 驳回' },
|
|
86
|
+
reviewRemark: { type: 'string', maxLength: 500, description: '审核备注;不传保持原备注' }
|
|
80
87
|
}
|
|
81
88
|
};
|
|
82
89
|
|
|
83
90
|
const regenerateBody = {
|
|
84
91
|
type: 'object',
|
|
85
92
|
properties: {
|
|
86
|
-
id:
|
|
87
|
-
ids:
|
|
88
|
-
type: 'array',
|
|
89
|
-
items: {}
|
|
90
|
-
}
|
|
93
|
+
id: ID,
|
|
94
|
+
ids: IDS
|
|
91
95
|
}
|
|
92
96
|
};
|
|
93
97
|
|
|
@@ -95,12 +99,9 @@ const copyToNamespaceBody = {
|
|
|
95
99
|
type: 'object',
|
|
96
100
|
required: ['namespace'],
|
|
97
101
|
properties: {
|
|
98
|
-
namespace: { type: 'string' },
|
|
99
|
-
id:
|
|
100
|
-
ids:
|
|
101
|
-
type: 'array',
|
|
102
|
-
items: {}
|
|
103
|
-
}
|
|
102
|
+
namespace: { type: 'string', description: '目标命名空间;与源命名空间相同或目标下已存在同编码同语言的词条会被跳过' },
|
|
103
|
+
id: ID,
|
|
104
|
+
ids: IDS
|
|
104
105
|
}
|
|
105
106
|
};
|
|
106
107
|
|
|
@@ -108,40 +109,111 @@ const messagesQuery = {
|
|
|
108
109
|
type: 'object',
|
|
109
110
|
required: ['locale'],
|
|
110
111
|
properties: {
|
|
111
|
-
locale: { type: 'string' },
|
|
112
|
-
namespace: { type: 'string', default: 'global' }
|
|
112
|
+
locale: { type: 'string', description: '语言编码,如 zh-CN' },
|
|
113
|
+
namespace: { type: 'string', default: 'global', description: '命名空间,默认 global' }
|
|
113
114
|
}
|
|
114
115
|
};
|
|
115
116
|
|
|
116
117
|
const exportQuery = {
|
|
117
118
|
type: 'object',
|
|
118
119
|
properties: {
|
|
119
|
-
namespace: { type: 'string' }
|
|
120
|
+
namespace: { type: 'string', description: '只导出该命名空间;不传导出全部' }
|
|
120
121
|
}
|
|
121
122
|
};
|
|
122
123
|
|
|
123
124
|
const importFileItem = {
|
|
124
125
|
type: 'object',
|
|
125
126
|
required: ['filename', 'content'],
|
|
127
|
+
description: 'i18n 文件',
|
|
126
128
|
properties: {
|
|
127
|
-
filename: { type: 'string' },
|
|
128
|
-
content: { type: 'string' }
|
|
129
|
+
filename: { type: 'string', description: '文件名:namespace.locale.i18n,或 locale.i18n(命名空间为 global)' },
|
|
130
|
+
content: { type: 'string', description: '文件内容,每行 code="target",译文支持 \\" 与 \\\\ 转义,空行忽略' }
|
|
129
131
|
}
|
|
130
132
|
};
|
|
131
133
|
|
|
132
134
|
const importBody = {
|
|
133
135
|
type: 'object',
|
|
134
136
|
properties: {
|
|
135
|
-
filename: { type: 'string' },
|
|
136
|
-
content: { type: 'string' },
|
|
137
|
+
filename: { type: 'string', description: '单文件导入的文件名,格式同 files[].filename;传了非空 files 时忽略' },
|
|
138
|
+
content: { type: 'string', description: '单文件导入的文件内容,格式同 files[].content' },
|
|
137
139
|
files: {
|
|
138
140
|
type: 'array',
|
|
139
141
|
minItems: 1,
|
|
140
|
-
items: importFileItem
|
|
142
|
+
items: importFileItem,
|
|
143
|
+
description: '批量导入的文件列表'
|
|
141
144
|
}
|
|
142
145
|
}
|
|
143
146
|
};
|
|
144
147
|
|
|
148
|
+
const listResponse = success('语言词条分页列表,按命名空间、编码、语言升序', {
|
|
149
|
+
type: 'object',
|
|
150
|
+
additionalProperties: true,
|
|
151
|
+
properties: {
|
|
152
|
+
pageData: arr('当前页词条', langLibListItem),
|
|
153
|
+
totalCount: num('符合条件的词条总数')
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const namespacesResponse = success('全部命名空间,global 排在最前,其余按名称排序', {
|
|
158
|
+
type: 'object',
|
|
159
|
+
additionalProperties: true,
|
|
160
|
+
properties: {
|
|
161
|
+
pageData: arr('命名空间列表', obj('命名空间', { namespace: str('命名空间名称') })),
|
|
162
|
+
totalCount: num('命名空间数量')
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const addPackageResponse = success('新建的词条列表,每种语言一条', arr(undefined, langLib));
|
|
167
|
+
|
|
168
|
+
const copyToNamespaceResponse = success(
|
|
169
|
+
'复制结果统计',
|
|
170
|
+
obj(undefined, {
|
|
171
|
+
createdCount: num('新建的词条数'),
|
|
172
|
+
skippedCount: num('跳过的词条数(源命名空间与目标相同,或目标下已存在)')
|
|
173
|
+
})
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const messagesResponse = success('扁平语言包:键为词条编码,值为译文;只包含已启用且审核通过的词条', {
|
|
177
|
+
type: 'object',
|
|
178
|
+
additionalProperties: true
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const exportResponse = success(
|
|
182
|
+
'导出的 i18n 文件列表,按命名空间 + 语言分组',
|
|
183
|
+
obj(undefined, {
|
|
184
|
+
pageData: arr(
|
|
185
|
+
'i18n 文件',
|
|
186
|
+
obj('i18n 文件', {
|
|
187
|
+
filename: str('文件名:namespace.locale.i18n'),
|
|
188
|
+
content: str('文件内容,每行 code="target"')
|
|
189
|
+
})
|
|
190
|
+
)
|
|
191
|
+
})
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const importResponse = success(
|
|
195
|
+
'导入结果统计;已存在的词条会被覆盖并直接审核通过',
|
|
196
|
+
obj(
|
|
197
|
+
undefined,
|
|
198
|
+
Object.assign({}, importResultFields, {
|
|
199
|
+
pageData: arr(
|
|
200
|
+
'每个文件的导入结果',
|
|
201
|
+
obj(
|
|
202
|
+
'单个文件导入结果',
|
|
203
|
+
Object.assign(
|
|
204
|
+
{
|
|
205
|
+
filename: str('文件名'),
|
|
206
|
+
namespace: str('解析出的命名空间'),
|
|
207
|
+
locale: str('解析出的语言编码')
|
|
208
|
+
},
|
|
209
|
+
importResultFields
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
})
|
|
214
|
+
)
|
|
215
|
+
);
|
|
216
|
+
|
|
145
217
|
module.exports = {
|
|
146
218
|
listQuery,
|
|
147
219
|
addPackageBody,
|
|
@@ -153,5 +225,12 @@ module.exports = {
|
|
|
153
225
|
copyToNamespaceBody,
|
|
154
226
|
messagesQuery,
|
|
155
227
|
exportQuery,
|
|
156
|
-
importBody
|
|
228
|
+
importBody,
|
|
229
|
+
listResponse,
|
|
230
|
+
namespacesResponse,
|
|
231
|
+
addPackageResponse,
|
|
232
|
+
copyToNamespaceResponse,
|
|
233
|
+
messagesResponse,
|
|
234
|
+
exportResponse,
|
|
235
|
+
importResponse
|
|
157
236
|
};
|
package/libs/schema/lang-type.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
const { obj, arr, num, bool, success, langType, langTypeListItem } = require('./response');
|
|
2
|
+
|
|
3
|
+
const ID = { description: '语言类型 ID' };
|
|
4
|
+
|
|
1
5
|
const listQuery = {
|
|
2
6
|
type: 'object',
|
|
3
7
|
properties: {
|
|
4
|
-
filter: { type: 'object', default: {} },
|
|
5
|
-
keyword: { type: 'string' },
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
filter: { type: 'object', default: {}, description: '筛选条件对象,可含 keyword、status、isDefault、namespace;与顶层同名字段同时传时以 filter 为准' },
|
|
9
|
+
keyword: { type: 'string', description: '关键字,模糊匹配语言名称、编码、描述' },
|
|
10
|
+
status: { description: '按状态筛选:open 启用,closed 禁用' },
|
|
11
|
+
namespace: { type: 'string', description: '命名空间;只影响 entryCount / defaultEntryCount 的统计范围,不过滤语言类型' },
|
|
12
|
+
isDefault: { type: 'boolean', description: '按是否默认语言筛选' },
|
|
13
|
+
perPage: { type: 'number', default: 20, description: '每页条数,非正数按 20 处理' },
|
|
14
|
+
currentPage: { type: 'number', default: 1, description: '页码,从 1 开始,非正数按 1 处理' }
|
|
10
15
|
}
|
|
11
16
|
};
|
|
12
17
|
|
|
@@ -14,11 +19,11 @@ const createBody = {
|
|
|
14
19
|
type: 'object',
|
|
15
20
|
required: ['name', 'code'],
|
|
16
21
|
properties: {
|
|
17
|
-
name: { type: 'string' },
|
|
18
|
-
code: { type: 'string' },
|
|
19
|
-
params: { type: 'string' },
|
|
20
|
-
description: { type: 'string' },
|
|
21
|
-
status: { type: 'string', enum: ['open', 'closed'] }
|
|
22
|
+
name: { type: 'string', description: '语言名称,如 中文(简体)' },
|
|
23
|
+
code: { type: 'string', description: '语言编码,如 zh-CN;去除首尾空白后不能为空且不能与已有语言重复,创建后不可修改' },
|
|
24
|
+
params: { type: 'string', description: '翻译参数提示,供 AI 翻译时参考' },
|
|
25
|
+
description: { type: 'string', description: '描述' },
|
|
26
|
+
status: { type: 'string', enum: ['open', 'closed'], description: '状态:open 启用,closed 禁用;不传为 open' }
|
|
22
27
|
}
|
|
23
28
|
};
|
|
24
29
|
|
|
@@ -26,11 +31,11 @@ const saveBody = {
|
|
|
26
31
|
type: 'object',
|
|
27
32
|
required: ['id'],
|
|
28
33
|
properties: {
|
|
29
|
-
id:
|
|
30
|
-
name: { type: 'string' },
|
|
31
|
-
params: { type: 'string' },
|
|
32
|
-
description: { type: 'string' },
|
|
33
|
-
status: { type: 'string', enum: ['open', 'closed'] }
|
|
34
|
+
id: ID,
|
|
35
|
+
name: { type: 'string', description: '语言名称;不传保持原值' },
|
|
36
|
+
params: { type: 'string', description: '翻译参数提示;不传保持原值' },
|
|
37
|
+
description: { type: 'string', description: '描述;不传保持原值' },
|
|
38
|
+
status: { type: 'string', enum: ['open', 'closed'], description: '状态:open 启用,closed 禁用;不传保持原值' }
|
|
34
39
|
}
|
|
35
40
|
};
|
|
36
41
|
|
|
@@ -38,7 +43,7 @@ const removeBody = {
|
|
|
38
43
|
type: 'object',
|
|
39
44
|
required: ['id'],
|
|
40
45
|
properties: {
|
|
41
|
-
id: {}
|
|
46
|
+
id: { description: '语言类型 ID;仍有关联词条时不能删除,删除默认语言后最早创建的语言自动成为默认' }
|
|
42
47
|
}
|
|
43
48
|
};
|
|
44
49
|
|
|
@@ -46,8 +51,8 @@ const setStatusBody = {
|
|
|
46
51
|
type: 'object',
|
|
47
52
|
required: ['id', 'status'],
|
|
48
53
|
properties: {
|
|
49
|
-
id:
|
|
50
|
-
status: { type: 'string', enum: ['open', 'closed'] }
|
|
54
|
+
id: ID,
|
|
55
|
+
status: { type: 'string', enum: ['open', 'closed'], description: '目标状态:open 启用,closed 禁用' }
|
|
51
56
|
}
|
|
52
57
|
};
|
|
53
58
|
|
|
@@ -55,7 +60,7 @@ const setDefaultBody = {
|
|
|
55
60
|
type: 'object',
|
|
56
61
|
required: ['id'],
|
|
57
62
|
properties: {
|
|
58
|
-
id: {}
|
|
63
|
+
id: { description: '要设为默认的语言类型 ID;其他语言的默认标记会被清除' }
|
|
59
64
|
}
|
|
60
65
|
};
|
|
61
66
|
|
|
@@ -63,8 +68,8 @@ const moveBody = {
|
|
|
63
68
|
type: 'object',
|
|
64
69
|
required: ['id', 'direction'],
|
|
65
70
|
properties: {
|
|
66
|
-
id:
|
|
67
|
-
direction: { type: 'string', enum: ['up', 'down'] }
|
|
71
|
+
id: ID,
|
|
72
|
+
direction: { type: 'string', enum: ['up', 'down'], description: '移动方向:up 上移,down 下移(与相邻项交换排序)' }
|
|
68
73
|
}
|
|
69
74
|
};
|
|
70
75
|
|
|
@@ -72,12 +77,37 @@ const translateRemainingBody = {
|
|
|
72
77
|
type: 'object',
|
|
73
78
|
required: ['id'],
|
|
74
79
|
properties: {
|
|
75
|
-
id: {},
|
|
76
|
-
namespace: { type: 'string' },
|
|
77
|
-
limit: { type: 'integer', minimum: 1, maximum: 500 }
|
|
80
|
+
id: { description: '目标语言类型 ID,须为已启用的非默认语言' },
|
|
81
|
+
namespace: { type: 'string', description: '只补译该命名空间的缺失词条;不传补译全部命名空间' },
|
|
82
|
+
limit: { type: 'integer', minimum: 1, maximum: 500, description: '本次最多处理的词条数,用于分批补译;不传一次处理全部' }
|
|
78
83
|
}
|
|
79
84
|
};
|
|
80
85
|
|
|
86
|
+
const listResponse = success('语言类型分页列表,按排序、创建时间升序', {
|
|
87
|
+
type: 'object',
|
|
88
|
+
additionalProperties: true,
|
|
89
|
+
properties: {
|
|
90
|
+
pageData: arr('当前页语言类型', langTypeListItem),
|
|
91
|
+
totalCount: num('符合条件的语言类型总数')
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const createResponse = success('新建的语言类型;系统第一条语言自动设为默认,新建语言排在最后', langType);
|
|
96
|
+
|
|
97
|
+
const moveResponse = success('移动结果', obj(undefined, { moved: bool('是否发生了移动;已在首位上移或末位下移时为 false') }));
|
|
98
|
+
|
|
99
|
+
const translateRemainingResponse = success(
|
|
100
|
+
'补译结果统计',
|
|
101
|
+
obj(undefined, {
|
|
102
|
+
createdCount: num('本次新建的目标语言词条数'),
|
|
103
|
+
copiedCount: num('非 global 命名空间词条直接复用 global 同编码译文的条数(直接审核通过)'),
|
|
104
|
+
generatedCount: num('调用 generateTargets 生成译文的条数(待审核)'),
|
|
105
|
+
remainingCount: num('因 limit 限制本次未处理、仍可继续补译的条数'),
|
|
106
|
+
remainingSkipped: num('默认语言源文为空而跳过的缺失词条数'),
|
|
107
|
+
totalProcessable: num('本次调用前可补译的缺失词条总数')
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
|
|
81
111
|
module.exports = {
|
|
82
112
|
listQuery,
|
|
83
113
|
createBody,
|
|
@@ -86,5 +116,9 @@ module.exports = {
|
|
|
86
116
|
setStatusBody,
|
|
87
117
|
setDefaultBody,
|
|
88
118
|
moveBody,
|
|
89
|
-
translateRemainingBody
|
|
119
|
+
translateRemainingBody,
|
|
120
|
+
listResponse,
|
|
121
|
+
createResponse,
|
|
122
|
+
moveResponse,
|
|
123
|
+
translateRemainingResponse
|
|
90
124
|
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// 响应 schema 只用于 OpenAPI 文档:字段可空、对象允许额外字段,避免序列化改变实际输出
|
|
2
|
+
const str = description => ({ type: 'string', nullable: true, description });
|
|
3
|
+
const num = description => ({ type: 'number', nullable: true, description });
|
|
4
|
+
const bool = description => ({ type: 'boolean', nullable: true, description });
|
|
5
|
+
// ID(BIGINT)、外键与日期可能是 number / string / Date,不声明类型,原样输出
|
|
6
|
+
const any = description => ({ description });
|
|
7
|
+
const obj = (description, properties) => ({ type: 'object', nullable: true, additionalProperties: true, description, properties });
|
|
8
|
+
const arr = (description, items) => ({ type: 'array', items, description });
|
|
9
|
+
|
|
10
|
+
const success = (description, schema) => ({ 200: Object.assign({}, schema || { type: 'object', additionalProperties: true }, { description }) });
|
|
11
|
+
const empty = description => success(description || '操作成功,返回空对象');
|
|
12
|
+
|
|
13
|
+
const STATUS = '状态:open 启用,closed 禁用';
|
|
14
|
+
const REVIEW_STATUS = '审核状态:approved 已通过,pending 待审核,rejected 已驳回';
|
|
15
|
+
|
|
16
|
+
const createdUser = obj('添加人(账号插件的用户记录)');
|
|
17
|
+
|
|
18
|
+
const langTypeFields = {
|
|
19
|
+
id: any('语言类型 ID'),
|
|
20
|
+
name: str('语言名称'),
|
|
21
|
+
code: str('语言编码,如 zh-CN'),
|
|
22
|
+
params: str('翻译参数提示'),
|
|
23
|
+
description: str('描述'),
|
|
24
|
+
status: str(STATUS),
|
|
25
|
+
isDefault: bool('是否为默认语言(全局唯一)'),
|
|
26
|
+
sort: num('排序,越小越靠前'),
|
|
27
|
+
userId: any('添加人用户 ID'),
|
|
28
|
+
createdAt: any('创建时间'),
|
|
29
|
+
updatedAt: any('更新时间'),
|
|
30
|
+
deletedAt: any('删除时间,未删除为 null')
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const langType = obj('语言类型', langTypeFields);
|
|
34
|
+
|
|
35
|
+
const langTypeListItem = obj(
|
|
36
|
+
'语言类型(含词条统计)',
|
|
37
|
+
Object.assign({}, langTypeFields, {
|
|
38
|
+
createdUser,
|
|
39
|
+
entryCount: num('该语言的词条数(传 namespace 时只统计该命名空间)'),
|
|
40
|
+
defaultEntryCount: num('默认语言的词条数(统计口径同 entryCount),用于计算翻译完成度')
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const langLibFields = {
|
|
45
|
+
id: any('词条 ID'),
|
|
46
|
+
namespace: str('命名空间'),
|
|
47
|
+
code: str('词条编码'),
|
|
48
|
+
locale: str('语言编码'),
|
|
49
|
+
target: str('译文'),
|
|
50
|
+
reviewStatus: str(REVIEW_STATUS),
|
|
51
|
+
reviewRemark: str('审核备注'),
|
|
52
|
+
status: str(STATUS),
|
|
53
|
+
userId: any('添加人用户 ID'),
|
|
54
|
+
createdAt: any('创建时间'),
|
|
55
|
+
updatedAt: any('更新时间'),
|
|
56
|
+
deletedAt: any('删除时间,未删除为 null')
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const langLib = obj('语言词条', langLibFields);
|
|
60
|
+
|
|
61
|
+
const langLibListItem = obj(
|
|
62
|
+
'语言词条(含默认语言源文)',
|
|
63
|
+
Object.assign({}, langLibFields, {
|
|
64
|
+
createdUser,
|
|
65
|
+
sourceTarget: str('同命名空间、同编码的默认语言译文;当前词条即默认语言时为自身译文,未设置默认语言或无源文时为空字符串'),
|
|
66
|
+
defaultLocale: str('当前默认语言编码,未设置时为空字符串')
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const importResultFields = {
|
|
71
|
+
createdCount: num('新增词条数'),
|
|
72
|
+
updatedCount: num('覆盖更新的已有词条数'),
|
|
73
|
+
skippedCount: num('跳过的词条数')
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
str,
|
|
78
|
+
num,
|
|
79
|
+
bool,
|
|
80
|
+
any,
|
|
81
|
+
obj,
|
|
82
|
+
arr,
|
|
83
|
+
success,
|
|
84
|
+
empty,
|
|
85
|
+
STATUS,
|
|
86
|
+
REVIEW_STATUS,
|
|
87
|
+
langType,
|
|
88
|
+
langTypeListItem,
|
|
89
|
+
langLib,
|
|
90
|
+
langLibListItem,
|
|
91
|
+
importResultFields
|
|
92
|
+
};
|