feat: integrate Sensei Spook mascot in 4 stances, speech bubbles, and style updates

This commit is contained in:
Ajasra committed 2026-10-28 01:20:01 +08:00
commit e04672db2a
104 files changed
+8262

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
import { createContext, useContext, useState, useEffect, ReactNode } from "react";
import { LocalizedString } from "@/types/booth";
export type Language = "en" | "zh";
interface I18nContextType {
language: Language;
setLanguage: (lang: Language) => void;
toggleLanguage: () => void;
t: (field: LocalizedString | string | undefined, fallback?: string) => string;
isWeChat: boolean;
}
const I18nContext = createContext<I18nContextType | null>(null);
export function I18nProvider({ children }: { children: ReactNode }) {
const [language, setLanguageState] = useState<Language>("zh");
const [isWeChat, setIsWeChat] = useState<boolean>(false);
useEffect(() => {
if (typeof window !== "undefined") {
// 1. Detect WeChat browser environment
const ua = navigator.userAgent.toLowerCase();
const inWeChat = ua.includes("micromessenger");
setIsWeChat(inWeChat);
// 2. Check stored preference first
const stored = localStorage.getItem("booth_lang") as Language | null;
if (stored === "en" || stored === "zh") {
setLanguageState(stored);
return;
}
// 3. Fallback to system / browser language
const browserLang = (navigator.language || "").toLowerCase();
if (browserLang.startsWith("zh")) {
setLanguageState("zh");
} else {
setLanguageState("en");
}
}
}, []);
const setLanguage = (lang: Language) => {
setLanguageState(lang);
if (typeof window !== "undefined") {
localStorage.setItem("booth_lang", lang);
}
};
const toggleLanguage = () => {
const nextLang = language === "zh" ? "en" : "zh";
setLanguage(nextLang);
};
const t = (field: LocalizedString | string | undefined, fallback: string = ""): string => {
if (!field) return fallback;
if (typeof field === "string") return field;
return field[language] || field.en || fallback;
};
return (
<I18nContext.Provider value={{ language, setLanguage, toggleLanguage, t, isWeChat }}>
{children}
</I18nContext.Provider>
);
}
export function useI18n() {
const context = useContext(I18nContext);
if (!context) {
return {
language: "zh" as Language,
setLanguage: () => {},
toggleLanguage: () => {},
t: (field: LocalizedString | string | undefined, fallback: string = "") => {
if (!field) return fallback;
if (typeof field === "string") return field;
return field.zh || field.en || fallback;
},
isWeChat: false,
};
}
return context;
}