feat(i18n): T15 full-stack language audit with coverage assertions
- 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>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
// T15 全端语言验收(P4b Task 6):admin 侧错误码→文案映射的机器守护。
|
||||
// 1. 契约 ErrorCodes 枚举全集在 API_CODE_MSG_KEYS 无缺漏(A12「主要提示
|
||||
// 无漏译」——服务端 message 仅英文诊断兜底,本表即管理端错误文案全集);
|
||||
// 2. 表内每个文案键在 zh/en 两份 locale 文件都真实存在(点路径解析);
|
||||
// 3. zh/en locale 键集合递归相等(模板级一致性,镜像 P3 mobile arb 完整性
|
||||
// 测试与 viewer 侧 error-coverage 守护)。
|
||||
import { describe, expect, it } from 'vitest'
|
||||
// 根入口导入(非 ./errors 子路径):admin tsconfig 为 classic moduleResolution
|
||||
// (不走 package exports),根 main/types 字段可解析;仅测试环境(node)使用,
|
||||
// internal-sync 的 node:crypto 不入产物。production 浏览器代码如需运行时
|
||||
// 枚举请走 @photo/contracts/errors 子路径(viewer 先例)。
|
||||
import { ErrorCodes } from '@photo/contracts'
|
||||
import { API_CODE_MSG_KEYS } from './error'
|
||||
import enMessages from '@/locales/langs/en.json'
|
||||
import zhMessages from '@/locales/langs/zh.json'
|
||||
|
||||
type Messages = Record<string, unknown>
|
||||
|
||||
/** 递归收集 locale 文件的叶子键点路径集合(apiMsg.ALBUM_NOT_FOUND 等)。 */
|
||||
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(zhMessages as Messages)
|
||||
const enPaths = leafPaths(enMessages as Messages)
|
||||
|
||||
it('契约 ErrorCodes 枚举全集在 API_CODE_MSG_KEYS 无缺漏', () => {
|
||||
const codes = Object.values(ErrorCodes)
|
||||
expect(codes.length).toBeGreaterThanOrEqual(18) // 契约现存 18 码,只增不减
|
||||
for (const code of codes) {
|
||||
expect(API_CODE_MSG_KEYS[code], `缺少错误码映射: ${code}`).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
it('映射表内每个文案键在 zh 与 en locale 均存在且非空', () => {
|
||||
for (const [code, key] of Object.entries(API_CODE_MSG_KEYS)) {
|
||||
for (const [locale, messages] of [
|
||||
['zh', zhMessages],
|
||||
['en', enMessages]
|
||||
] as const) {
|
||||
const paths = locale === 'zh' ? zhPaths : enPaths
|
||||
expect(paths.has(key), `${code} 的文案键 ${key} 未登记于 ${locale} locale`).toBe(true)
|
||||
const value = resolvePath(messages as Messages, key)
|
||||
expect(
|
||||
typeof value === 'string' && value.length > 0,
|
||||
`${key} 在 ${locale} 非非空字符串`
|
||||
).toBe(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('zh 与 en locale 键集合递归相等(无单侧漏译)', () => {
|
||||
expect([...zhPaths].filter((path) => !enPaths.has(path))).toEqual([])
|
||||
expect([...enPaths].filter((path) => !zhPaths.has(path))).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -113,12 +113,38 @@ const getErrorMessage = (status: number): string => {
|
||||
return $t(errorMap[status] || 'httpMsg.internalServerError')
|
||||
}
|
||||
|
||||
/** 契约错误码 → 本地化文案键(服务端 message 是英文兜底,优先本地化) */
|
||||
const API_CODE_MSG_KEYS: Record<string, string> = {
|
||||
/**
|
||||
* 契约错误码 → 本地化文案键(服务端 message 是英文兜底,优先本地化)。
|
||||
*
|
||||
* T15 全端语言验收(P4b Task 6):按契约 ErrorCodes 枚举全集登记(机器守护
|
||||
* 见同目录 error.spec.ts,遍历 @photo/contracts ErrorCodes 断言无缺漏)——
|
||||
* 登录/会话/框架码复用 httpMsg 既有文案,业务码走 apiMsg 专属词条。
|
||||
*/
|
||||
export const API_CODE_MSG_KEYS: Record<string, string> = {
|
||||
// 401/会话域(模板 httpMsg 既有文案)
|
||||
AUTH_INVALID_CREDENTIALS: 'apiMsg.AUTH_INVALID_CREDENTIALS',
|
||||
AUTH_TOKEN_EXPIRED: 'httpMsg.unauthorized',
|
||||
AUTH_RATE_LIMITED: 'apiMsg.AUTH_RATE_LIMITED',
|
||||
// P2 公开互动匿名限速(管理端可达性低,防御性登记专属文案)
|
||||
RATE_LIMITED: 'apiMsg.RATE_LIMITED',
|
||||
AUTH_FORBIDDEN: 'httpMsg.forbidden',
|
||||
// 框架/路由 404
|
||||
NOT_FOUND: 'httpMsg.notFound',
|
||||
// 相册/照片域
|
||||
ALBUM_NOT_FOUND: 'apiMsg.ALBUM_NOT_FOUND',
|
||||
ALBUM_NOT_PUBLIC: 'apiMsg.ALBUM_NOT_PUBLIC',
|
||||
ALBUM_UPLOAD_DISABLED: 'apiMsg.ALBUM_UPLOAD_DISABLED',
|
||||
PHOTO_NOT_FOUND: 'apiMsg.PHOTO_NOT_FOUND',
|
||||
PHOTO_NOT_PUBLIC: 'apiMsg.PHOTO_NOT_PUBLIC',
|
||||
// P4a 下载两级开关(A18)
|
||||
DOWNLOAD_DISABLED: 'apiMsg.DOWNLOAD_DISABLED',
|
||||
// 成员/上传域
|
||||
MEMBER_CONFLICT: 'apiMsg.MEMBER_CONFLICT',
|
||||
UPLOAD_SESSION_CONFLICT: 'apiMsg.UPLOAD_SESSION_CONFLICT',
|
||||
UPLOAD_VERIFY_FAILED: 'apiMsg.UPLOAD_VERIFY_FAILED',
|
||||
// 服务间内部同步鉴权(/internal/**):管理端不可达,防御性登记
|
||||
SYNC_AUTH_FAILED: 'apiMsg.SYNC_AUTH_FAILED',
|
||||
// 通用
|
||||
VALIDATION_FAILED: 'apiMsg.VALIDATION_FAILED',
|
||||
INTERNAL_ERROR: 'httpMsg.internalServerError'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user