diff --git a/apps/web/__tests__/i18n-parity.test.ts b/apps/web/__tests__/i18n-parity.test.ts new file mode 100644 index 0000000..f547808 --- /dev/null +++ b/apps/web/__tests__/i18n-parity.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync, readdirSync } from "fs"; +import { join } from "path"; + +const MESSAGES_DIR = join(__dirname, "..", "messages"); + +function collectKeys(obj: unknown, prefix = ""): string[] { + if (obj === null || typeof obj !== "object") return []; + const keys: string[] = []; + for (const [k, v] of Object.entries(obj as Record)) { + const path = prefix ? `${prefix}.${k}` : k; + if (v !== null && typeof v === "object" && !Array.isArray(v)) { + keys.push(...collectKeys(v, path)); + } else { + keys.push(path); + } + } + return keys; +} + +describe("i18n messages parity", () => { + const enPath = join(MESSAGES_DIR, "en.json"); + const en = JSON.parse(readFileSync(enPath, "utf-8")) as unknown; + const enKeys = new Set(collectKeys(en)); + + const localeFiles = readdirSync(MESSAGES_DIR).filter( + (f) => f.endsWith(".json") && f !== "en.json" + ); + + for (const file of localeFiles) { + it(`${file} has the same key set as en.json`, () => { + const data = JSON.parse( + readFileSync(join(MESSAGES_DIR, file), "utf-8") + ) as unknown; + const keys = new Set(collectKeys(data)); + + const missing = [...enKeys].filter((k) => !keys.has(k)); + const extra = [...keys].filter((k) => !enKeys.has(k)); + + expect({ file, missing, extra }).toEqual({ + file, + missing: [], + extra: [], + }); + }); + } +}); diff --git a/apps/web/__tests__/lib/page-budget.test.ts b/apps/web/__tests__/lib/page-budget.test.ts new file mode 100644 index 0000000..d68f3fa --- /dev/null +++ b/apps/web/__tests__/lib/page-budget.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from "vitest"; +import { locales } from "../../i18n/config"; +import { getAllTools, getAllCategories } from "@utils-live/tools"; + +// Cloudflare Pages free plan caps a single deployment at 20,000 pages. +// generateStaticParams emits (locales × (tools + categories + static + blog)) +// so re-enabling locales without checking this silently breaks deploys. +// Keep a comfortable headroom below the ceiling. +const PAGE_BUDGET_CEILING = 19500; +const STATIC_PAGES_PER_LOCALE = 10; // home, about, contact, privacy, tools, etc. + +describe("Cloudflare Pages page-budget guard", () => { + it("total generated static pages stays under the 20k ceiling", () => { + const toolCount = getAllTools().length; + const categoryCount = getAllCategories().length; + const estimated = + locales.length * (toolCount + categoryCount + STATIC_PAGES_PER_LOCALE); + + expect({ locales: locales.length, estimated }).toMatchObject({ + locales: locales.length, + }); + expect(estimated).toBeLessThan(PAGE_BUDGET_CEILING); + }); +}); diff --git a/apps/web/app/[locale]/page.tsx b/apps/web/app/[locale]/page.tsx index 5520366..8886d28 100644 --- a/apps/web/app/[locale]/page.tsx +++ b/apps/web/app/[locale]/page.tsx @@ -17,6 +17,7 @@ import { ToolDemo } from "@/components/marketing/tool-demo"; import { CategoryShowcase } from "@/components/marketing/category-showcase"; import { FeatureCards } from "@/components/marketing/feature-cards"; import { CTASection } from "@/components/marketing/cta-section"; +import { MotionProvider } from "@/components/providers/motion-provider"; import { buildAlternates } from "@/lib/alternates"; const toolCountLabel = getToolCountLabel(); @@ -70,35 +71,37 @@ export default async function HomePage({
- ({ - id, - name: - categoryMetaMessages?.[id]?.name ?? - categories.find((c) => c.id === id)?.name ?? + + ({ id, - }))} - /> - - - - + name: + categoryMetaMessages?.[id]?.name ?? + categories.find((c) => c.id === id)?.name ?? + id, + }))} + /> + + + + +