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 ( <>