diff --git a/src/app/docs/components/DocsI18n.tsx b/src/app/docs/components/DocsI18n.tsx deleted file mode 100644 index 49a76a01f7..0000000000 --- a/src/app/docs/components/DocsI18n.tsx +++ /dev/null @@ -1,200 +0,0 @@ -"use client"; - -import { useMemo, useState } from "react"; -import { useSearchParams, useRouter, usePathname } from "next/navigation"; - -type Locale = "en" | "pt-BR" | "es" | "fr" | "de" | "ja" | "zh-CN" | "ko" | "ru" | "ar"; - -const SECTION_LABELS: Record> = { - en: { - "Getting Started": "Getting Started", - Features: "Features", - "API & Protocols": "API & Protocols", - Deployment: "Deployment", - Operations: "Operations", - Development: "Development", - }, - "pt-BR": { - "Getting Started": "Primeiros Passos", - Features: "Recursos", - "API & Protocols": "API & Protocolos", - Deployment: "Implantação", - Operations: "Operações", - Development: "Desenvolvimento", - }, - es: { - "Getting Started": "Primeros Pasos", - Features: "Características", - "API & Protocols": "API y Protocolos", - Deployment: "Despliegue", - Operations: "Operaciones", - Development: "Desarrollo", - }, - fr: { - "Getting Started": "Démarrage", - Features: "Fonctionnalités", - "API & Protocols": "API et Protocoles", - Deployment: "Déploiement", - Operations: "Opérations", - Development: "Développement", - }, - de: { - "Getting Started": "Erste Schritte", - Features: "Funktionen", - "API & Protocols": "API & Protokolle", - Deployment: "Bereitstellung", - Operations: "Betrieb", - Development: "Entwicklung", - }, - ja: { - "Getting Started": "はじめに", - Features: "機能", - "API & Protocols": "APIとプロトコル", - Deployment: "デプロイ", - Operations: "運用", - Development: "開発", - }, - "zh-CN": { - "Getting Started": "快速开始", - Features: "功能", - "API & Protocols": "API与协议", - Deployment: "部署", - Operations: "运维", - Development: "开发", - }, - ko: { - "Getting Started": "시작하기", - Features: "기능", - "API & Protocols": "API 및 프로토콜", - Deployment: "배포", - Operations: "운영", - Development: "개발", - }, - ru: { - "Getting Started": "Начало работы", - Features: "Возможности", - "API & Protocols": "API и Протоколы", - Deployment: "Развертывание", - Operations: "Эксплуатация", - Development: "Разработка", - }, - ar: { - "Getting Started": "البدء", - Features: "الميزات", - "API & Protocols": "API والبروتوكولات", - Deployment: "النشر", - Operations: "العمليات", - Development: "التطوير", - }, -}; - -function detectLocale(): Locale { - if (typeof navigator === "undefined") return "en"; - const lang = navigator.language; - if (lang.startsWith("pt-BR")) return "pt-BR"; - if (lang.startsWith("es")) return "es"; - if (lang.startsWith("fr")) return "fr"; - if (lang.startsWith("de")) return "de"; - if (lang.startsWith("ja")) return "ja"; - if (lang.startsWith("zh")) return "zh-CN"; - if (lang.startsWith("ko")) return "ko"; - if (lang.startsWith("ru")) return "ru"; - if (lang.startsWith("ar")) return "ar"; - return "en"; -} - -export function useDocsLocale() { - const searchParams = useSearchParams(); - return useMemo(() => { - const langParam = searchParams.get("lang"); - if (langParam && langParam in SECTION_LABELS) return langParam as Locale; - return detectLocale(); - }, [searchParams]); -} - -export function useLocalizedSectionTitle(englishTitle: string): string { - const locale = useDocsLocale(); - return SECTION_LABELS[locale]?.[englishTitle] ?? englishTitle; -} - -export function getAvailableLocales(): Locale[] { - return Object.keys(SECTION_LABELS) as Locale[]; -} - -export const LOCALE_NAMES: Record = { - en: "English", - "pt-BR": "Português (Brasil)", - es: "Español", - fr: "Français", - de: "Deutsch", - ja: "日本語", - "zh-CN": "简体中文", - ko: "한국어", - ru: "Русский", - ar: "العربية", -}; - -const COMMON_LOCALES: Locale[] = ["en", "pt-BR", "es", "fr", "de", "ja", "zh-CN", "ko", "ru", "ar"]; - -export function DocsLocaleSwitcher() { - const searchParams = useSearchParams(); - const router = useRouter(); - const pathname = usePathname(); - const [isOpen, setIsOpen] = useState(false); - const currentLocale = useMemo(() => { - const langParam = searchParams.get("lang"); - if (langParam && langParam in SECTION_LABELS) return langParam as Locale; - if (typeof navigator !== "undefined") { - const navLocale = detectLocale(); - if (navLocale in SECTION_LABELS) return navLocale; - } - return "en"; - }, [searchParams]); - - const handleLocaleChange = (locale: Locale) => { - setIsOpen(false); - const params = new URLSearchParams(searchParams.toString()); - if (locale === "en") { - params.delete("lang"); - } else { - params.set("lang", locale); - } - const queryString = params.toString(); - router.push(`${pathname}${queryString ? `?${queryString}` : ""}`); - }; - - return ( -
- - {isOpen && ( -
- {COMMON_LOCALES.map((locale) => ( - - ))} -
- )} -
- ); -} diff --git a/src/app/docs/layout.tsx b/src/app/docs/layout.tsx index 4890d479c4..fbab0e946a 100644 --- a/src/app/docs/layout.tsx +++ b/src/app/docs/layout.tsx @@ -1,7 +1,7 @@ import Link from "next/link"; import { ReactNode, Suspense } from "react"; import { DocsSidebarClient } from "./components/DocsSidebarClient"; -import { DocsLocaleSwitcher } from "./components/DocsI18n"; +import LanguageSelector from "@/shared/components/LanguageSelector"; export const metadata = { title: { @@ -56,7 +56,7 @@ export default function DocsLayout({ children }: { children: ReactNode }) { }> - + diff --git a/tests/unit/docs-site-overhaul.test.ts b/tests/unit/docs-site-overhaul.test.ts index e2f25d3e20..584c828268 100644 --- a/tests/unit/docs-site-overhaul.test.ts +++ b/tests/unit/docs-site-overhaul.test.ts @@ -314,23 +314,31 @@ test("WhatsNewSection is importable", async () => { }); // ────────────────────────────────────────────── -// i18n locale system +// i18n locale system (next-intl + config/i18n.json) // ────────────────────────────────────────────── -test("DocsI18n is importable", async () => { - const mod = await import("../../src/app/docs/components/DocsI18n"); - assert.ok(mod.useLocalizedSectionTitle, "useLocalizedSectionTitle should be exported"); - assert.ok(mod.getAvailableLocales, "getAvailableLocales should be exported"); - assert.ok(mod.LOCALE_NAMES, "LOCALE_NAMES should be exported"); - assert.equal(mod.LOCALE_NAMES.en, "English"); - assert.equal(mod.LOCALE_NAMES["zh-CN"], "简体中文"); +test("docs locale handling uses the shared next-intl config", async () => { + const cfg = await import("../../src/i18n/config"); + assert.ok(Array.isArray(cfg.LANGUAGES), "LANGUAGES should be exported"); + assert.ok(cfg.LANGUAGES.length >= 10, "LANGUAGES should cover all configured locales"); + const codes = cfg.LANGUAGES.map((l) => l.code); + assert.ok(codes.includes("en")); + assert.ok(codes.includes("pt-BR")); + assert.ok(codes.includes("zh-CN")); + const en = cfg.LANGUAGES.find((l) => l.code === "en"); + const zh = cfg.LANGUAGES.find((l) => l.code === "zh-CN"); + assert.equal(en?.english, "English"); + assert.equal(zh?.native, "中文 (简体)"); }); -test("getAvailableLocales returns 10 locales", async () => { - const { getAvailableLocales } = await import("../../src/app/docs/components/DocsI18n"); - const locales = getAvailableLocales(); - assert.equal(locales.length, 10); - assert.ok(locales.includes("en")); - assert.ok(locales.includes("pt-BR")); - assert.ok(locales.includes("zh-CN")); +test("docs language selector reuses the global LanguageSelector component", async () => { + const selector = await import("../../src/shared/components/LanguageSelector"); + assert.ok(selector.default, "LanguageSelector default export should exist"); +}); + +test("DocsI18n shim is no longer present (replaced by next-intl)", async () => { + await assert.rejects( + async () => import("../../src/app/docs/components/DocsI18n"), + "DocsI18n.tsx should be removed; docs UI uses next-intl directly" + ); });