/** * Pre-Push Content & Quality Check * * Comprehensive static analysis that catches bugs BEFORE push: * 1. i18n: key parity, namespace coverage, ClientWrapper gaps, missing keys * 2. Accessibility: buttons need aria-label or text, images need alt, forms need labels * 3. Conventions: no raw hex colors, no emojis, no unguarded console.error in client code * 4. Email: all mailto: links use valid email format * 5. Environment variables: all process.env.XXX are documented * 6. Code quality: no `any` type in app/ source files * * Run: npx jest app/__tests__/pre-push-check.test.ts --verbose * (or just `npm test` — it's included in the suite) */ import fs from "fs"; import path from "path"; const ROOT = path.resolve(__dirname, "../.."); const SRC_ROOT = path.resolve(__dirname, ".."); const en = JSON.parse(fs.readFileSync(path.join(ROOT, "messages/en.json"), "utf-8")); const de = JSON.parse(fs.readFileSync(path.join(ROOT, "messages/de.json"), "utf-8")); const LOCALES: Record> = { en, de }; function collectPaths(obj: unknown, prefix = ""): string[] { if (obj === null || typeof obj !== "object" || Array.isArray(obj)) return [prefix]; return Object.entries(obj as Record).flatMap(([k, v]) => collectPaths(v, prefix ? `${prefix}.${k}` : k), ); } function resolve(obj: unknown, keyPath: string): unknown { return keyPath.split(".").reduce((o, k) => { if (o === null || typeof o !== "object") return undefined; return (o as Record)[k]; }, obj); } function findTsxFiles(dir: string, excludeTests = true): string[] { return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { const full = path.join(dir, entry.name); if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules" && entry.name !== ".next") { if (excludeTests && entry.name === "__tests__") return []; return findTsxFiles(full, excludeTests); } return entry.name.endsWith(".tsx") || entry.name.endsWith(".ts") ? [full] : []; }); } function readFile(p: string): string { return fs.readFileSync(p, "utf-8"); } // ═══════════════════════════════════════════════════════════════════════════ // 1. i18n: Structural parity // ═══════════════════════════════════════════════════════════════════════════ describe("i18n: message files", () => { it("en.json and de.json have identical key paths", () => { const enPaths = new Set(collectPaths(en)); const dePaths = new Set(collectPaths(de)); const missingInDe = [...enPaths].filter((p) => !dePaths.has(p)); const missingInEn = [...dePaths].filter((p) => !enPaths.has(p)); expect(missingInDe).toEqual([]); expect(missingInEn).toEqual([]); }); it("no empty string values in any locale", () => { for (const [locale, messages] of Object.entries(LOCALES)) { const emptyPaths = collectPaths(messages).filter((p) => { const val = p.split(".").reduce((o, k) => (o as Record)[k], messages); return val === ""; }); if (emptyPaths.length > 0) { throw new Error(`Empty translations in ${locale}: ${emptyPaths.join(", ")}`); } } }); }); // ═══════════════════════════════════════════════════════════════════════════ // 2. i18n: ClientWrapper namespace coverage // ═══════════════════════════════════════════════════════════════════════════ const WRAPPED_COMPONENT_NAMESPACES: Record = { About: ["home.about", "about"], Projects: ["home.projects"], Contact: ["home.contact", "home.contact.form", "home.contact.info"], Footer: ["footer"], }; const CLIENT_WRAPPER_PROVIDES: Record = { AboutClient: ["home.about", "about"], ProjectsClient: ["home.projects"], ContactClient: ["home.contact"], FooterClient: ["footer"], }; describe("i18n: ClientWrappers provide all requested namespaces", () => { const componentToWrapper: Record = { About: "AboutClient", Projects: "ProjectsClient", Contact: "ContactClient", Footer: "FooterClient", }; for (const [component, wrapper] of Object.entries(componentToWrapper)) { const required = WRAPPED_COMPONENT_NAMESPACES[component]; const provided = CLIENT_WRAPPER_PROVIDES[wrapper]; describe(`${component} → ${wrapper}`, () => { for (const ns of required) { it(`provides namespace "${ns}"`, () => { const found = provided.some((p) => ns === p || ns.startsWith(p + ".")); if (!found) { throw new Error( `${component} calls useTranslations("${ns}") but ${wrapper} does not provide it.\n` + ` Provided: [${provided.join(", ")}]\n` + ` Fix: add "${ns}" to the messages object in ${wrapper}`, ); } }); } }); } }); // ═══════════════════════════════════════════════════════════════════════════ // 3. i18n: Static key coverage // ═══════════════════════════════════════════════════════════════════════════ const STATIC_KEYS: Record> = { nav: { Header: ["home", "about", "projects", "contact"] }, common: { "books/page": ["back"], "legal-notice/page": ["back"], "privacy-policy/page": ["back"], ProjectDetailClient: ["back"], ProjectsPageClient: ["backToHome"], }, consent: { ConsentBanner: ["title", "description", "essential", "chat", "alwaysOn", "acceptAll", "acceptSelected", "rejectAll", "hide"] }, "home.hero": { Hero: ["badge", "line1", "line2", "description", "ctaWork", "ctaContact"] }, "home.about": { About: ["title", "p1", "p2", "p3", "funFactTitle", "funFactBody", "hobbiesTitle"] }, "home.about.activity": { ActivityFeed: ["idleStatus", "codingNow", "gaming", "inGame", "listening"] }, "home.about.currentlyReading": { CurrentlyReading: ["title", "progress"] }, "home.about.readBooks": { ReadBooks: ["title", "showMore", "showLess", "readMore", "collapseReview", "finishedAt", "empty"] }, "home.projects": { Projects: ["title", "subtitle", "viewAll", "noProjects"] }, "home.contact": { Contact: ["title", "subtitle"] }, "home.contact.form": { Contact: ["title", "sending", "send", "labels.name", "labels.email", "labels.subject", "labels.message", "placeholders.name", "placeholders.email", "placeholders.subject", "placeholders.message", "errors.nameRequired", "errors.nameMin", "errors.emailRequired", "errors.emailInvalid", "errors.subjectRequired", "errors.subjectMin", "errors.messageRequired", "errors.messageMin", "characters"] }, "home.contact.info": { Contact: ["locationValue"] }, about: { About: ["status", "aiAssistant", "library", "viewAll", "myGear", "gearMain", "gearPC", "gearServer", "gearOS", "curiosity"] }, "projects.list": { ProjectsPageClient: ["title", "intro", "searchPlaceholder", "all", "noResults", "clearFilters"] }, "projects.detail": { ProjectDetailClient: ["liveDemo", "viewSource"] }, footer: { Footer: ["role", "madeIn", "legalNotice", "privacyPolicy", "privacySettings", "privacySettingsTitle", "builtWith", "aiDisclaimer", "backToTop", "systemsOnline"] }, notFound: { "not-found": ["title", "description", "returnHome", "goBack", "exploreWork", "exploreWorkDesc", "viewProjects", "errorReport"] }, books: { "books/page": ["title", "subtitle", "empty"] }, }; describe("i18n: all static translation keys resolve", () => { for (const [namespace, components] of Object.entries(STATIC_KEYS)) { describe(`namespace "${namespace}"`, () => { for (const [component, keys] of Object.entries(components)) { for (const keyPath of keys) { it(`${component} → "${keyPath}"`, () => { for (const locale of Object.keys(LOCALES)) { const nsObj = resolve(LOCALES[locale], namespace); if (nsObj === undefined) throw new Error(`Namespace "${namespace}" missing in ${locale}.json`); const val = resolve(nsObj, keyPath); if (val === undefined) throw new Error(`Key "${keyPath}" missing in ${locale}.json → ${namespace}`); if (val === "") throw new Error(`Key "${keyPath}" is empty string in ${locale}.json → ${namespace}`); } }); } } }); } }); // ═══════════════════════════════════════════════════════════════════════════ // 4. i18n: Unregistered namespaces detected // ═══════════════════════════════════════════════════════════════════════════ describe("i18n: no unregistered useTranslation namespaces", () => { const knownNamespaces = new Set(Object.keys(STATIC_KEYS)); const allTsx = findTsxFiles(SRC_ROOT); const useNamespaceRegex = /useTranslations\(["']([^"']+)["']\)/g; const serverNamespaceRegex = /getTranslations\s*\(\s*\{[^}]*namespace\s*:\s*["']([^"']+)["']/g; const foundInSource = new Map(); for (const file of allTsx) { const content = readFile(file); const rel = path.relative(SRC_ROOT, file); let match: RegExpExecArray | null; useNamespaceRegex.lastIndex = 0; while ((match = useNamespaceRegex.exec(content)) !== null) { if (!foundInSource.has(match[1])) foundInSource.set(match[1], []); foundInSource.get(match[1])!.push(rel); } serverNamespaceRegex.lastIndex = 0; while ((match = serverNamespaceRegex.exec(content)) !== null) { if (!foundInSource.has(match[1])) foundInSource.set(match[1], []); foundInSource.get(match[1])!.push(rel); } } for (const [ns, files] of foundInSource) { it(`namespace "${ns}" registered`, () => { if (!knownNamespaces.has(ns)) { throw new Error(`Namespace "${ns}" used in source but not in STATIC_KEYS.\nUsed in: ${files.join(", ")}`); } }); } }); // ═══════════════════════════════════════════════════════════════════════════ // 5. Accessibility: Buttons need aria-label or visible text // ═══════════════════════════════════════════════════════════════════════════ describe("Accessibility: buttons have accessible names", () => { const appFiles = findTsxFiles(path.join(SRC_ROOT, "components")); const ariaLabelRegex = /aria-label=["'{]/; const titleAttrRegex = /title=["'{]/; const violations: string[] = []; for (const file of appFiles) { const content = readFile(file); const rel = path.relative(SRC_ROOT, file); const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (!line.includes("]*>\s*\S/.test(line) || /]*>[^<]/.test(line); if (!hasAriaLabel && !hasTitle && !hasVisibleText) { const isIconButton = /<\w+\s[^>]*size\s*=\s*\{?\d/.test(line) || line.includes("") || line.includes(" { if (violations.length > 0) { throw new Error(`Found ${violations.length} button(s) missing accessible names:\n${violations.join("\n")}`); } }); }); // ═══════════════════════════════════════════════════════════════════════════ // 6. Accessibility: Images must have alt text // ═══════════════════════════════════════════════════════════════════════════ describe("Accessibility: images have alt text", () => { it("no tags missing alt attribute", () => { const appFiles = findTsxFiles(SRC_ROOT); const violations: string[] = []; for (const file of appFiles) { if (file.includes("__tests__")) continue; const content = readFile(file); const rel = path.relative(SRC_ROOT, file); const imgRegex = /]*>/g; let match: RegExpExecArray | null; while ((match = imgRegex.exec(content)) !== null) { const tag = match[0]; if (!tag.includes("alt=")) { const lineNum = content.slice(0, match.index).split("\n").length; violations.push(`${rel}:${lineNum}`); } } imgRegex.lastIndex = 0; } if (violations.length > 0) { throw new Error(`Found tags without alt attribute:\n${violations.join("\n")}`); } }); }); // ═══════════════════════════════════════════════════════════════════════════ // 7. Accessibility: Forms must have aria-label or labeled inputs // ═══════════════════════════════════════════════════════════════════════════ describe("Accessibility: forms have labels", () => { it("all
elements have aria-label or