feat: 添加国际化支持,优化语言切换器和登录界面,增强用户体验
Some checks failed
lotteryfront CI / build (push) Has been cancelled
Some checks failed
lotteryfront CI / build (push) Has been cancelled
This commit is contained in:
25
src/components/i18n-hydration-provider.tsx
Normal file
25
src/components/i18n-hydration-provider.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
|
||||
|
||||
import { syncPreferredLanguage } from "@/i18n";
|
||||
|
||||
const I18nHydrationContext = createContext(false);
|
||||
|
||||
/** hydration 完成前保持 DEFAULT_LANGUAGE,与 SSR 一致;完成后再同步用户偏好语言。 */
|
||||
export function I18nHydrationProvider({ children }: { children: ReactNode }) {
|
||||
const [hydrated, setHydrated] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
syncPreferredLanguage();
|
||||
setHydrated(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<I18nHydrationContext.Provider value={hydrated}>{children}</I18nHydrationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useI18nHydrated(): boolean {
|
||||
return useContext(I18nHydrationContext);
|
||||
}
|
||||
@@ -6,8 +6,15 @@ import { ChevronDown, Globe } from "lucide-react";
|
||||
import { useMemo, useState, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useI18nHydrated } from "@/components/i18n-hydration-provider";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { normalizeLanguage, SUPPORTED_LANGUAGES, type AppLanguage } from "@/i18n";
|
||||
import zhCommon from "@/i18n/locales/zh/common.json";
|
||||
import {
|
||||
DEFAULT_LANGUAGE,
|
||||
normalizeLanguage,
|
||||
SUPPORTED_LANGUAGES,
|
||||
type AppLanguage,
|
||||
} from "@/i18n";
|
||||
import { playerViewportPopoverMaxClass } from "@/lib/player-viewport";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -33,6 +40,7 @@ export function LanguageSwitcher({
|
||||
const { i18n, t } = useTranslation("common");
|
||||
const active = normalizeLanguage(i18n.language) as AppLanguage;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const mounted = useI18nHydrated();
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
@@ -49,9 +57,14 @@ export function LanguageSwitcher({
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
const currentLabel = useFullLabel
|
||||
? (options.find((o) => o.code === active)?.label ?? active.toUpperCase())
|
||||
: (options.find((o) => o.code === active)?.short ?? active.toUpperCase());
|
||||
const displayLang = mounted ? active : DEFAULT_LANGUAGE;
|
||||
const currentLabel = mounted
|
||||
? useFullLabel
|
||||
? (options.find((o) => o.code === displayLang)?.label ?? displayLang.toUpperCase())
|
||||
: (options.find((o) => o.code === displayLang)?.short ?? displayLang.toUpperCase())
|
||||
: useFullLabel
|
||||
? zhCommon.language[displayLang]
|
||||
: zhCommon.languageShort[displayLang];
|
||||
const currentFlag = options.find((o) => o.code === active)?.flag ?? "";
|
||||
|
||||
const variantStyles = {
|
||||
@@ -105,7 +118,9 @@ export function LanguageSwitcher({
|
||||
>
|
||||
<Globe className="size-4" aria-hidden />
|
||||
{showFlag && currentFlag ? <span className="text-base">{currentFlag}</span> : null}
|
||||
{showLabel ? <span>{currentLabel}</span> : null}
|
||||
{showLabel ? (
|
||||
<span suppressHydrationWarning>{currentLabel}</span>
|
||||
) : null}
|
||||
<ChevronDown
|
||||
className={cn("size-4 transition-transform duration-200", isOpen && "rotate-180")}
|
||||
aria-hidden
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, type ReactNode } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { I18nHydrationProvider } from "@/components/i18n-hydration-provider";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { ErrorProvider } from "@/components/error-provider";
|
||||
import { IframeBridge } from "@/components/iframe-bridge";
|
||||
@@ -9,19 +10,15 @@ import { PlayEffectiveWsListener } from "@/components/play-effective-ws-listener
|
||||
import { PlayerBalanceWsListener } from "@/components/player-balance-ws-listener";
|
||||
import { TokenSilentRefresh } from "@/components/token-silent-refresh";
|
||||
import "@/i18n";
|
||||
import { syncPreferredLanguage } from "@/i18n";
|
||||
|
||||
type ProvidersProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function Providers({ children }: ProvidersProps): ReactNode {
|
||||
useEffect(() => {
|
||||
syncPreferredLanguage();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<I18nHydrationProvider>
|
||||
<ErrorProvider>
|
||||
{/* iframe 通信桥接 - 支持主站嵌入 */}
|
||||
<IframeBridge>
|
||||
@@ -32,6 +29,7 @@ export function Providers({ children }: ProvidersProps): ReactNode {
|
||||
<TokenSilentRefresh />
|
||||
</IframeBridge>
|
||||
</ErrorProvider>
|
||||
</I18nHydrationProvider>
|
||||
<Toaster />
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user