7385785504
- viewer/admin 错误码→文案映射按契约 ErrorCodes 枚举全集登记(机器守护 spec 遍历枚举断言无缺漏 + 映射键在 zh/en 词条均存在 + locale 键集 递归相等);mobile 镜像补齐 DOWNLOAD_DISABLED(P4a 增补漏登) - 漏译修复:viewer.photoRemoved en 侧缺失(审计测试捕获); DOWNLOAD_DISABLED 观众侧专属文案 + useDownload 专属提示接线; admin apiMsg 增 RATE_LIMITED/ALBUM_NOT_PUBLIC/DOWNLOAD_DISABLED/ SYNC_AUTH_FAILED 四码词条 - 运行时抽查:admin 主旅程 en 版 e2e(登录→相册管理全英文 + 偏好 持久);embed iframe lang=en 渲染与深链跟随 e2e(zh 侧既有用例覆盖) - contracts 新增 ./errors 浏览器安全子路径(errors.ts 纯 TS); admin 增 @photo/contracts devDep(仅测试消费) Co-Authored-By: Claude Code <noreply@anthropic.com>
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([]);
|
||
});
|
||
});
|