72 lines
3.1 KiB
TypeScript
72 lines
3.1 KiB
TypeScript
|
|
import { describe, expect, it } from 'vitest';
|
|||
|
|
import { ErrorCodes } from '@photo/contracts/errors';
|
|||
|
|
import { errorCodeI18nKeys } from '../api/errors';
|
|||
|
|
import en from '../i18n/locales/en';
|
|||
|
|
import zhCN from '../i18n/locales/zh-CN';
|
|||
|
|
|
|||
|
|
// T15 全端语言验收(P4b Task 6):错误码→文案映射的机器守护。
|
|||
|
|
// 1. 契约 ErrorCodes 枚举全集在本表无缺漏(A12「主要提示无漏译」);
|
|||
|
|
// 2. 表内每个文案键在 zh-CN / en 两份词条都真实存在(防键注册了但一侧
|
|||
|
|
// 没写文案的真漏译;点路径解析);
|
|||
|
|
// 3. zh-CN / en 两份词条键集合递归相等(模板级 zh/en 一致性,镜像 admin
|
|||
|
|
// 侧 locale 守护与 P3 mobile arb 完整性测试)。
|
|||
|
|
// 服务端不做文案本地化(错误体 message 仅英文诊断兜底,客户端按稳定 code
|
|||
|
|
// 映射——见 openapi.yaml info.description),故本表即观众侧错误文案全集。
|
|||
|
|
|
|||
|
|
type Messages = Record<string, unknown>;
|
|||
|
|
|
|||
|
|
/** 递归收集词条文件的叶子键点路径集合(errors.albumNotFound 等)。 */
|
|||
|
|
function leafPaths(messages: Messages, prefix = ''): Set<string> {
|
|||
|
|
const paths = new Set<string>();
|
|||
|
|
for (const [key, value] of Object.entries(messages)) {
|
|||
|
|
const path = prefix === '' ? key : `${prefix}.${key}`;
|
|||
|
|
if (value !== null && typeof value === 'object') {
|
|||
|
|
for (const child of leafPaths(value as Messages, path)) paths.add(child);
|
|||
|
|
} else {
|
|||
|
|
paths.add(path);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return paths;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 按点路径取词条值(不存在返回 undefined)。 */
|
|||
|
|
function resolvePath(messages: Messages, path: string): unknown {
|
|||
|
|
let current: unknown = messages;
|
|||
|
|
for (const segment of path.split('.')) {
|
|||
|
|
if (current === null || typeof current !== 'object') return undefined;
|
|||
|
|
current = (current as Messages)[segment];
|
|||
|
|
}
|
|||
|
|
return current;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
describe('错误码文案覆盖(T15/A12)', () => {
|
|||
|
|
const zhPaths = leafPaths(zhCN as Messages);
|
|||
|
|
const enPaths = leafPaths(en as Messages);
|
|||
|
|
|
|||
|
|
it('契约 ErrorCodes 枚举全集在 errorCodeI18nKeys 无缺漏', () => {
|
|||
|
|
const codes = Object.values(ErrorCodes);
|
|||
|
|
expect(codes.length).toBeGreaterThanOrEqual(18); // 契约现存 18 码,只增不减
|
|||
|
|
for (const code of codes) {
|
|||
|
|
expect(errorCodeI18nKeys[code], `缺少错误码映射: ${code}`).toBeDefined();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('映射表内每个文案键在 zh-CN 与 en 词条均存在且非空', () => {
|
|||
|
|
for (const [code, key] of Object.entries(errorCodeI18nKeys)) {
|
|||
|
|
for (const [locale, paths] of [
|
|||
|
|
['zh-CN', zhPaths],
|
|||
|
|
['en', enPaths],
|
|||
|
|
] as const) {
|
|||
|
|
expect(paths.has(key), `${code} 的文案键 ${key} 未登记于 ${locale} 词条`).toBe(true);
|
|||
|
|
const value = resolvePath((locale === 'zh-CN' ? zhCN : en) as Messages, key);
|
|||
|
|
expect(typeof value === 'string' && value.length > 0, `${key} 在 ${locale} 非非空字符串`).toBe(true);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('zh-CN 与 en 词条键集合递归相等(无单侧漏译)', () => {
|
|||
|
|
expect([...zhPaths].filter((path) => !enPaths.has(path))).toEqual([]);
|
|||
|
|
expect([...enPaths].filter((path) => !zhPaths.has(path))).toEqual([]);
|
|||
|
|
});
|
|||
|
|
});
|