diff --git a/public/locales/ar.json b/public/locales/ar.json
index 57a91db..2fdd339 100644
--- a/public/locales/ar.json
+++ b/public/locales/ar.json
@@ -152,5 +152,9 @@
"gettingStarted": "البدء في العمل",
"home": "الرئيسية",
"toggleMenu": "تغيير القائمة"
+ },
+ "languageSwitcher": {
+ "listbox": "اختيار اللغة",
+ "current": "اللغة الحالية: {name}"
}
}
diff --git a/public/locales/en.json b/public/locales/en.json
index 3f0deda..dc0c6b2 100644
--- a/public/locales/en.json
+++ b/public/locales/en.json
@@ -161,5 +161,9 @@
"gettingStarted": "Getting Started",
"home": "Home",
"toggleMenu": "Toggle menu"
+ },
+ "languageSwitcher": {
+ "listbox": "Select language",
+ "current": "Current language: {name}"
}
}
diff --git a/public/locales/es.json b/public/locales/es.json
index 88d9132..f2b18a3 100644
--- a/public/locales/es.json
+++ b/public/locales/es.json
@@ -157,5 +157,9 @@
"gettingStarted": "Comenzando",
"home": "Inicio",
"toggleMenu": "Cambiar el menú"
+ },
+ "languageSwitcher": {
+ "listbox": "Seleccionar idioma",
+ "current": "Idioma actual: {name}"
}
}
diff --git a/public/locales/ja.json b/public/locales/ja.json
index 1ce9f1f..cc89448 100644
--- a/public/locales/ja.json
+++ b/public/locales/ja.json
@@ -145,5 +145,9 @@
"gettingStarted": "始めること",
"home": "ホーム",
"toggleMenu": "トーグルメニュー"
+ },
+ "languageSwitcher": {
+ "listbox": "言語を選択",
+ "current": "現在の言語: {name}"
}
}
diff --git a/src/app/docs/layout.tsx b/src/app/docs/layout.tsx
index f5f9e9d..68c9620 100644
--- a/src/app/docs/layout.tsx
+++ b/src/app/docs/layout.tsx
@@ -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";
@@ -52,25 +53,29 @@ export default async function Layout({ children }: { children: ReactNode }) {
{...baseOptions}
sidebar={{
footer: (
-
-
-
-
- llms-full.txt
-
-
- v{version}
-
+
),
}}
diff --git a/src/components/landing/navbar.tsx b/src/components/landing/navbar.tsx
index 55ed5c8..5ff256a 100644
--- a/src/components/landing/navbar.tsx
+++ b/src/components/landing/navbar.tsx
@@ -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 = [
@@ -50,6 +51,10 @@ export function Navbar({ stats, starsCount }: NavbarProps = {}) {
{/* Right side actions */}
+
+
+
+
{/* GitHub counter - desktop (cycles stars → forks → PRs) */}
@@ -97,6 +102,9 @@ export function Navbar({ stats, starsCount }: NavbarProps = {}) {
{t(link.label)}
))}
+
+
+
({ 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 (, 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 (
+ t("current", { name }) }}
+ className={className}
+ />
+ );
+}
diff --git a/test/site-language-switcher.test.ts b/test/site-language-switcher.test.ts
new file mode 100644
index 0000000..40fc37c
--- /dev/null
+++ b/test/site-language-switcher.test.ts
@@ -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");
+ });
+});