@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.
@@ -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
- query: schema.listQuery
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
- summary: '语言库命名空间列表'
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
- body: schema.addPackageBody
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
- body: schema.saveBody
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
- body: schema.removeBody
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
- body: schema.setStatusBody
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
- body: schema.reviewBody
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
- body: schema.regenerateBody
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
- body: schema.copyToNamespaceBody
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
- query: schema.messagesQuery
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
- query: schema.exportQuery
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
- body: schema.importBody
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
- query: schema.listQuery
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
- body: schema.createBody
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
- body: schema.saveBody
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
- body: schema.removeBody
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
- body: schema.setStatusBody
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
- body: schema.setDefaultBody
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
- body: schema.moveBody
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
- body: schema.translateRemainingBody
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)
@@ -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
- perPage: { type: 'number', default: 20 },
10
- currentPage: { type: 'number', default: 1 }
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: { type: 'object', additionalProperties: { type: 'string' } }
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
- type: 'array',
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
- type: 'array',
76
- items: {}
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
  };
@@ -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
- namespace: { type: 'string' },
7
- isDefault: { type: 'boolean' },
8
- perPage: { type: 'number', default: 20 },
9
- currentPage: { type: 'number', default: 1 }
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kne/fastify-intl-admin",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Fastify 业务插件:国际化语言类型与语言库管理(lang-type / lang-lib)",
5
5
  "main": "index.js",
6
6
  "scripts": {