Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .babelize/local/babelize-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -3310,6 +3310,66 @@
"manual": true
}
}
},
"meta.title": {
"sourceHash": "dca9a93e7b6b3390aa7884b3271b21ceba70bdfbd164fbe534a35c8b82a519ef",
"translatedAt": "2026-11-01T20:14:34.000Z",
"languages": {
"es": "Babelize Elements: componentes de UI de localización de código abierto",
"ja": "Babelize Elements - オープンソースのローカライゼーション UI コンポーネント",
"ar": "Babelize Elements - مكوّنات واجهة التوطين مفتوحة المصدر"
},
"metadata": {
"es": {
"manual": true
},
"ja": {
"manual": true
},
"ar": {
"manual": true
}
}
},
"meta.description": {
"sourceHash": "8b336f7877abdd6c8eebe6c6da6068ace722e50247487348346348977232f1a9",
"translatedAt": "2026-11-01T20:14:34.000Z",
"languages": {
"es": "Componentes de UI de localización de código abierto para React y Tailwind CSS. Selectores de idioma, selectores de configuración regional, widgets de traducción y más, creados por la comunidad.",
"ja": "React と Tailwind CSS 向けのオープンソースのローカライゼーション UI コンポーネント。言語スイッチャー、ロケールピッカー、翻訳ウィジェットなどを、コミュニティと一緒に開発しています。",
"ar": "مكوّنات واجهة التوطين مفتوحة المصدر لـ React و Tailwind CSS: مبدّلات اللغة ومنتقيات الإعدادات المحلية وأدوات الترجمة وغير ذلك، من تطوير المجتمع."
},
"metadata": {
"es": {
"manual": true
},
"ja": {
"manual": true
},
"ar": {
"manual": true
}
}
},
"meta.ogDescription": {
"sourceHash": "c4c92a08345545e848ae0782628d285f03cdb804cf85b42f0bb27af252927961",
"translatedAt": "2026-11-01T20:14:34.000Z",
"languages": {
"es": "Componentes de UI de localización de código abierto para React y Tailwind CSS. Creados por la comunidad.",
"ja": "React と Tailwind CSS 向けのオープンソースのローカライゼーション UI コンポーネント。コミュニティによる開発。",
"ar": "مكوّنات واجهة التوطين مفتوحة المصدر لـ React و Tailwind CSS، من تطوير المجتمع."
},
"metadata": {
"es": {
"manual": true
},
"ja": {
"manual": true
},
"ar": {
"manual": true
}
}
}
}
}
5 changes: 5 additions & 0 deletions public/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@
"current": "اللغة الحالية: {name}",
"listbox": "اختيار اللغة"
},
"meta": {
"description": "مكوّنات واجهة التوطين مفتوحة المصدر لـ React و Tailwind CSS: مبدّلات اللغة ومنتقيات الإعدادات المحلية وأدوات الترجمة وغير ذلك، من تطوير المجتمع.",
"ogDescription": "مكوّنات واجهة التوطين مفتوحة المصدر لـ React و Tailwind CSS، من تطوير المجتمع.",
"title": "Babelize Elements - مكوّنات واجهة التوطين مفتوحة المصدر"
},
"navbar": {
"babelizeElements": "Babelize Elements",
"components": "مكوّنات",
Expand Down
5 changes: 5 additions & 0 deletions public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@
"current": "Current language: {name}",
"listbox": "Select language"
},
"meta": {
"description": "Open-source Localization UI components for React and Tailwind CSS. Language switchers, locale pickers, translation widgets, and more — built by the community.",
"ogDescription": "Open-source Localization UI components for React and Tailwind CSS. Built by the community.",
"title": "Babelize Elements - Open Source Localization UI Components"
},
"navbar": {
"babelizeElements": "Babelize Elements",
"components": "Components",
Expand Down
5 changes: 5 additions & 0 deletions public/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@
"current": "Idioma actual: {name}",
"listbox": "Seleccionar idioma"
},
"meta": {
"description": "Componentes de UI de localización de código abierto para React y Tailwind CSS. Selectores de idioma, selectores de configuración regional, widgets de traducción y más, creados por la comunidad.",
"ogDescription": "Componentes de UI de localización de código abierto para React y Tailwind CSS. Creados por la comunidad.",
"title": "Babelize Elements: componentes de UI de localización de código abierto"
},
"navbar": {
"babelizeElements": "Babelize Elements",
"components": "Componentes",
Expand Down
5 changes: 5 additions & 0 deletions public/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@
"current": "現在の言語: {name}",
"listbox": "言語を選択"
},
"meta": {
"description": "React と Tailwind CSS 向けのオープンソースのローカライゼーション UI コンポーネント。言語スイッチャー、ロケールピッカー、翻訳ウィジェットなどを、コミュニティと一緒に開発しています。",
"ogDescription": "React と Tailwind CSS 向けのオープンソースのローカライゼーション UI コンポーネント。コミュニティによる開発。",
"title": "Babelize Elements - オープンソースのローカライゼーション UI コンポーネント"
},
"navbar": {
"babelizeElements": "Babelize Elements",
"components": "コンポーネント",
Expand Down
20 changes: 18 additions & 2 deletions src/app/docs/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { getLocale } from "next-intl/server";
import defaultMdxComponents from "fumadocs-ui/mdx";
import { source } from "@/lib/source";
import { localizedHref } from "@/lib/localized-href";
import { baseOpenGraph, languageAlternates } from "@/lib/locale-path";
import { locales } from "@/i18n/locales";
import type { ComponentProps } from "react";
import { LanguageSwitcherDemo } from "@/components/docs/language-switcher-demo";
import { Flow, RulebookDownload, Takeaway } from "@/components/docs/rulebook";
Expand Down Expand Up @@ -66,13 +68,27 @@ export async function generateStaticParams() {

export async function generateMetadata(props: { params: Promise<{ slug?: string[] }> }) {
const params = await props.params;
const page = source.getPage(params.slug, await getLocale());
const locale = await getLocale();
const page = source.getPage(params.slug, locale);
if (!page) notFound();

return {
openGraph: {
...baseOpenGraph(locale),
url: page.url,
title: page.data.title,
description: page.data.description,
},
title: page.data.title,
description: page.data.description,
// Self-referencing canonical; resolves against metadataBase in the root layout.
alternates: { canonical: page.url },
// hreflang only for the languages this page is translated into.
alternates: {
canonical: page.url,
languages: languageAlternates(
page.url,
locales.filter((l) => source.getPage(params.slug, l)?.locale === l),
),
},
};
}
73 changes: 37 additions & 36 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { RootProvider } from "fumadocs-ui/provider/next";
import { ThemeProvider } from "next-themes";
import "./globals.css";
import { NextIntlClientProvider, useLocale } from "next-intl";
import { getLocale, getTranslations } from "next-intl/server";
import { localeDirection } from "../i18n/locales";
import { baseOpenGraph } from "@/lib/locale-path";

const geistSans = Geist({
variable: "--font-geist-sans",
Expand All @@ -16,42 +18,41 @@ const geistMono = Geist_Mono({
subsets: ["latin"],
});

export const metadata: Metadata = {
metadataBase: new URL("https://elements.babelize.co/"),
title: {
default: "Babelize Elements - Open Source Localization UI Components",
template: "%s | Babelize Elements",
},
description:
"Open-source Localization UI components for React and Tailwind CSS. Language switchers, locale pickers, translation widgets, and more — built by the community.",
keywords: [
"localization",
"i18n",
"internationalization",
"UI components",
"React components",
"Tailwind CSS",
"language switcher",
"locale picker",
"Babelize",
"open source",
],
openGraph: {
type: "website",
locale: "en_US",
url: "https://elements.babelize.co",
siteName: "Babelize Elements",
title: "Babelize Elements - Open Source Localization UI Components",
description:
"Open-source Localization UI components for React and Tailwind CSS. Built by the community.",
},
twitter: {
card: "summary_large_image",
title: "Babelize Elements - Open Source Localization UI Components",
description:
"Open-source Localization UI components for React and Tailwind CSS. Built by the community.",
},
};
export async function generateMetadata(): Promise<Metadata> {
const locale = await getLocale();
const t = await getTranslations("meta");
return {
metadataBase: new URL("https://elements.babelize.co/"),
title: {
default: t("title"),
template: "%s | Babelize Elements",
},
description: t("description"),
keywords: [
"localization",
"i18n",
"internationalization",
"UI components",
"React components",
"Tailwind CSS",
"language switcher",
"locale picker",
"Babelize",
"open source",
],
// Pages set og:url (and docs their own title); a page's openGraph replaces this one.
openGraph: {
...baseOpenGraph(locale),
title: t("title"),
description: t("ogDescription"),
},
twitter: {
card: "summary_large_image",
title: t("title"),
description: t("ogDescription"),
},
};
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
const locale = useLocale();
Expand Down
19 changes: 16 additions & 3 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,23 @@ import { Faq } from "@/components/landing/faq";
import { Footer } from "@/components/landing/footer";

import { getGitHubStats } from "@/lib/github";
import { getLocale, getTranslations } from "next-intl/server";
import { baseOpenGraph, languageAlternates, pathInLocale } from "@/lib/locale-path";

export const metadata: Metadata = {
alternates: { canonical: "/" },
};
// Each language's own URL is canonical (/ja, not /), with the others as hreflang alternates.
export async function generateMetadata(): Promise<Metadata> {
const [locale, t] = await Promise.all([getLocale(), getTranslations("meta")]);
const url = pathInLocale("/", locale);
return {
alternates: { canonical: url, languages: languageAlternates("/") },
openGraph: {
...baseOpenGraph(locale),
url,
title: t("title"),
description: t("ogDescription"),
},
};
}

export default async function Home() {
const stats = await getGitHubStats();
Expand Down
38 changes: 28 additions & 10 deletions src/app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,36 @@
import type { MetadataRoute } from "next";
import { source } from "@/lib/source";
import { locales } from "@/i18n/locales";
import { languageAlternates, pathInLocale } from "@/lib/locale-path";

const siteUrl = "https://elements.babelize.co";

/** Absolute URL as the pages' canonicals write it (the home page without a trailing slash). */
const absolute = (path: string) => (path === "/" ? siteUrl : `${siteUrl}${path}`);
const absoluteAll = (languages: Record<string, string>) =>
Object.fromEntries(Object.entries(languages).map(([l, path]) => [l, absolute(path)]));

/** Every page in every language it exists in, each listing the others as alternates. */
export default function sitemap(): MetadataRoute.Sitemap {
return [
{ url: `${siteUrl}/`, changeFrequency: "weekly", priority: 1 },
...source
.getLanguages()
.flatMap(({ pages }) => pages)
.map((page) => ({
url: `${siteUrl}${page.url}`,
changeFrequency: "weekly" as const,
const landing = languageAlternates("/");
const entries: MetadataRoute.Sitemap = locales.map((locale) => ({
url: absolute(pathInLocale("/", locale)),
changeFrequency: "weekly",
priority: 1,
alternates: { languages: absoluteAll(landing) },
}));

for (const page of source.getPages()) {
const available = locales.filter((l) => source.getPage(page.slugs, l)?.locale === l);
const languages = absoluteAll(languageAlternates(page.url, available));
for (const locale of available) {
entries.push({
url: absolute(pathInLocale(page.url, locale)),
changeFrequency: "weekly",
priority: 0.8,
})),
];
alternates: { languages },
});
}
}
return entries;
}
11 changes: 2 additions & 9 deletions src/components/site-language-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,12 @@

import { useLocale, useTranslations } from "next-intl";
import { LanguageSwitcher } from "@/registry/components/language-switcher";
import { locales, sourceLocale } from "@/i18n/locales";
import { locales } from "@/i18n/locales";
import { pathInLocale } from "@/lib/locale-path";

const ONE_YEAR = 60 * 60 * 24 * 365;
const siteLocales = locales.map((code) => ({ code }));

/** `pathname` in `locale`: `/docs/x` → `/ja/docs/x`; English has no prefix. */
export function pathInLocale(pathname: string, locale: string): string {
const [, first = "", ...rest] = pathname.split("/");
const bare = locales.includes(first) && first !== sourceLocale ? `/${rest.join("/")}` : pathname;
if (locale === sourceLocale) return bare;
return bare === "/" ? `/${locale}` : `/${locale}${bare}`;
}

/**
* The site's language dropdown, built on our own LanguageSwitcher. Remembers
* the choice in NEXT_LOCALE and reloads the same page in that language.
Expand Down
43 changes: 43 additions & 0 deletions src/lib/locale-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { locales, sourceLocale } from "@/i18n/locales";

/** `pathname` in `locale`: `/docs/x` → `/ja/docs/x`; English has no prefix. */
export function pathInLocale(pathname: string, locale: string): string {
const [, first = "", ...rest] = pathname.split("/");
const bare = locales.includes(first) && first !== sourceLocale ? `/${rest.join("/")}` : pathname;
if (locale === sourceLocale) return bare;
return bare === "/" ? `/${locale}` : `/${locale}${bare}`;
}

/** Open Graph `og:locale` per site locale. */
export const OG_LOCALE: Record<string, string> = {
en: "en_US",
es: "es_ES",
ja: "ja_JP",
ar: "ar_AR",
};

/** The Open Graph fields every page shares, for `locale`; pages add their own url, title and description. */
export function baseOpenGraph(locale: string) {
return {
type: "website" as const,
siteName: "Babelize Elements",
locale: OG_LOCALE[locale] ?? OG_LOCALE.en,
alternateLocale: Object.entries(OG_LOCALE)
.filter(([code]) => code !== locale)
.map(([, og]) => og),
};
}

/**
* hreflang alternates for a page that exists in `available` locales (default:
* all), English as `x-default`. Paths are relative to metadataBase.
*/
export function languageAlternates(
pathname: string,
available: readonly string[] = locales,
): Record<string, string> {
const languages: Record<string, string> = {};
for (const locale of available) languages[locale] = pathInLocale(pathname, locale);
languages["x-default"] = pathInLocale(pathname, sourceLocale);
return languages;
}
Loading
Loading