@maiyunnet/kebab 9.16.1 → 9.17.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/index.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * --- 本文件用来定义每个目录实体地址的常量 ---
6
6
  */
7
7
  /** --- 当前系统版本号 --- */
8
- export declare const VER = "9.16.1";
8
+ export declare const VER = "9.17.0";
9
9
  /** --- 框架根目录,以 / 结尾 --- */
10
10
  export declare const ROOT_PATH: string;
11
11
  /** --- 框架的 LIB,以 / 结尾 --- */
package/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * --- 本文件用来定义每个目录实体地址的常量 ---
7
7
  */
8
8
  /** --- 当前系统版本号 --- */
9
- export const VER = '9.16.1';
9
+ export const VER = '9.17.0';
10
10
  // --- 服务端用的路径 ---
11
11
  const imu = decodeURIComponent(import.meta.url).replace('file://', '').replace(/^\/(\w:)/, '$1');
12
12
  /** --- /xxx/xxx --- */
package/lib/core.d.ts CHANGED
@@ -2,10 +2,13 @@ import * as http from 'http';
2
2
  import * as http2 from 'http2';
3
3
  import * as stream from 'stream';
4
4
  import * as net from 'net';
5
+ import * as valibot from 'valibot';
5
6
  import * as kebab from '#kebab/index.js';
6
7
  import * as lNetResponse from '#kebab/lib/net/response.js';
7
8
  import * as lUndiciResponse from '#kebab/lib/undici/response.js';
8
9
  import * as sCtr from '#kebab/sys/ctr.js';
10
+ /** --- Valibot 数据校验工具,业务侧可通过 lCore.v 使用 --- */
11
+ export { valibot as v };
9
12
  /** --- 全局参数 --- */
