feat: 增强国际化支持与安全头配置

- 在 .env.example 中新增 i18next 相关配置项以支持多语言功能
- 在 next.config.ts 中添加安全头配置以支持 iframe 嵌入
- 更新 Providers 组件以引入 i18n 配置
- 在 PlayerAppShell 中集成 LanguageSwitcher 组件以实现语言切换功能
- 优化 HallWalletStrip 组件的网络状态管理逻辑
- 更新多个组件以支持国际化文本
This commit is contained in:
2026-05-13 17:53:56 +08:00
parent c8f8f90515
commit 587a6ad66c
32 changed files with 2126 additions and 436 deletions

View File

85
src/i18n/index.ts Normal file
View File

@@ -0,0 +1,85 @@
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";
import enCommon from "./locales/en/common.json";
import enEntry from "./locales/en/entry.json";
import enLayout from "./locales/en/layout.json";
import neCommon from "./locales/ne/common.json";
import neEntry from "./locales/ne/entry.json";
import neLayout from "./locales/ne/layout.json";
import zhCommon from "./locales/zh/common.json";
import zhEntry from "./locales/zh/entry.json";
import zhLayout from "./locales/zh/layout.json";
/** 对齐后端与产品:尼泊尔语 / 英语 / 中文(简体) */
export const SUPPORTED_LANGUAGES = [
{ code: "en" as const, flag: "🇺🇸" },
{ code: "ne" as const, flag: "🇳🇵" },
{ code: "zh" as const, flag: "🇨🇳" },
];
export type AppLanguage = (typeof SUPPORTED_LANGUAGES)[number]["code"];
export const DEFAULT_LANGUAGE: AppLanguage = "en";
const namespaces = ["common", "entry", "layout"] as const;
const resources = {
en: {
common: enCommon,
entry: enEntry,
layout: enLayout,
},
ne: {
common: neCommon,
entry: neEntry,
layout: neLayout,
},
zh: {
common: zhCommon,
entry: zhEntry,
layout: zhLayout,
},
} satisfies Record<
AppLanguage,
Record<(typeof namespaces)[number], Record<string, unknown>>
>;
export function normalizeLanguage(lang: string | undefined): AppLanguage {
const base = lang?.split("-")[0]?.toLowerCase();
if (base === "ne") return "ne";
if (base === "zh") return "zh";
return "en";
}
if (!i18n.isInitialized) {
void i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources,
fallbackLng: DEFAULT_LANGUAGE,
supportedLngs: ["en", "ne", "zh"],
defaultNS: "common",
ns: [...namespaces],
/** zh-CN → zhne-NP → ne未匹配时用 fallbackLng */
load: "languageOnly",
detection: {
order: ["localStorage", "navigator"],
caches: ["localStorage"],
lookupLocalStorage: "i18nextLng",
},
interpolation: {
escapeValue: false,
},
react: {
useSuspense: false,
},
});
}
export default i18n;

View File

@@ -0,0 +1,21 @@
{
"language": {
"en": "English",
"ne": "नेपाली",
"zh": "中文"
},
"languageShort": {
"en": "EN",
"ne": "NE",
"zh": "ZH"
},
"status": {
"done": "Done",
"inProgress": "In progress",
"pending": "Pending",
"failed": "Failed"
},
"errors": {
"general": "General"
}
}

View File

@@ -0,0 +1,65 @@
{
"header": {
"backgroundAlt": "Header background"
},
"loading": {
"title": "Loading lottery hall",
"progress": "Progress"
},
"steps": {
"token": {
"title": "Checking token",
"description": "Verifying your session securely."
},
"account": {
"title": "Creating account",
"description": "Setting up your account."
},
"hall": {
"title": "Loading lottery hall",
"description": "Preparing the lottery hall."
}
},
"messages": {
"initializing": "Initializing…",
"retrying": "Retrying…",
"verifying": "Verifying session…",
"connectingHall": "Connecting to lottery hall…",
"ready": "Ready",
"retryProgress": "Retrying ({{current}}/{{total}})…"
},
"failure": {
"title": "Authorization failed",
"subtitle": "We couldnt authorize your session. Please try again.",
"detailsTitle": "Failure details",
"table": {
"no": "No.",
"check": "Check",
"reason": "Reason"
},
"reenter": "Re-enter"
},
"success": {
"title": "Authorization successful!",
"subtitle": "Your session has been verified successfully.",
"doneLabel": "Done",
"continue": "Continue to lottery hall"
},
"footer": {
"secure": "Secure. Trusted. Authorized access."
},
"errors": {
"noToken": "No authorization token found",
"noTokenDetail": "Please return to the main site and try again.",
"authFailed": "Authorization failed",
"unknown": "Unknown error",
"network": "Network error occurred",
"networkDetail": "Please check your internet connection and try again.",
"maxRetries": "Unable to connect after multiple attempts",
"maxRetriesDetail": "The server may be temporarily unavailable.",
"tryLater": "Please try again later",
"http401": "Token is invalid or expired",
"http403": "Account has been blocked",
"http404": "Account not found in the system"
}
}

View File

@@ -0,0 +1,5 @@
{
"brand": {
"title": "Lottery"
}
}

View File

@@ -0,0 +1,21 @@
{
"language": {
"en": "English",
"ne": "नेपाली",
"zh": "中文"
},
"languageShort": {
"en": "EN",
"ne": "NE",
"zh": "ZH"
},
"status": {
"done": "पूरा",
"inProgress": "जारी",
"pending": "बाँकी",
"failed": "असफल"
},
"errors": {
"general": "सामान्य"
}
}

