feat: 添加国际化支持,优化语言切换器和登录界面,增强用户体验
Some checks failed
lotteryfront CI / build (push) Has been cancelled

This commit is contained in:
2026-06-29 15:27:37 +08:00
parent 1a43e81fb4
commit ebc6f4d349
8 changed files with 181 additions and 100 deletions

View 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);
}

View File

@@ -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

View File

@@ -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 />
</>
);