From e517848ecfc0b282c37c18bfcc0b6ac4f899adb9 Mon Sep 17 00:00:00 2001 From: bntvllnt Date: Wed, 13 May 2026 06:31:04 +0200 Subject: [PATCH 1/2] feat(registry): add localized registry routes --- .github/workflows/ci.yml | 3 + .../{ => [locale]}/components/[slug]/page.tsx | 107 +- .../app/{ => [locale]}/components/page.tsx | 64 +- .../registry/app/{ => [locale]}/docs/page.tsx | 45 +- apps/registry/app/[locale]/layout.tsx | 140 +++ .../app/[locale]/llms-full.txt/route.ts | 85 ++ apps/registry/app/[locale]/llms.txt/route.ts | 78 ++ .../registry/app/{ => [locale]}/not-found.tsx | 21 +- apps/registry/app/[locale]/page.tsx | 65 + .../app/{ => [locale]}/philosophy/page.tsx | 43 +- apps/registry/app/[locale]/report/page.tsx | 57 + .../{ => [locale]}/report/report-bug-form.tsx | 83 +- .../app/[locale]/request-component/page.tsx | 55 + .../request-component-form.tsx | 83 +- apps/registry/app/{ => [locale]}/vs/page.tsx | 61 +- .../app/{ => [locale]}/vs/shadcn/page.tsx | 79 +- apps/registry/app/layout.tsx | 113 -- apps/registry/app/llms-full.txt/route.ts | 26 +- apps/registry/app/llms.txt/route.ts | 28 +- apps/registry/app/page.tsx | 42 - apps/registry/app/r/[name]/route.ts | 56 +- apps/registry/app/report/page.tsx | 43 - apps/registry/app/request-component/page.tsx | 33 - apps/registry/app/sitemap.ts | 72 +- apps/registry/components/footer/footer.tsx | 109 +- apps/registry/components/header/header.tsx | 61 +- apps/registry/components/landing/landing.tsx | 109 +- .../{components.mdx => components/en.mdx} | 0 apps/registry/content/pages/components/fr.mdx | 9 + .../content/pages/{docs.mdx => docs/en.mdx} | 0 apps/registry/content/pages/docs/fr.mdx | 33 + .../content/pages/{home.mdx => home/en.mdx} | 0 apps/registry/content/pages/home/fr.mdx | 62 + .../{philosophy.mdx => philosophy/en.mdx} | 0 apps/registry/content/pages/philosophy/fr.mdx | 62 + apps/registry/i18n/locales.ts | 4 + apps/registry/i18n/request.ts | 19 + apps/registry/i18n/routing.ts | 20 + apps/registry/lib/component-metadata.json | 666 ++++++++++ apps/registry/lib/content.ts | 31 +- apps/registry/lib/og.ts | 15 +- apps/registry/lib/registry-i18n.ts | 26 + apps/registry/lib/seo.ts | 46 +- apps/registry/lib/sidebar-sections.ts | 116 +- apps/registry/messages/en.json | 152 +++ apps/registry/messages/fr.json | 152 +++ apps/registry/next.config.mjs | 6 +- apps/registry/package.json | 2 + apps/registry/proxy.ts | 9 + apps/registry/registry.json | 1108 +++++++++++++---- .../scripts/check-page-translations.ts | 46 + .../scripts/generate-component-metadata.ts | 3 + .../scripts/inline-component-source.ts | 8 + .../scripts/stamp-registry-metadata.ts | 6 + apps/registry/types/registry.ts | 5 + pnpm-lock.yaml | 273 +++- 56 files changed, 3777 insertions(+), 863 deletions(-) rename apps/registry/app/{ => [locale]}/components/[slug]/page.tsx (76%) rename apps/registry/app/{ => [locale]}/components/page.tsx (60%) rename apps/registry/app/{ => [locale]}/docs/page.tsx (71%) create mode 100644 apps/registry/app/[locale]/layout.tsx create mode 100644 apps/registry/app/[locale]/llms-full.txt/route.ts create mode 100644 apps/registry/app/[locale]/llms.txt/route.ts rename apps/registry/app/{ => [locale]}/not-found.tsx (81%) create mode 100644 apps/registry/app/[locale]/page.tsx rename apps/registry/app/{ => [locale]}/philosophy/page.tsx (67%) create mode 100644 apps/registry/app/[locale]/report/page.tsx rename apps/registry/app/{ => [locale]}/report/report-bug-form.tsx (68%) create mode 100644 apps/registry/app/[locale]/request-component/page.tsx rename apps/registry/app/{ => [locale]}/request-component/request-component-form.tsx (67%) rename apps/registry/app/{ => [locale]}/vs/page.tsx (73%) rename apps/registry/app/{ => [locale]}/vs/shadcn/page.tsx (86%) delete mode 100644 apps/registry/app/layout.tsx delete mode 100644 apps/registry/app/page.tsx delete mode 100644 apps/registry/app/report/page.tsx delete mode 100644 apps/registry/app/request-component/page.tsx rename apps/registry/content/pages/{components.mdx => components/en.mdx} (100%) create mode 100644 apps/registry/content/pages/components/fr.mdx rename apps/registry/content/pages/{docs.mdx => docs/en.mdx} (100%) create mode 100644 apps/registry/content/pages/docs/fr.mdx rename apps/registry/content/pages/{home.mdx => home/en.mdx} (100%) create mode 100644 apps/registry/content/pages/home/fr.mdx rename apps/registry/content/pages/{philosophy.mdx => philosophy/en.mdx} (100%) create mode 100644 apps/registry/content/pages/philosophy/fr.mdx create mode 100644 apps/registry/i18n/locales.ts create mode 100644 apps/registry/i18n/request.ts create mode 100644 apps/registry/i18n/routing.ts create mode 100644 apps/registry/lib/registry-i18n.ts create mode 100644 apps/registry/messages/en.json create mode 100644 apps/registry/messages/fr.json create mode 100644 apps/registry/proxy.ts create mode 100644 apps/registry/scripts/check-page-translations.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9a769c1..a0f55ceb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,9 @@ jobs: - name: Build run: pnpm build + - name: Check page translations + run: pnpm -F @vllnt/ui-registry check:translations + - name: Test run: pnpm test:once diff --git a/apps/registry/app/components/[slug]/page.tsx b/apps/registry/app/[locale]/components/[slug]/page.tsx similarity index 76% rename from apps/registry/app/components/[slug]/page.tsx rename to apps/registry/app/[locale]/components/[slug]/page.tsx index 827ecfa4..83a2196d 100644 --- a/apps/registry/app/components/[slug]/page.tsx +++ b/apps/registry/app/[locale]/components/[slug]/page.tsx @@ -4,19 +4,17 @@ import path from "node:path"; import { Breadcrumb, CodeBlock, Sidebar, TableOfContents } from "@vllnt/ui"; import { ExternalLink } from "lucide-react"; import type { Metadata } from "next"; -import Link from "next/link"; import { notFound } from "next/navigation"; +import { getTranslations, setRequestLocale } from "next-intl/server"; import { QuickAdd } from "@/components/quick-add"; import { StorybookEmbed } from "@/components/storybook-embed"; +import { Link, type Locale, routing } from "@/i18n/routing"; import componentMetadata from "@/lib/component-metadata.json"; -import { - breadcrumbLd, - jsonLdScript, - softwareSourceCodeLd, -} from "@/lib/jsonld"; +import { breadcrumbLd, jsonLdScript, softwareSourceCodeLd } from "@/lib/jsonld"; import { generateOGMetadata, generateTwitterMetadata } from "@/lib/og"; -import { canonical } from "@/lib/seo"; +import { getLocalizedDescription } from "@/lib/registry-i18n"; +import { canonical, languageAlternates, localizePathname } from "@/lib/seo"; import { getCategoryForComponent, getSidebarSections, @@ -25,7 +23,7 @@ import registryData from "@/registry.json"; import type { Registry, RegistryComponent } from "@/types/registry"; type Props = { - params: Promise<{ slug: string }>; + params: Promise<{ locale: Locale; slug: string }>; }; const registry = registryData as Registry; @@ -45,13 +43,18 @@ const STORYBOOK_URL = process.env.NEXT_PUBLIC_STORYBOOK_URL ?? "http://localhost:6006"; export async function generateStaticParams() { - return registry.items + const components = registry.items .filter( (item): item is RegistryComponent => item.type === "registry:component", ) - .map((item) => ({ - slug: item.name, - })); + .map((item) => item.name); + + return routing.locales.flatMap((locale) => + components.map((slug) => ({ + locale, + slug, + })), + ); } function getNpmUrl(packageName: string): string { @@ -59,7 +62,7 @@ function getNpmUrl(packageName: string): string { } export async function generateMetadata(props: Props): Promise { - const { slug } = await props.params; + const { locale, slug } = await props.params; const component = registry.items.find( (item): item is RegistryComponent => item.name === slug && item.type === "registry:component", @@ -72,7 +75,7 @@ export async function generateMetadata(props: Props): Promise { const meta = metadata_map[slug]; const category = getCategoryForComponent(slug); const title = meta?.title ?? component.title; - const description = meta?.description ?? component.description; + const description = getLocalizedDescription(component, locale); const ogParameters = { category, @@ -82,16 +85,23 @@ export async function generateMetadata(props: Props): Promise { }; return { - alternates: { canonical: canonical(`/components/${slug}`) }, + alternates: { + canonical: canonical(`/components/${slug}`, locale), + languages: languageAlternates(`/components/${slug}`), + }, description, - openGraph: generateOGMetadata(ogParameters), + openGraph: generateOGMetadata(ogParameters, { + locale, + pathname: `/components/${slug}`, + }), title: `${title} - VLLNT UI`, twitter: generateTwitterMetadata(ogParameters), }; } export default async function ComponentPage(props: Props) { - const { slug } = await props.params; + const { locale, slug } = await props.params; + setRequestLocale(locale); const component = registry.items.find( (item): item is RegistryComponent => item.name === slug && item.type === "registry:component", @@ -103,7 +113,9 @@ export default async function ComponentPage(props: Props) { const meta = metadata_map[slug]; const displayTitle = meta?.title ?? component.title ?? component.name; - const displayDescription = meta?.description ?? component.description ?? ""; + const displayDescription = getLocalizedDescription(component, locale); + const t = await getTranslations({ locale, namespace: "pages.component" }); + const common = await getTranslations({ locale, namespace: "common" }); // Read component source for code display let componentCode = ""; @@ -158,17 +170,16 @@ export default async function ComponentPage(props: Props) { const installCommand = `pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/${component.name}.json`; const sections = [ - { id: "installation", title: "Installation" }, - ...(meta?.defaultStoryId ? [{ id: "storybook", title: "Storybook" }] : []), - ...(componentCode ? [{ id: "code", title: "Code" }] : []), + { id: "installation", title: t("installation") }, + ...(meta?.defaultStoryId + ? [{ id: "storybook", title: t("storybook") }] + : []), + ...(componentCode ? [{ id: "code", title: t("code") }] : []), ...(component.dependencies && component.dependencies.length > 0 - ? [{ id: "dependencies", title: "Dependencies" }] + ? [{ id: "dependencies", title: t("dependencies") }] : []), ] as { id: string; title: string }[]; - const SITE_URL = - process.env.NEXT_PUBLIC_SITE_URL ?? "https://ui.vllnt.ai"; - return ( <>