View File

@@ -0,0 +1,65 @@
{
"header": {
"backgroundAlt": "हेडर पृष्ठभूमि"
},
"loading": {
"title": "लटरी हल लोड हुँदैछ",
"progress": "प्रगति"
},
"steps": {
"token": {
"title": "टोकन जाँच गर्दै",
"description": "तपाईंको सत्र सुरक्षित रूपमा प्रमाणित गर्दै।"
},
"account": {
"title": "खाता सिर्जना गर्दै",
"description": "तपाईंको खाता सेट अप गर्दै।"
},
"hall": {
"title": "लटरी हल लोड गर्दै",
"description": "लटरी हल तयार गर्दै।"
}
},
"messages": {
"initializing": "सुरु गर्दै…",
"retrying": "पुन: प्रयास गर्दै…",
"verifying": "सत्र प्रमाणित गर्दै…",
"connectingHall": "लटरी हलमा जोडिँदै…",
"ready": "तयार",
"retryProgress": "पुन: प्रयास ({{current}}/{{total}})…"
},
"failure": {
"title": "प्राधिकरण असफल",
"subtitle": "हामीले तपाईंको सत्र प्राधिकृत गर्न सकेनौं। कृपया फेरि प्रयास गर्नुहोस्।",
"detailsTitle": "विफलताको विवरण",
"table": {
"no": "क्र.सं.",
"check": "जाँच",
"reason": "कारण"
},
"reenter": "पुन: प्रवेश"
},
"success": {
"title": "प्राधिकरण सफल!",
"subtitle": "तपाईंको सत्र सफलतापूर्वक प्रमाणित भयो।",
"doneLabel": "पूरा",
"continue": "लटरी हलमा जानुहोस्"
},
"footer": {
"secure": "सुरक्षित। विश्वसनीय। अधिकृत पहुँच।"
},
"errors": {
"noToken": "कुनै प्राधिकरण टोकन फेला परेन",
"noTokenDetail": "कृपया मुख्य साइटमा फर्कनुहोस् र फेरि प्रयास गर्नुहोस्।",
"authFailed": "प्राधिकरण असफल",
"unknown": "अज्ञात त्रुटि",
"network": "नेटवर्क त्रुटि भयो",
"networkDetail": "कृपया आफ्नो इन्टरनेट जाँच गर्नुहोस् र फेरि प्रयास गर्नुहोस्।",
"maxRetries": "धेरै पटक पछि पनि जडान हुन सकेन",
"maxRetriesDetail": "सर्भर अस्थायी रूपमा अनुपलब्ध हुन सक्छ।",
"tryLater": "कृपया पछि प्रयास गर्नुहोस्",
"http401": "टोकन अमान्य वा म्याद सकियो",
"http403": "खाता रोकिएको छ",
"http404": "प्रणालीमा खाता फेला परेन"
}
}

View File

@@ -0,0 +1,5 @@
{
"brand": {
"title": "लटरी"
}
}

View File

@@ -0,0 +1,21 @@
{
"language": {
"en": "English",
"ne": "नेपाली",
"zh": "中文"
},
"languageShort": {
"en": "EN",
"ne": "NE",
"zh": "ZH"
},
"status": {
"done": "完成",
"inProgress": "进行中",
"pending": "待处理",
"failed": "失败"
},
"errors": {
"general": "通用"
}
}

View File

@@ -0,0 +1,65 @@
{
"header": {
"backgroundAlt": "页头背景"
},
"loading": {
"title": "正在进入彩票大厅",
"progress": "进度"
},
"steps": {
"token": {
"title": "校验登录凭证",
"description": "正在安全验证您的会话。"
},
"account": {
"title": "创建/同步账号",
"description": "正在为您设置账号。"
},
"hall": {
"title": "加载彩票大厅",
"description": "正在准备彩票大厅。"
}
},
"messages": {
"initializing": "正在初始化…",
"retrying": "正在重试…",
"verifying": "正在校验会话…",
"connectingHall": "正在连接彩票大厅…",
"ready": "就绪",
"retryProgress": "正在重试({{current}}/{{total}})…"
},
"failure": {
"title": "授权失败",
"subtitle": "无法完成授权,请重试。",
"detailsTitle": "失败详情",
"table": {
"no": "序号",
"check": "检查项",
"reason": "原因"
},
"reenter": "重新进入"
},
"success": {
"title": "授权成功!",
"subtitle": "您的会话已通过验证。",
"doneLabel": "完成",
"continue": "进入彩票大厅"
},
"footer": {
"secure": "安全 · 可信 · 授权访问"
},
"errors": {
"noToken": "未发现授权令牌",
"noTokenDetail": "请返回主站后重试。",
"authFailed": "授权失败",
"unknown": "未知错误",
"network": "网络异常",
"networkDetail": "请检查网络连接后重试。",
"maxRetries": "多次重试仍无法连接",
"maxRetriesDetail": "服务器可能暂时不可用。",
"tryLater": "请稍后再试",
"http401": "令牌无效或已过期",
"http403": "账号已被封禁",
"http404": "系统中未找到该账号"
}
}

View File

@@ -0,0 +1,5 @@
{
"brand": {
"title": "彩票"
}
}