10
13
  export declare const globalConfig: kebab.IConfig & {
11
14
  'httpPort': number;
package/lib/core.js CHANGED
@@ -11,6 +11,7 @@ import * as net from 'net';
11
11
  import * as crypto from 'crypto';
12
12
  import Ajv from 'ajv';
13
13
  import addFormats from 'ajv-formats';
14
+ import * as valibot from 'valibot';
14
15
  import * as kebab from '#kebab/index.js';
15
16
  import * as lTime from '#kebab/lib/time.js';
16
17
  import * as lFs from '#kebab/lib/fs.js';
@@ -20,6 +21,8 @@ import * as lCrypto from '#kebab/lib/crypto.js';
20
21
  import * as lNetResponse from '#kebab/lib/net/response.js';
21
22
  import * as lUndiciResponse from '#kebab/lib/undici/response.js';
22
23
  import * as sCtr from '#kebab/sys/ctr.js';
24
+ /** --- Valibot 数据校验工具,业务侧可通过 lCore.v 使用 --- */
25
+ export { valibot as v };
23
26
  /** --- 全局参数 --- */
24
27
  export const globalConfig = {};
25
28
  /** --- JSON Schema 校验器 --- */
package/lib/fs.js CHANGED
@@ -440,10 +440,13 @@ export async function readToResponse(path, req, res, stat) {
440
440
  }
441
441
  // --- 这些文件可能需要缓存 ---
442
442
  if (['htm', 'html', 'css', 'js', 'mjs', 'xml', 'jpg', 'jpeg', 'svg', 'gif', 'png', 'json'].includes(mimeData.extension)) {
443
+ /** --- 静态文件默认缓存秒数 --- */
444
+ const cacheTTL = 600;
443
445
  const hash = `W/"${stat.size.toString(16)}-${stat.mtime.getTime().toString(16)}"`;
444
446
  const lastModified = stat.mtime.toUTCString();
445
447
  res.setHeader('etag', hash);
446
- res.setHeader('cache-control', 'public, max-age=600');
448
+ res.setHeader('expires', new Date(Date.now() + cacheTTL * 1_000).toUTCString());
449
+ res.setHeader('cache-control', 'public, max-age=' + cacheTTL.toString());
447
450
  // --- 判断返回 304 吗 ---
448
451
  const noneMatch = req.headers['if-none-match'];
449
452
  const modifiedSince = req.headers['if-modified-since'];
@@ -31,7 +31,7 @@ export declare class Response {
31
31
  /**
32
32
  * --- 获取响应读取流对象 ---
33
33
  */
34
- getStream(): zlib.BrotliDecompress | zlib.Gunzip | zlib.Inflate | (import("undici/types/readable").default & undici.Dispatcher.BodyMixin) | null;
34
+ getStream(): (import("undici/types/readable").default & undici.Dispatcher.BodyMixin) | zlib.Gunzip | zlib.Inflate | zlib.BrotliDecompress | null;
35
35
  /**
36
36
  * --- 获取原生响应读取流对象 ---
37
37
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maiyunnet/kebab",
3
- "version": "9.16.1",
3
+ "version": "9.17.0",
4
4
  "description": "Simple, easy-to-use, and fully-featured Node.js framework that is ready-to-use out of the box.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -22,10 +22,10 @@
22
22
  "#kebab/*": "./*"
23
23
  },
24
24
  "dependencies": {
25
- "@aws-sdk/client-s3": "^3.1096.0",
26
- "@aws-sdk/lib-storage": "^3.1096.0",
25
+ "@aws-sdk/client-s3": "^3.1120.0",
26
+ "@aws-sdk/lib-storage": "^3.1120.0",
27
27
  "@litert/http-client": "^1.1.2",
28
- "@litert/mime": "^0.1.3",
28
+ "@litert/mime": "^2.0.0",
29
29
  "@litert/redis": "^3.2.1",
30
30
  "@litert/websocket": "^0.2.8",
31
31
  "@radix-ui/react-checkbox": "^1.3.11",
@@ -39,30 +39,31 @@
39
39
  "class-variance-authority": "^0.7.1",
40
40
  "clsx": "^2.1.1",
41
41
  "ejs": "^6.0.1",
42
- "esbuild": "^0.28.1",
42
+ "esbuild": "^0.28.2",
43
43
  "jszip": "^3.10.1",
44
- "mysql2": "^3.23.2",
44
+ "mysql2": "^3.24.2",
45
45
  "node-cron": "^4.6.0",
46
- "openai": "^7.0.0",
47
- "pg": "^8.22.0",
46
+ "openai": "^7.8.0",
47
+ "pg": "^8.23.0",
48
48
  "react": "^19.2.8",
49
49
  "react-dom": "^19.2.8",
50
- "react-router-dom": "^7.18.1",
50
+ "react-router-dom": "^7.18.2",
51
51
  "ssh2": "^1.17.0",
52
52
  "svg-captcha": "^1.4.0",
53
53
  "tailwind-merge": "^3.6.0",
54
- "tencentcloud-sdk-nodejs": "^4.1.277",
55
- "undici": "^8.9.0"
54
+ "tencentcloud-sdk-nodejs": "^4.1.303",
55
+ "undici": "^8.10.0",
56
+ "valibot": "^1.4.2"
56
57
  },
57
58
  "devDependencies": {
58
59
  "@litert/eslint-plugin-rules": "^0.3.1",
59
60
  "@types/ejs": "^3.1.5",
60
- "@types/node": "^26.1.2",
61
- "@types/pg": "^8.20.0",
62
- "@types/react": "^19.2.17",
63
- "@types/react-dom": "^19.2.3",
61
+ "@types/node": "^26.4.0",
62
+ "@types/pg": "^8.23.1",
63
+ "@types/react": "^19.2.18",
64
+ "@types/react-dom": "^19.2.5",
64
65
  "typedoc": "^0.28.20",
65
- "typedoc-plugin-markdown": "^4.12.0",
66
+ "typedoc-plugin-markdown": "^4.13.0",
66
67
  "typescript": "^6.0.3"
67
68
  }
68
69
  }
package/sys/ctr.d.ts CHANGED
@@ -1,5 +1,11 @@
1
+ /**
2
+ * Project: Kebab, User: JianSuoQiYue
3
+ * Date: 2020-3-14 17:24:38
4
+ * Last: 2020-3-30 15:31:40, 2022-07-22 16:59:00, 2022-09-12 23:51:56, 2022-09-23 15:53:58, 2022-12-29 01:18:08, 2023-2-28 20:07:57, 2023-12-27 18:39:35, 2024-3-1 19:38:53, 2024-4-9 16:03:58, 2025-2-12 18:55:44, 2025-6-12 16:56:08, 2026-8-22
5
+ */
1
6
  import * as http from 'http';
2
7
  import * as http2 from 'http2';
8
+ import type * as v from 'valibot';
3
9
  import * as kebab from '#kebab/index.js';
4
10
  import * as lSession from '#kebab/lib/session.js';
5
11
  import * as lDb from '#kebab/lib/db.js';
@@ -9,6 +15,49 @@ import * as lWs from '#kebab/lib/ws.js';
9
15
  * --- 清除已经加载的 data 与语言包文件缓存 ---
10
16
  */
11
17
  export declare function clearLocaleData(): void;
18
+ /** --- Valibot 校验失败时可直接返回给客户端的内容或生成函数 --- */
19
+ export type TValibotResponse<TIssue extends v.BaseIssue<unknown>> = kebab.Json[] | ((issues: [TIssue, ...TIssue[]]) => kebab.Json[]);
20
+ /** --- 在 issue 路径完整后,根据 Kebab 生成的语言键翻译消息 --- */
21
+ export type TValibotTranslate = (key: string, issue: v.BaseIssue<unknown>) => string;
22
+ /** --- Valibot 同步校验选项 --- */
23
+ export interface IValibotOptions<TIssue extends v.BaseIssue<unknown>> {
24
+ /** --- 传给 Valibot 的校验配置 --- */
25
+ 'config'?: v.Config<TIssue>;
26
+ /** --- 校验失败时返回给客户端的内容,默认使用首个 issue 的消息 --- */
27
+ 'response'?: TValibotResponse<TIssue>;
28
+ /** --- 使用完整 issue 路径翻译消息 --- */
29
+ 'translate'?: TValibotTranslate;
30
+ }
31
+ /** --- XSRF 校验失败的问题描述 --- */
32
+ export interface IValibotXsrfIssue extends v.BaseIssue<unknown> {
33
+ readonly kind: 'validation';
34
+ readonly type: 'xsrf';
35
+ }
36
+ /** --- 带 XSRF 检测的 Valibot 同步校验选项 --- */
37
+ export interface IValibotXOptions<TIssue extends v.BaseIssue<unknown>> {
38
+ /** --- 传给 Valibot 的校验配置 --- */
39
+ 'config'?: v.Config<TIssue>;
40
+ /** --- 校验失败时返回给客户端的内容,回调同时可能收到 XSRF issue --- */
41
+ 'response'?: TValibotResponse<TIssue | IValibotXsrfIssue>;
42
+ /** --- 使用完整 issue 路径翻译消息,包括 XSRF issue --- */
43
+ 'translate'?: TValibotTranslate;
44
+ /** --- 是否忽略 XSRF 检测 --- */
45
+ 'ignoreXsrf'?: boolean;
46
+ }
47
+ /** --- Valibot 校验结果:仅成功分支提供 schema 推导后的 output --- */
48
+ export type TValibotResult<TSchema extends v.GenericSchema, TIssue extends v.BaseIssue<unknown> = v.InferIssue<TSchema>> = {
49
+ readonly typed: true;
50
+ readonly success: true;
51
+ readonly output: v.InferOutput<TSchema>;
52
+ readonly issues: undefined;
53
+ readonly response: undefined;
54
+ } | {
55
+ readonly typed: boolean;
56
+ readonly success: false;
57
+ readonly output: undefined;
58
+ readonly issues: [TIssue, ...TIssue[]];
59
+ readonly response: kebab.Json[];
60
+ };
12
61
  export declare class Ctr {
13
62
  /** --- 路由参数序列数组 --- */
14
63
  protected _param: string[];
@@ -200,6 +249,42 @@ export declare class Ctr {
200
249
  * @param msgSuffix 可选的消息后缀
201
250
  */
202
251
  private _setCheckError;
252
+ /**
253
+ * --- 生成 Valibot 校验失败结果 ---
254
+ * @param issues Valibot 问题列表
255
+ * @param response 自定义客户端返回值或生成函数
256
+ * @returns 校验失败结果
257
+ */
258
+ private _getValibotFailure;
259
+ /**
260
+ * --- 根据完整 issue 路径生成 Kebab 语言包键 ---
261
+ * @param issue Valibot 校验问题
262
+ * @returns 语言包键
263
+ */
264
+ private _getValibotLocaleKey;
265
+ /**
266
+ * --- 在 Valibot 完成父级路径组装后翻译问题消息 ---
267
+ * @param issues Valibot 问题列表
268
+ * @param translate 消息翻译函数
269
+ * @returns 翻译后的问题列表
270
+ */
271
+ private _translateValibotIssues;
272
+ /**
273
+ * --- 使用 Valibot schema 校验并解析输入,成功后 output 会自动推导类型 ---
274
+ * @param schema Valibot 同步 schema
275
+ * @param input 待校验的输入
276
+ * @param options Valibot 配置和自定义客户端返回值
277
+ * @returns 可判别的校验结果
278
+ */
279
+ protected _valibot<const TSchema extends v.GenericSchema>(schema: TSchema, input: unknown, options?: IValibotOptions<v.InferIssue<TSchema>>): TValibotResult<TSchema>;
280
+ /**
281
+ * --- 使用 Valibot schema 校验并解析输入,同时检测 XSRF ---
282
+ * @param schema Valibot 同步 schema
283
+ * @param input 待校验的输入
284
+ * @param options Valibot 配置、自定义客户端返回值和 XSRF 选项
285
+ * @returns 可判别的校验结果
286
+ */
287
+ protected _valibotx<const TSchema extends v.GenericSchema>(schema: TSchema, input: unknown, options?: IValibotXOptions<v.InferIssue<TSchema>>): TValibotResult<TSchema, v.InferIssue<TSchema> | IValibotXsrfIssue>;
203
288
  /**
204
289
  * --- 检测提交的数据类型 ---
205
290
  * @param input 要校验的输入项
package/sys/ctr.js CHANGED
@@ -1,8 +1,3 @@
1
- /**
2
- * Project: Kebab, User: JianSuoQiYue
3
- * Date: 2020-3-14 17:24:38
4
- * Last: 2020-3-30 15:31:40, 2022-07-22 16:59:00, 2022-09-12 23:51:56, 2022-09-23 15:53:58, 2022-12-29 01:18:08, 2023-2-28 20:07:57, 2023-12-27 18:39:35, 2024-3-1 19:38:53, 2024-4-9 16:03:58, 2025-2-12 18:55:44, 2025-6-12 16:56:08, 2026-8-22
5
- */
6
1
  import ejs from 'ejs';
7
2
  import * as lCore from '#kebab/lib/core.js';
8
3
  import * as lFs from '#kebab/lib/fs.js';
@@ -416,6 +411,123 @@ export class Ctr {
416
411
  rtn[2] = lastVal[2];
417
412
  }
418
413
  }
414
+ /**
415
+ * --- 生成 Valibot 校验失败结果 ---
416
+ * @param issues Valibot 问题列表
417
+ * @param response 自定义客户端返回值或生成函数
418
+ * @returns 校验失败结果
419
+ */
420
+ _getValibotFailure(issues, response, translate) {
421
+ /** --- 路径已完整且消息已翻译的问题列表 --- */
422
+ const translatedIssues = this._translateValibotIssues(issues, translate);
423
+ const clientResponse = typeof response === 'function' ? response(translatedIssues) : response;
424
+ return {
425
+ 'typed': false,
426
+ 'success': false,
427
+ 'output': undefined,
428
+ 'issues': translatedIssues,
429
+ 'response': clientResponse ?? [0, translatedIssues[0].message],
430
+ };
431
+ }
432
+ /**
433
+ * --- 根据完整 issue 路径生成 Kebab 语言包键 ---
434
+ * @param issue Valibot 校验问题
435
+ * @returns 语言包键
436
+ */
437
+ _getValibotLocaleKey(issue) {
438
+ if ((issue.type === 'strict_object') && (issue.expected === 'never')) {
439
+ return 'validation.strict_object';
440
+ }
441
+ const path = lCore.v.getDotPath(issue);
442
+ return path ? `validation.${path}.${issue.type}` : `validation.${issue.type}`;
443
+ }
444
+ /**
445
+ * --- 在 Valibot 完成父级路径组装后翻译问题消息 ---
446
+ * @param issues Valibot 问题列表
447
+ * @param translate 消息翻译函数
448
+ * @returns 翻译后的问题列表
449
+ */
450
+ _translateValibotIssues(issues, translate) {
451
+ if (!translate) {
452
+ return issues;
453
+ }
454
+ const translateIssue = (issue) => {
455
+ const key = this._getValibotLocaleKey(issue);
456
+ const message = translate(key, issue);
457
+ return {
458
+ ...issue,
459
+ /** --- 语言包缺失时保留键名,并回退到 Valibot 原始英文原因 --- */
460
+ 'message': message === `[LocaleError]${key}` ? `[${key}] ${issue.message}` : message,
461
+ };
462
+ };
463
+ return [translateIssue(issues[0]), ...issues.slice(1).map(translateIssue)];
464
+ }
465
+ /**
466
+ * --- 使用 Valibot schema 校验并解析输入,成功后 output 会自动推导类型 ---
467
+ * @param schema Valibot 同步 schema
468
+ * @param input 待校验的输入
469
+ * @param options Valibot 配置和自定义客户端返回值
470
+ * @returns 可判别的校验结果
471
+ */
472
+ _valibot(schema, input, options) {
473
+ const result = lCore.v.safeParse(schema, input, options?.config);
474
+ if (!result.success) {
475
+ return this._getValibotFailure(result.issues, options?.response, options?.translate);
476
+ }
477
+ return {
478
+ ...result,
479
+ 'response': undefined,
480
+ };
481
+ }
482
+ /**
483
+ * --- 使用 Valibot schema 校验并解析输入,同时检测 XSRF ---
484
+ * @param schema Valibot 同步 schema
485
+ * @param input 待校验的输入
486
+ * @param options Valibot 配置、自定义客户端返回值和 XSRF 选项
487
+ * @returns 可判别的校验结果
488
+ */
489
+ _valibotx(schema, input, options) {
490
+ /** --- 用户提交的 XSRF token --- */
491
+ let submittedXsrf;
492
+ /** --- 移除 XSRF 传输字段后交给业务 schema 的输入 --- */
493
+ let schemaInput = input;
494
+ if ((typeof input === 'object') && (input !== null) && !Array.isArray(input)) {
495
+ const inputRecord = input;
496
+ submittedXsrf = inputRecord['_xsrf'];
497
+ schemaInput = { ...inputRecord };
498
+ delete schemaInput['_xsrf'];
499
+ }
500
+ if (!options?.ignoreXsrf) {
501
+ /** --- Cookie 中应匹配的 XSRF token --- */
502
+ const expectedXsrf = this._cookie['XSRF-TOKEN'];
503
+ if (!expectedXsrf || (submittedXsrf !== expectedXsrf)) {
504
+ const issue = {
505
+ 'kind': 'validation',
506
+ 'type': 'xsrf',
507
+ 'input': undefined,
508
+ 'expected': 'valid XSRF token',
509
+ 'received': submittedXsrf === undefined ? 'undefined' : typeof submittedXsrf,
510
+ 'message': 'Bad request, no permission.',
511
+ 'path': [{
512
+ 'type': 'unknown',
513
+ 'origin': 'value',
514
+ 'input': undefined,
515
+ 'key': '_xsrf',
516
+ 'value': undefined,
517
+ }],
518
+ };
519
+ return this._getValibotFailure([issue], options?.response, options?.translate);
520
+ }
521
+ }
522
+ const result = lCore.v.safeParse(schema, schemaInput, options?.config);
523
+ if (!result.success) {
524
+ return this._getValibotFailure(result.issues, options?.response, options?.translate);
525
+ }
526
+ return {
527
+ ...result,
528
+ 'response': undefined,
529
+ };
530
+ }
419
531
  /**
420
532
  * --- 检测提交的数据类型 ---
421
533
  * @param input 要校验的输入项
package/sys/route.js CHANGED
@@ -175,7 +175,7 @@ export async function run(data) {
175
175
  path = '@';
176
176
  }
177
177
  /** --- 不做语言跳转、不进入动态处理流程的路径 --- */
178
- const stcPaths = /^(stc\/.*|favicon.\w+?\??.*|apple[\w-]+?\.png\??.*|[\w-]+?\.txt\??.*|[\w-]+?\.html\??.*)/;
178
+ const stcPaths = /^(stc\/.*|favicon.\w+?\??.*|apple[\w-]+?\.png\??.*|[\w-]+?\.(?:txt|html|xml)\??.*)/;
179
179
  // --- 检测是否托管语言页面 ---
180
180
  const param = [];
181
181
  if (config.lang && data.res && !stcPaths.test(path)) {
@@ -18,6 +18,8 @@ export default class extends sCtr.Ctr {
18
18
  ctrCheckinput1(): Promise<kebab.Json[]>;
19
19
  ctrCheckinputSchema(): string;
20
20
  ctrCheckinputSchema1(): Promise<kebab.Json[]>;
21
+ ctrValibot(): Promise<kebab.Json[] | string>;
22
+ ctrValibot1(): Promise<kebab.Json[]>;
21
23
  ctrLocale(): Promise<kebab.Json[] | string>;
22
24
  ctrCachettl(): string;
23
25
  ctrHttpcode(): string;
@@ -28,6 +28,15 @@ import * as sCtr from '#kebab/sys/ctr.js';
28
28
  // --- mod ---
29
29
  import mTest from '../mod/test.js';
30
30
  import mTestData from '../mod/testdata.js';
31
+ /** --- Kebab 核心库提供的 Valibot 对象 --- */
32
+ const v = lCore.v;
33
+ /** --- Valibot 示例支持的语言 --- */
34
+ const valibotLocaleSchema = v.picklist(['en', 'sc', 'tc', 'ja']);
35
+ /** --- 模块级复用的 Valibot 输入规则,不绑定任何请求或语言 --- */
36
+ const valibotInputSchema = v.strictObject({
37
+ 'title': v.pipe(v.string(), v.nonEmpty()),
38
+ 'count': v.pipe(v.string(), v.toNumber(), v.integer()),
39
+ });
31
40
  export default class extends sCtr.Ctr {
32
41
  _internalUrl = '';
33
42
  onLoad() {
@@ -114,6 +123,7 @@ export default class extends sCtr.Ctr {
114
123
  '<br><br><b>Ctr:</b>',
115
124
  `<br><br><a href="${this._config.const.urlBase}test/ctr-xsrf">View "test/ctr-xsrf"</a>`,
116
125
  `<br><a href="${this._config.const.urlBase}test/ctr-checkinput">View "test/ctr-checkinput"</a> <a href="${this._config.const.urlBase}test/ctr-checkinput-schema">schema</a>`,
126
+ `<br><a href="${this._config.const.urlBase}test/ctr-valibot">View "test/ctr-valibot"</a>`,
117
127
  `<br><a href="${this._config.const.urlBase}test/ctr-locale">View "test/ctr-locale"</a>`,
118
128
  `<br><a href="${this._config.const.urlBase}test/ctr-cachettl">View "test/ctr-cachettl"</a>`,
119
129
  `<br><a href="${this._config.const.urlBase}test/ctr-httpcode">View "test/ctr-httpcode"</a>`,
@@ -546,6 +556,88 @@ function post(p) {
546
556
  }
547
557
  return [1, { 'post': this._post }];
548
558
  }
559
+ async ctrValibot() {
560
+ const locale = this._valibot(valibotLocaleSchema, this._get['lang'] ?? 'en', {
561
+ 'response': [0, 'Wrong language.'],
562
+ });
563
+ if (!locale.success) {
564
+ return locale.response;
565
+ }
566
+ if (!await this._loadLocale(locale.output, 'test')) {
567
+ return [0, 'Could not load locale.'];
568
+ }
569
+ this._enabledXsrf();
570
+ const echo = [
571
+ '<b>Test _valibotx with inferred output and request locale</b><br><br>',
572
+ `<a href="${this._config.const.urlBase}test/ctr-valibot?lang=en">English</a> | ` +
573
+ `<a href="${this._config.const.urlBase}test/ctr-valibot?lang=sc">简体中文</a> | ` +
574
+ `<a href="${this._config.const.urlBase}test/ctr-valibot?lang=tc">繁體中文</a> | ` +
575
+ `<a href="${this._config.const.urlBase}test/ctr-valibot?lang=ja">日本語</a><br><br>`,
576
+ `<b>Current locale:</b> ${locale.output}<br><br>`,
577
+ `<pre>const v = lCore.v;
578
+ const valibotInputSchema = v.strictObject({
579
+ 'title': v.pipe(v.string(), v.nonEmpty()),
580
+ 'count': v.pipe(v.string(), v.toNumber(), v.integer()),
581
+ });</pre>`,
582
+ ];
583
+ const posts = [
584
+ { 'title': 'Kebab', 'count': '2' },
585
+ { 'title': '', 'count': '2' },
586
+ { 'title': 'Kebab', 'count': 'abc' },
587
+ { 'title': 'Kebab', 'count': '1.5' },
588
+ { 'title': 'Kebab', 'count': '2', 'extra': true },
589
+ ];
590
+ for (const item of posts) {
591
+ const str = lText.stringifyJson(item).replace(/"/g, '&quot;');
592
+ echo.push(`<input type="button" value="Post '${str}'" onclick="post('${str}')"><br>`);
593
+ }
594
+ echo.push(`<script>
595
+ function post(p) {
596
+ const data = JSON.parse(p);
597
+ data._xsrf = '${this._xsrf}';
598
+ document.getElementById('result').innerText = 'Waiting...';
599
+ fetch('${this._config.const.urlBase}test/ctr-valibot1?lang=${locale.output}', {
600
+ method: 'POST',
601
+ headers: {
602
+ 'content-type': 'application/json'
603
+ },
604
+ body: JSON.stringify(data)
605
+ }).then(function(r) {
606
+ return r.text();
607
+ }).then(function(t) {
608
+ document.getElementById('result').innerText = t;
609
+ });
610
+ }
611
+ </script>
612
+ <br>Result:<pre id="result">Nothing.</pre>`);
613
+ return echo.join('') + this._getEnd();
614
+ }
615
+ async ctrValibot1() {
616
+ if (!await this._handleFormData()) {
617
+ return [0];
618
+ }
619
+ const locale = this._valibot(valibotLocaleSchema, this._get['lang'] ?? 'en', {
620
+ 'response': [0, 'Wrong language.'],
621
+ });
622
+ if (!locale.success) {
623
+ return locale.response;
624
+ }
625
+ if (!await this._loadLocale(locale.output, 'test')) {
626
+ return [0, 'Could not load locale.'];
627
+ }
628
+ const parsed = this._valibotx(valibotInputSchema, this._post, {
629
+ 'translate': key => this._l(key),
630
+ 'response': issues => [0, issues[0].message, {
631
+ 'issues': v.flatten(issues),
632
+ }],
633
+ });
634
+ if (!parsed.success) {
635
+ return parsed.response;
636
+ }
637
+ return [1, {
638
+ 'post': parsed.output,
639
+ }];
640
+ }
549
641
  async ctrLocale() {
550
642
  const rtn = [];
551
643
  if (!this._checkInput(this._get, {
@@ -4,5 +4,22 @@
4
4
  "test": "The ? test ?.",
5
5
  "test2": {
6
6
  "test3": "test4"
7
+ },
8
+ "validation": {
9
+ "strict_object": "The submitted data must be an object and contain only supported fields.",
10
+ "_xsrf": {
11
+ "xsrf": "Bad request, no permission."
12
+ },
13
+ "title": {
14
+ "strict_object": "The title is required.",
15
+ "string": "The title must be a string.",
16
+ "non_empty": "Please enter a title."
17
+ },
18
+ "count": {
19
+ "strict_object": "The count is required.",
20
+ "string": "The count must be a string.",
21
+ "to_number": "The count must be a number.",
22
+ "integer": "The count must be an integer."
23
+ }
7
24
  }
8
- }
25
+ }
@@ -4,5 +4,22 @@
4
4
  "test": "これ ? 試験 ?。",
5
5
  "test2": {
6
6
  "test3": "試験4"
7
+ },
8
+ "validation": {
9
+ "strict_object": "送信内容の形式が正しくないか、未対応のフィールドが含まれています。",
10
+ "_xsrf": {
11
+ "xsrf": "無効なリクエスト、または権限がありません。"
12
+ },
13
+ "title": {
14
+ "strict_object": "タイトルがありません。",
15
+ "string": "タイトルは文字列で指定してください。",
16
+ "non_empty": "タイトルを入力してください。"
17
+ },
18
+ "count": {
19
+ "strict_object": "数量がありません。",
20
+ "string": "数量は文字列で指定してください。",
21
+ "to_number": "数量は数値で指定してください。",
22
+ "integer": "数量は整数で指定してください。"
23
+ }
7
24
  }
8
- }
25
+ }
@@ -4,5 +4,22 @@
4
4
  "test": "这个 ? 测试 ?。",
5
5
  "test2": {
6
6
  "test3": "测试4"
7
+ },
8
+ "validation": {
9
+ "strict_object": "提交内容格式不正确或包含不支持的字段。",
10
+ "_xsrf": {
11
+ "xsrf": "请求无效或无权限。"
12
+ },
13
+ "title": {
14
+ "strict_object": "缺少标题。",
15
+ "string": "标题必须是字符串。",
16
+ "non_empty": "请输入标题。"
17
+ },
18
+ "count": {
19
+ "strict_object": "缺少数量。",
20
+ "string": "数量必须是字符串。",
21
+ "to_number": "数量必须是数字。",
22
+ "integer": "数量必须是整数。"
23
+ }
7
24
  }
8
- }
25
+ }
@@ -4,5 +4,22 @@
4
4
  "test": "這個 ? 測試 ?.",
5
5
  "test2": {
6
6
  "test3": "測試4"
7
+ },
8
+ "validation": {
9
+ "strict_object": "提交內容格式不正確或包含不支援的欄位。",
10
+ "_xsrf": {
11
+ "xsrf": "請求無效或沒有權限。"
12
+ },
13
+ "title": {
14
+ "strict_object": "缺少標題。",
15
+ "string": "標題必須是字串。",
16
+ "non_empty": "請輸入標題。"
17
+ },
18
+ "count": {
19
+ "strict_object": "缺少數量。",
20
+ "string": "數量必須是字串。",
21
+ "to_number": "數量必須是數字。",
22
+ "integer": "數量必須是整數。"
23
+ }
7
24
  }
8
- }
25
+ }