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
4 changes: 4 additions & 0 deletions public/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,5 +152,9 @@
"gettingStarted": "البدء في العمل",
"home": "الرئيسية",
"toggleMenu": "تغيير القائمة"
},
"languageSwitcher": {
"listbox": "اختيار اللغة",
"current": "اللغة الحالية: {name}"
}
}
4 changes: 4 additions & 0 deletions public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,5 +161,9 @@
"gettingStarted": "Getting Started",
"home": "Home",
"toggleMenu": "Toggle menu"
},
"languageSwitcher": {
"listbox": "Select language",
"current": "Current language: {name}"
}
}
4 changes: 4 additions & 0 deletions public/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,5 +157,9 @@
"gettingStarted": "Comenzando",
"home": "Inicio",
"toggleMenu": "Cambiar el menú"
},
"languageSwitcher": {
"listbox": "Seleccionar idioma",
"current": "Idioma actual: {name}"
}
}
4 changes: 4 additions & 0 deletions public/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,9 @@
"gettingStarted": "始めること",
"home": "ホーム",
"toggleMenu": "トーグルメニュー"
},
"languageSwitcher": {
"listbox": "言語を選択",
"current": "現在の言語: {name}"
}
}
43 changes: 24 additions & 19 deletions src/app/docs/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DocsLayout } from "fumadocs-ui/layouts/docs";
import { getLocale } from "next-intl/server";
import { source } from "@/lib/source";
import { localizeTree } from "@/lib/localized-href";
import { SiteLanguageSwitcher } from "@/components/site-language-switcher";
import { baseOptions } from "../layout.config";
import { version as localVersion } from "../../../package.json";

Expand Down Expand Up @@ -52,25 +53,29 @@ export default async function Layout({ children }: { children: ReactNode }) {
{...baseOptions}
sidebar={{
footer: (
<div
key="sidebar-footer"
className="flex items-center gap-1 border-t border-fd-border pt-3"
>
<IconLink href="https://discord.gg/Qqzr2g4KGx" label="Community" path={DISCORD_PATH} />
<IconLink
href="https://github.com/babelize/babelize-elements"
label="GitHub"
path={GITHUB_PATH}
/>
<a
href="/llms-full.txt"
className="ml-1 rounded-md border border-fd-border px-2 py-1 font-mono text-[0.6875rem] text-fd-muted-foreground transition-colors hover:border-fd-primary/40 hover:text-fd-primary"
>
llms-full.txt
</a>
<span className="ml-auto font-mono text-[0.6875rem] text-fd-muted-foreground/60">
v{version}
</span>
<div key="sidebar-footer" className="flex flex-col gap-2 border-t border-fd-border pt-3">
<SiteLanguageSwitcher className="w-full justify-between" />
<div className="flex items-center gap-1">
<IconLink
href="https://discord.gg/Qqzr2g4KGx"
label="Community"
path={DISCORD_PATH}
/>
<IconLink
href="https://github.com/babelize/babelize-elements"
label="GitHub"
path={GITHUB_PATH}
/>
<a
href="/llms-full.txt"
className="ml-1 rounded-md border border-fd-border px-2 py-1 font-mono text-[0.6875rem] text-fd-muted-foreground transition-colors hover:border-fd-primary/40 hover:text-fd-primary"
>
llms-full.txt
</a>
<span className="ml-auto font-mono text-[0.6875rem] text-fd-muted-foreground/60">
v{version}
</span>
</div>
</div>
),
}}
Expand Down
8 changes: 8 additions & 0 deletions src/components/landing/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Image from "next/image";
import { useState } from "react";
import { Menu, X, CornerDownLeft } from "lucide-react";
import { GitHubCounter, RotatingGitHubCounter } from "./github-counter";
import { SiteLanguageSwitcher } from "../site-language-switcher";
import type { GitHubStats } from "@/lib/github";

const navLinks = [
Expand Down Expand Up @@ -50,6 +51,10 @@ export function Navbar({ stats, starsCount }: NavbarProps = {}) {

{/* Right side actions */}
<div className="flex items-center gap-2 sm:gap-3">
<div className="hidden sm:block">
<SiteLanguageSwitcher className="rounded-full text-white/80 hover:bg-white/5 hover:text-white" />
</div>

{/* GitHub counter - desktop (cycles stars → forks → PRs) */}
<div className="hidden sm:block">
<RotatingGitHubCounter stats={resolvedStats} />
Expand Down Expand Up @@ -97,6 +102,9 @@ export function Navbar({ stats, starsCount }: NavbarProps = {}) {
{t(link.label)}
</Link>
))}
<div className="flex justify-center px-2 py-1">
<SiteLanguageSwitcher className="w-full justify-center text-white/80" />
</div>
<div className="mt-1 flex justify-center">
<GitHubCounter
metric="stars"
Expand Down
50 changes: 50 additions & 0 deletions src/components/site-language-switcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"use client";

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

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.
*/
export function SiteLanguageSwitcher({ className }: { className?: string }) {
const locale = useLocale();
const t = useTranslations("languageSwitcher");

const change = (code: string) => {
if (code === locale) return;
document.cookie = `NEXT_LOCALE=${code}; path=/; max-age=${ONE_YEAR}; samesite=lax`;
const { pathname, search, hash } = window.location;
// A full load, not router.push: the root layout (<html lang dir>, messages)
// only re-renders on a document request.
// eslint-disable-next-line @next/next/no-location-assign-relative-destination
window.location.assign(`${pathInLocale(pathname, code)}${search}${hash}`);
};

return (
<LanguageSwitcher
locales={siteLocales}
value={locale}
onValueChange={change}
label="native"
size="sm"
variant="ghost"
align="end"
searchable={false}
labels={{ listbox: t("listbox"), current: (name) => t("current", { name }) }}
className={className}
/>
);
}
21 changes: 21 additions & 0 deletions test/site-language-switcher.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest";
import { pathInLocale } from "@/components/site-language-switcher";

describe("pathInLocale", () => {
it("adds the language prefix, keeping the page", () => {
expect(pathInLocale("/", "ja")).toBe("/ja");
expect(pathInLocale("/docs/getting-started/install", "ar")).toBe(
"/ar/docs/getting-started/install",
);
});

it("swaps one language prefix for another, and drops it for English", () => {
expect(pathInLocale("/es/docs/components", "ja")).toBe("/ja/docs/components");
expect(pathInLocale("/ja", "en")).toBe("/");
expect(pathInLocale("/ar/docs", "en")).toBe("/docs");
});

it("doesn't mistake a path segment for a language", () => {
expect(pathInLocale("/docs/es-module", "ja")).toBe("/ja/docs/es-module");
});
});
Loading