mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-11-01 05:15:22 +08:00
feat(docs): persist browser and manual locale preferences (#123)
* feat(docs): persist browser and manual locale preferences * test(docs): include localized 404 pages in provider coverage --------- Co-authored-by: dadachann <[email protected]>
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, type ReactNode } from "react";
|
||||
import { I18nProvider, type I18nProviderProps } from "fumadocs-ui/contexts/i18n";
|
||||
import { useRouter } from "fumapress/client";
|
||||
import { preferredLocale, saveLocale, localeDestination, LOCALES } from "./locale-preference.mjs";
|
||||
|
||||
/** Reuse both stock selectors via their native onLocaleChange callback. */
|
||||
export function LocalePreferenceProvider({ i18n, children }: {
|
||||
i18n: I18nProviderProps;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
useEffect(() => { preferredLocale(); }, []);
|
||||
return <I18nProvider {...i18n} onLocaleChange={(locale) => {
|
||||
if (!LOCALES.includes(locale)) return;
|
||||
saveLocale(locale);
|
||||
const alternates = Array.from(document.querySelectorAll<HTMLLinkElement>('link[rel="alternate"][hreflang]'))
|
||||
.map((link) => ({ language: link.hreflang, href: link.href }));
|
||||
// Fumapress adds /docs itself. Use canonical hreflang routes for API pages
|
||||
// whose translated slugs differ, and a guide when no translation exists.
|
||||
const target = localeDestination(locale, alternates, location.origin);
|
||||
router.push(target.slice("/docs".length) + location.search + location.hash);
|
||||
}}>{children}</I18nProvider>;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export const STORAGE_KEY: string;
|
||||
export const LOCALES: readonly string[];
|
||||
export function detectLocale(saved: unknown, languages?: readonly unknown[]): string;
|
||||
export function saveLocale(locale: string, browser?: Window): void;
|
||||
export function preferredLocale(browser?: Window): string;
|
||||
export function localeDestination(locale: string, alternates: {language: string; href: string}[], origin: string): string;
|
||||
export function enterDocs(browser?: Window): void;
|
||||
@@ -0,0 +1,48 @@
|
||||
export const STORAGE_KEY = "langbot-docs-locale";
|
||||
export const LOCALES = ["en", "zh", "ja"];
|
||||
|
||||
export function detectLocale(saved, languages = []) {
|
||||
if (LOCALES.includes(saved)) return saved;
|
||||
for (const language of languages) {
|
||||
if (typeof language !== "string") continue;
|
||||
const base = language.toLowerCase().split("-")[0];
|
||||
if (LOCALES.includes(base)) return base;
|
||||
}
|
||||
return "en";
|
||||
}
|
||||
|
||||
export function saveLocale(locale, browser = window) {
|
||||
if (!LOCALES.includes(locale)) return;
|
||||
try { browser.localStorage.setItem(STORAGE_KEY, locale); } catch { /* Private/blocked storage. */ }
|
||||
}
|
||||
|
||||
export function preferredLocale(browser = window) {
|
||||
let saved;
|
||||
try { saved = browser.localStorage.getItem(STORAGE_KEY); } catch { /* Storage may throw on access. */ }
|
||||
const locale = detectLocale(saved, browser.navigator.languages?.length
|
||||
? browser.navigator.languages : [browser.navigator.language]);
|
||||
// Initialize from the browser, never from an explicit shared page's locale.
|
||||
if (saved !== locale) saveLocale(locale, browser);
|
||||
return locale;
|
||||
}
|
||||
|
||||
export function localeDestination(locale, alternates, origin) {
|
||||
if (!LOCALES.includes(locale)) locale = "en";
|
||||
for (const alternate of alternates) {
|
||||
if (alternate.language.toLowerCase().split("-")[0] !== locale) continue;
|
||||
try {
|
||||
const url = new URL(alternate.href, origin);
|
||||
// Exported canonicals point to production even on previews. Only accept
|
||||
// our canonical host or the current origin, and an exact locale prefix.
|
||||
if ((url.origin === origin || url.origin === "https://langbot.app") &&
|
||||
url.pathname.startsWith(`/docs/${locale}/`)) return url.pathname;
|
||||
} catch { /* Ignore malformed alternate metadata. */ }
|
||||
}
|
||||
return `/docs/${locale}/insight/guide`;
|
||||
}
|
||||
|
||||
export function enterDocs(browser = window) {
|
||||
if (!/^\/docs\/?$/.test(browser.location.pathname)) return;
|
||||
const locale = preferredLocale(browser);
|
||||
browser.location.replace(`/docs/${locale}/insight/guide${browser.location.search}${browser.location.hash}`);
|
||||
}
|
||||
Reference in New Issue
Block a user