From 1b1dfc92abd85009d96816872ca3b1b8e1d2d606 Mon Sep 17 00:00:00 2001 From: kang Date: Tue, 19 May 2026 09:11:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E7=AB=AF=E5=A4=9A=E8=AF=AD=E8=A8=80=E4=B8=8E=E5=A4=9A=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E7=95=8C=E9=9D=A2=E5=9B=BD=E9=99=85=E5=8C=96=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/admin/admin-breadcrumb.tsx | 34 ++- src/components/admin/admin-date-field.tsx | 15 +- .../admin/admin-date-range-field.tsx | 30 +- .../admin/admin-language-switcher.tsx | 8 +- .../admin/admin-list-pagination-footer.tsx | 24 +- src/components/admin/admin-sidebar.tsx | 10 +- src/components/admin/auth-gate.tsx | 8 +- src/components/admin/login-form.tsx | 44 +-- src/components/admin/toolbar.tsx | 16 +- src/i18n/index.ts | 62 ++++- src/i18n/locales/en/adminUsers.json | 83 ++++++ src/i18n/locales/en/audit.json | 13 +- src/i18n/locales/en/auth.json | 23 +- src/i18n/locales/en/common.json | 63 ++++- src/i18n/locales/en/config.json | 83 ++++++ src/i18n/locales/en/dashboard.json | 56 +++- src/i18n/locales/en/draws.json | 132 +++++++++ src/i18n/locales/en/jackpot.json | 46 ++++ src/i18n/locales/en/players.json | 49 ++++ src/i18n/locales/en/reconcile.json | 45 +++ src/i18n/locales/en/reports.json | 33 ++- src/i18n/locales/en/risk.json | 91 +++++++ src/i18n/locales/en/settlement.json | 54 ++++ src/i18n/locales/en/tickets.json | 19 ++ src/i18n/locales/en/wallet.json | 69 +++++ src/i18n/locales/ne/adminUsers.json | 83 ++++++ src/i18n/locales/ne/audit.json | 13 +- src/i18n/locales/ne/auth.json | 23 +- src/i18n/locales/ne/common.json | 63 ++++- src/i18n/locales/ne/config.json | 83 ++++++ src/i18n/locales/ne/dashboard.json | 56 +++- src/i18n/locales/ne/draws.json | 132 +++++++++ src/i18n/locales/ne/jackpot.json | 46 ++++ src/i18n/locales/ne/players.json | 49 ++++ src/i18n/locales/ne/reconcile.json | 45 +++ src/i18n/locales/ne/reports.json | 33 ++- src/i18n/locales/ne/risk.json | 91 +++++++ src/i18n/locales/ne/settlement.json | 54 ++++ src/i18n/locales/ne/tickets.json | 19 ++ src/i18n/locales/ne/wallet.json | 69 +++++ src/i18n/locales/zh/adminUsers.json | 83 ++++++ src/i18n/locales/zh/audit.json | 13 +- src/i18n/locales/zh/auth.json | 23 +- src/i18n/locales/zh/common.json | 63 ++++- src/i18n/locales/zh/config.json | 83 ++++++ src/i18n/locales/zh/dashboard.json | 56 +++- src/i18n/locales/zh/draws.json | 132 +++++++++ src/i18n/locales/zh/jackpot.json | 46 ++++ src/i18n/locales/zh/players.json | 49 ++++ src/i18n/locales/zh/reconcile.json | 45 +++ src/i18n/locales/zh/reports.json | 33 ++- src/i18n/locales/zh/risk.json | 91 +++++++ src/i18n/locales/zh/settlement.json | 54 ++++ src/i18n/locales/zh/tickets.json | 19 ++ src/i18n/locales/zh/wallet.json | 69 +++++ src/modules/_config/admin-nav.ts | 33 +-- .../admin-users/admin-users-console.tsx | 162 +++++------ src/modules/admin-users/meta.ts | 2 +- src/modules/audit/audit-logs-console.tsx | 38 +-- src/modules/audit/meta.ts | 2 +- src/modules/auth/meta.ts | 2 +- src/modules/config/config-nav-model.ts | 38 +-- src/modules/config/config-status-badge.tsx | 10 +- src/modules/config/config-subnav.tsx | 20 +- src/modules/config/config-version-actions.tsx | 13 +- .../config/config-version-switcher.tsx | 78 +++--- src/modules/config/config-workspace-shell.tsx | 12 +- .../config/doc/odds-config-doc-screen.tsx | 86 +++--- .../config/doc/play-config-doc-screen.tsx | 95 +++---- src/modules/config/doc/prize-scopes.ts | 14 +- .../config/doc/rebate-config-doc-screen.tsx | 58 ++-- .../config/doc/risk-cap-doc-screen.tsx | 116 ++++---- .../config/doc/wallet-config-doc-screen.tsx | 44 +-- src/modules/config/meta.ts | 24 +- src/modules/dashboard/dashboard-console.tsx | 147 +++++----- src/modules/dashboard/meta.ts | 2 +- src/modules/draws/draw-detail-console.tsx | 77 +++--- src/modules/draws/draw-finance-console.tsx | 52 ++-- src/modules/draws/draw-publish-console.tsx | 49 ++-- src/modules/draws/draw-results-console.tsx | 37 ++- src/modules/draws/draw-review-console.tsx | 64 +++-- src/modules/draws/draw-subnav.tsx | 12 +- src/modules/draws/draws-index-console.tsx | 88 +++--- src/modules/draws/meta.ts | 2 +- src/modules/jackpot/jackpot-pools-console.tsx | 54 ++-- .../jackpot/jackpot-records-console.tsx | 46 ++-- src/modules/jackpot/jackpot-subnav.tsx | 10 +- src/modules/jackpot/meta.ts | 2 +- src/modules/players/meta.ts | 2 +- src/modules/players/players-console.tsx | 132 +++++---- src/modules/reconcile/meta.ts | 2 +- src/modules/reconcile/reconcile-console.tsx | 117 ++++---- src/modules/reports/meta.ts | 2 +- src/modules/reports/reports-console.tsx | 80 +++--- src/modules/risk/meta.ts | 2 +- src/modules/risk/risk-draw-header.tsx | 16 +- src/modules/risk/risk-index-console.tsx | 67 ++--- src/modules/risk/risk-lock-logs-console.tsx | 38 +-- src/modules/risk/risk-pool-detail-console.tsx | 46 ++-- src/modules/risk/risk-pools-console.tsx | 58 ++-- src/modules/risk/risk-subnav.tsx | 14 +- src/modules/settings/meta.ts | 2 +- src/modules/settlement/meta.ts | 2 +- .../settlement-batch-details-console.tsx | 79 +++--- .../settlement/settlement-batches-console.tsx | 70 ++--- src/modules/tickets/meta.ts | 2 +- .../tickets/player-tickets-console.tsx | 40 +-- src/modules/wallet/meta.ts | 2 +- src/modules/wallet/wallet-console.tsx | 257 +++++++++--------- src/modules/wallet/wallet-subnav.tsx | 14 +- 110 files changed, 4053 insertions(+), 1308 deletions(-) create mode 100644 src/i18n/locales/en/adminUsers.json create mode 100644 src/i18n/locales/en/config.json create mode 100644 src/i18n/locales/en/draws.json create mode 100644 src/i18n/locales/en/jackpot.json create mode 100644 src/i18n/locales/en/players.json create mode 100644 src/i18n/locales/en/reconcile.json create mode 100644 src/i18n/locales/en/risk.json create mode 100644 src/i18n/locales/en/settlement.json create mode 100644 src/i18n/locales/en/tickets.json create mode 100644 src/i18n/locales/en/wallet.json create mode 100644 src/i18n/locales/ne/adminUsers.json create mode 100644 src/i18n/locales/ne/config.json create mode 100644 src/i18n/locales/ne/draws.json create mode 100644 src/i18n/locales/ne/jackpot.json create mode 100644 src/i18n/locales/ne/players.json create mode 100644 src/i18n/locales/ne/reconcile.json create mode 100644 src/i18n/locales/ne/risk.json create mode 100644 src/i18n/locales/ne/settlement.json create mode 100644 src/i18n/locales/ne/tickets.json create mode 100644 src/i18n/locales/ne/wallet.json create mode 100644 src/i18n/locales/zh/adminUsers.json create mode 100644 src/i18n/locales/zh/config.json create mode 100644 src/i18n/locales/zh/draws.json create mode 100644 src/i18n/locales/zh/jackpot.json create mode 100644 src/i18n/locales/zh/players.json create mode 100644 src/i18n/locales/zh/reconcile.json create mode 100644 src/i18n/locales/zh/risk.json create mode 100644 src/i18n/locales/zh/settlement.json create mode 100644 src/i18n/locales/zh/tickets.json create mode 100644 src/i18n/locales/zh/wallet.json diff --git a/src/components/admin/admin-breadcrumb.tsx b/src/components/admin/admin-breadcrumb.tsx index ab355f6..212a892 100644 --- a/src/components/admin/admin-breadcrumb.tsx +++ b/src/components/admin/admin-breadcrumb.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useTranslation } from "react-i18next"; import { Breadcrumb, BreadcrumbItem, @@ -11,13 +12,12 @@ import { BreadcrumbSeparator, } from "@/components/ui/breadcrumb"; import { adminShellNavItems, ADMIN_BASE } from "@/modules/_config/admin-nav"; -import { CONFIG_ROUTE_LABELS } from "@/modules/config/config-nav-model"; import React from "react"; const DRAW_ROUTE_LABELS: Record = { - finance: "期号收支", - review: "审核", - results: "开奖结果", + finance: "Draw Finance", + review: "Review", + results: "Results", }; function titleCase(value: string): string { @@ -34,15 +34,16 @@ type BreadcrumbCrumb = { }; export function AdminBreadcrumb() { + const { t } = useTranslation(["common", "dashboard", "reports", "audit", "config", "draws"]); const pathname = usePathname(); - // 把路径拆分成段 + // Split the current path into segments. const segments = pathname.split("/").filter(Boolean); - // 基础面包屑:首页/仪表盘 + // Base breadcrumb: home / dashboard. const breadcrumbs: BreadcrumbCrumb[] = [ { - label: "首页", + label: t("nav.home", { ns: "common" }), href: ADMIN_BASE, isCurrent: pathname === ADMIN_BASE, }, @@ -56,8 +57,16 @@ export function AdminBreadcrumb() { }); if (navItem && navItem.href !== ADMIN_BASE) { + const navLabelMap: Record = { + dashboard: t("title", { ns: "dashboard" }), + reports: t("title", { ns: "reports" }), + "audit-logs": t("title", { ns: "audit" }), + }; breadcrumbs.push({ - label: navItem.segment === "draws" ? "期号列表" : navItem.label, + label: + navItem.segment === "draws" + ? "Draws" + : navLabelMap[navItem.segment] ?? navItem.label, href: navItem.href, isCurrent: pathname === navItem.href || segments.length === 2, }); @@ -67,9 +76,14 @@ export function AdminBreadcrumb() { const subSegment = segments[2]; let subLabel = ""; if (businessSegment === "config" && subSegment) { - subLabel = CONFIG_ROUTE_LABELS[subSegment] ?? titleCase(subSegment); + subLabel = t(`nav.items.${subSegment}`, { ns: "config", defaultValue: titleCase(subSegment) }); } else { - subLabel = subSegment ? DRAW_ROUTE_LABELS[subSegment] ?? titleCase(subSegment) : ""; + subLabel = subSegment + ? t(`subnav.${subSegment}`, { + ns: "draws", + defaultValue: DRAW_ROUTE_LABELS[subSegment] ?? titleCase(subSegment), + }) + : ""; } if (subLabel) { breadcrumbs.push({ diff --git a/src/components/admin/admin-date-field.tsx b/src/components/admin/admin-date-field.tsx index 66a7538..6db0997 100644 --- a/src/components/admin/admin-date-field.tsx +++ b/src/components/admin/admin-date-field.tsx @@ -2,8 +2,9 @@ import * as React from "react"; import { format, parse } from "date-fns"; -import { zhCN } from "date-fns/locale"; +import { enUS } from "date-fns/locale"; import { CalendarIcon } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { Button, buttonVariants } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; @@ -16,16 +17,18 @@ export function AdminDateField({ label, value, onChange, - placeholder = "选择日期", + placeholder, }: { id: string; label: string; - /** `yyyy-MM-dd` 或空 */ + /** `yyyy-MM-dd` or empty */ value: string; onChange: (next: string) => void; placeholder?: string; }) { + const { t } = useTranslation(["common"]); const [open, setOpen] = React.useState(false); + const resolvedPlaceholder = placeholder ?? t("date.placeholder", { ns: "common", defaultValue: "Select date" }); const parsed = React.useMemo(() => { if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { @@ -35,7 +38,7 @@ export function AdminDateField({ return Number.isNaN(d.getTime()) ? undefined : d; }, [value]); - const summary = parsed ? format(parsed, "yyyy年M月d日", { locale: zhCN }) : placeholder; + const summary = parsed ? format(parsed, "yyyy-MM-dd", { locale: enUS }) : resolvedPlaceholder; return (
@@ -62,7 +65,7 @@ export function AdminDateField({ - 清除 + {t("actions.clear", { ns: "common", defaultValue: "Clear" })}
diff --git a/src/components/admin/admin-date-range-field.tsx b/src/components/admin/admin-date-range-field.tsx index 76a8a0d..724f9db 100644 --- a/src/components/admin/admin-date-range-field.tsx +++ b/src/components/admin/admin-date-range-field.tsx @@ -3,8 +3,9 @@ import * as React from "react"; import type { DateRange } from "react-day-picker"; import { format, parse } from "date-fns"; -import { zhCN } from "date-fns/locale"; +import { enUS } from "date-fns/locale"; import { CalendarRange } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { Button, buttonVariants } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; @@ -31,19 +32,19 @@ function summarize(from: string, to: string, placeholder: string): string { if (!df && !dt) { return placeholder; } - const a = df ? format(df, "yyyy年M月d日", { locale: zhCN }) : "…"; - const b = dt ? format(dt, "yyyy年M月d日", { locale: zhCN }) : "…"; - return `${a} 至 ${b}`; + const a = df ? format(df, "yyyy-MM-dd", { locale: enUS }) : "..."; + const b = dt ? format(dt, "yyyy-MM-dd", { locale: enUS }) : "..."; + return `${a} - ${b}`; } -/** shadcn Popover + Calendar `mode="range"`;输出与原先两个 `yyyy-MM-dd` 筛选字段兼容 */ +/** Range date picker compatible with legacy `yyyy-MM-dd` filter fields. */ export function AdminDateRangeField({ id, label, from: fromProp, to: toProp, onRangeChange, - placeholder = "选择日期范围", + placeholder, }: { id: string; label?: string; @@ -52,8 +53,11 @@ export function AdminDateRangeField({ onRangeChange: (next: { from: string; to: string }) => void; placeholder?: string; }) { + const { t } = useTranslation(["common"]); const [open, setOpen] = React.useState(false); const isMobile = useIsMobile(); + const resolvedPlaceholder = + placeholder ?? t("date.rangePlaceholder", { ns: "common", defaultValue: "Select date range" }); const selected = React.useMemo((): DateRange | undefined => { const df = parseYmd(fromProp); @@ -98,16 +102,20 @@ export function AdminDateRangeField({ > - {summarize(fromProp, toProp, placeholder)} + {summarize(fromProp, toProp, resolvedPlaceholder)}

- 先选开始日,再选结束日(单日可对同一天点两次);点「完成」关闭面板。 + {t("date.rangeHint", { + ns: "common", + defaultValue: + "Select a start date, then an end date. For a single day, click the same date twice. Click Done to close.", + })}

- 清除 + {t("actions.clear", { ns: "common", defaultValue: "Clear" })}
diff --git a/src/components/admin/admin-language-switcher.tsx b/src/components/admin/admin-language-switcher.tsx index 244a09e..54c5ac7 100644 --- a/src/components/admin/admin-language-switcher.tsx +++ b/src/components/admin/admin-language-switcher.tsx @@ -37,7 +37,11 @@ export function AdminLanguageSwitcher() { applyAdminUiLocale(next); await i18n.changeLanguage(next); setLocale(next); - toast.success(`${t("language.changed", { defaultValue: "语言已切换" })}: ${ADMIN_LOCALE_LABELS[next]}`); + toast.success( + t("language.changed", { + language: ADMIN_LOCALE_LABELS[next], + }), + ); } return ( @@ -49,7 +53,7 @@ export function AdminLanguageSwitcher() { - {t("language.title", { defaultValue: "界面语言" })} + {t("language.title")} {ADMIN_API_LOCALES.map((code) => ( void; }) { + const { t } = useTranslation(["common"]); return (
setAccount(ev.target.value)} - placeholder="登录账号" + placeholder={t("accountPlaceholder")} required disabled={submitting} className="h-11 transition-shadow focus-visible:ring-2 focus-visible:ring-primary/25" @@ -179,7 +183,7 @@ export function LoginForm() {
setPassword(ev.target.value)} - placeholder="密码" + placeholder={t("passwordPlaceholder")} required disabled={submitting} className="h-11 transition-shadow focus-visible:ring-2 focus-visible:ring-primary/25" @@ -196,7 +200,7 @@ export function LoginForm() {
setCaptchaCode(ev.target.value)} - placeholder="图中字符" + placeholder={t("captchaPlaceholder")} maxLength={32} required disabled={submitting} @@ -218,7 +222,7 @@ export function LoginForm() { disabled={ loadingCaptcha || !apiConfigured || submitting } - aria-label={loadingCaptcha ? "加载验证码中" : "点击刷新验证码"} + aria-label={loadingCaptcha ? t("captchaLoading") : t("captchaRefresh")} > {captchaSrc ? ( // eslint-disable-next-line @next/next/no-img-element -- data URL from API @@ -231,7 +235,7 @@ export function LoginForm() { /> ) : ( - {loadingCaptcha ? "加载中…" : "点击获取"} + {loadingCaptcha ? t("captchaLoading") : t("captchaFetch")} )} @@ -245,7 +249,7 @@ export function LoginForm() { className="h-11 w-full text-base font-medium shadow-sm" disabled={submitting || !apiConfigured} > - {submitting ? "登录中…" : "登录"} + {submitting ? t("submitting") : t("submit")} diff --git a/src/components/admin/toolbar.tsx b/src/components/admin/toolbar.tsx index 080a6ce..5b10775 100644 --- a/src/components/admin/toolbar.tsx +++ b/src/components/admin/toolbar.tsx @@ -7,6 +7,7 @@ import { UserRoundIcon, } from "lucide-react"; import { useRouter } from "next/navigation"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { AdminLanguageSwitcher } from "@/components/admin/admin-language-switcher"; @@ -64,6 +65,7 @@ function initialsFromProfile(profile: AdminProfile | null): string { } export function ShellToolbar() { + const { t } = useTranslation("common"); const router = useRouter(); const adminProfile = useAdminProfile(); const clearSession = useAdminSessionStore((s) => s.clearSession); @@ -71,11 +73,11 @@ export function ShellToolbar() { const displayName = adminProfile?.nickname?.trim() || adminProfile?.username?.trim() || - "管理员"; + t("toolbar.defaultAdmin"); function onLogout() { clearSession(); - toast.success("已退出登录"); + toast.success(t("toolbar.loggedOut")); router.replace("/admin/login"); router.refresh(); } @@ -87,9 +89,9 @@ export function ShellToolbar() { variant="ghost" size="icon" className="relative shrink-0 text-primary hover:text-primary" - aria-label="通知" - title="通知" - onClick={() => toast.message("通知功能开发中")} + aria-label={t("toolbar.notifications")} + title={t("toolbar.notifications")} + onClick={() => toast.message(t("toolbar.notificationsComingSoon"))} > @@ -132,7 +134,7 @@ export function ShellToolbar() { - 账号设置 + {t("toolbar.accountSettings")} @@ -143,7 +145,7 @@ export function ShellToolbar() { onClick={onLogout} > - 退出登录 + {t("actions.logout")} diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 053b0a3..6a4de04 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -6,48 +6,108 @@ import { initReactI18next } from "react-i18next"; import { adminHtmlLang, applyAdminUiLocale, type AdminApiLocale } from "@/lib/admin-locale"; import enAudit from "@/i18n/locales/en/audit.json"; +import enAdminUsers from "@/i18n/locales/en/adminUsers.json"; import enAuth from "@/i18n/locales/en/auth.json"; import enCommon from "@/i18n/locales/en/common.json"; +import enConfig from "@/i18n/locales/en/config.json"; import enDashboard from "@/i18n/locales/en/dashboard.json"; +import enDraws from "@/i18n/locales/en/draws.json"; +import enJackpot from "@/i18n/locales/en/jackpot.json"; import enReports from "@/i18n/locales/en/reports.json"; +import enRisk from "@/i18n/locales/en/risk.json"; +import enSettlement from "@/i18n/locales/en/settlement.json"; +import enPlayers from "@/i18n/locales/en/players.json"; +import enTickets from "@/i18n/locales/en/tickets.json"; +import enReconcile from "@/i18n/locales/en/reconcile.json"; +import enWallet from "@/i18n/locales/en/wallet.json"; import neAudit from "@/i18n/locales/ne/audit.json"; +import neAdminUsers from "@/i18n/locales/ne/adminUsers.json"; import neAuth from "@/i18n/locales/ne/auth.json"; import neCommon from "@/i18n/locales/ne/common.json"; +import neConfig from "@/i18n/locales/ne/config.json"; import neDashboard from "@/i18n/locales/ne/dashboard.json"; +import neDraws from "@/i18n/locales/ne/draws.json"; +import neJackpot from "@/i18n/locales/ne/jackpot.json"; import neReports from "@/i18n/locales/ne/reports.json"; +import neRisk from "@/i18n/locales/ne/risk.json"; +import neSettlement from "@/i18n/locales/ne/settlement.json"; +import nePlayers from "@/i18n/locales/ne/players.json"; +import neTickets from "@/i18n/locales/ne/tickets.json"; +import neReconcile from "@/i18n/locales/ne/reconcile.json"; +import neWallet from "@/i18n/locales/ne/wallet.json"; import zhAudit from "@/i18n/locales/zh/audit.json"; +import zhAdminUsers from "@/i18n/locales/zh/adminUsers.json"; import zhAuth from "@/i18n/locales/zh/auth.json"; import zhCommon from "@/i18n/locales/zh/common.json"; +import zhConfig from "@/i18n/locales/zh/config.json"; import zhDashboard from "@/i18n/locales/zh/dashboard.json"; +import zhDraws from "@/i18n/locales/zh/draws.json"; +import zhJackpot from "@/i18n/locales/zh/jackpot.json"; import zhReports from "@/i18n/locales/zh/reports.json"; +import zhRisk from "@/i18n/locales/zh/risk.json"; +import zhSettlement from "@/i18n/locales/zh/settlement.json"; +import zhPlayers from "@/i18n/locales/zh/players.json"; +import zhTickets from "@/i18n/locales/zh/tickets.json"; +import zhReconcile from "@/i18n/locales/zh/reconcile.json"; +import zhWallet from "@/i18n/locales/zh/wallet.json"; export const ADMIN_SUPPORTED_LANGUAGES = ["en", "ne", "zh"] as const; export type AdminLanguage = (typeof ADMIN_SUPPORTED_LANGUAGES)[number]; export const ADMIN_DEFAULT_LANGUAGE: AdminLanguage = "en"; -const namespaces = ["common", "auth", "dashboard", "reports", "audit"] as const; +const namespaces = ["common", "auth", "dashboard", "reports", "audit", "draws", "settlement", "risk", "jackpot", "players", "tickets", "reconcile", "wallet", "adminUsers", "config"] as const; const resources = { en: { common: enCommon, + config: enConfig, + adminUsers: enAdminUsers, auth: enAuth, dashboard: enDashboard, + draws: enDraws, + jackpot: enJackpot, + players: enPlayers, + tickets: enTickets, + reconcile: enReconcile, reports: enReports, + risk: enRisk, audit: enAudit, + settlement: enSettlement, + wallet: enWallet, }, ne: { common: neCommon, + config: neConfig, + adminUsers: neAdminUsers, auth: neAuth, dashboard: neDashboard, + draws: neDraws, + jackpot: neJackpot, + players: nePlayers, + tickets: neTickets, + reconcile: neReconcile, reports: neReports, + risk: neRisk, audit: neAudit, + settlement: neSettlement, + wallet: neWallet, }, zh: { common: zhCommon, + config: zhConfig, + adminUsers: zhAdminUsers, auth: zhAuth, dashboard: zhDashboard, + draws: zhDraws, + jackpot: zhJackpot, + players: zhPlayers, + tickets: zhTickets, + reconcile: zhReconcile, reports: zhReports, + risk: zhRisk, audit: zhAudit, + settlement: zhSettlement, + wallet: zhWallet, }, } satisfies Record>>; diff --git a/src/i18n/locales/en/adminUsers.json b/src/i18n/locales/en/adminUsers.json new file mode 100644 index 0000000..2814a7c --- /dev/null +++ b/src/i18n/locales/en/adminUsers.json @@ -0,0 +1,83 @@ +{ + "title": "Admins", + "listTitle": "Admin user list", + "createAdmin": "Create admin", + "searchPlaceholder": "Search by username / nickname / email", + "loadFailed": "Failed to load admin list", + "nicknameRequired": "Enter a nickname", + "newPasswordMin": "New password must be at least 8 characters", + "roleRequired": "Select at least one role", + "usernameRequired": "Enter a login username", + "passwordMin": "Password must be at least 8 characters", + "createSuccess": "Created admin {{name}}", + "updateSuccess": "Updated {{name}}", + "saveAccountFailed": "Failed to save account", + "deleteSuccess": "Deleted {{name}}", + "deleteFailed": "Delete failed", + "allPermissions": "All permissions", + "saveRoleSuccess": "Updated roles for {{name}}", + "saveRoleFailed": "Failed to save roles", + "savePermissionSuccess": "Updated permissions for {{name}}", + "savePermissionFailed": "Failed to save permissions", + "saving": "Saving…", + "deleting": "Deleting…", + "common": { + "none": "None" + }, + "table": { + "account": "Account", + "nickname": "Nickname", + "status": "Status", + "roles": "Roles", + "direct": "Direct", + "effective": "Effective", + "actions": "Actions" + }, + "status": { + "enabled": "Enabled", + "disabled": "Disabled" + }, + "actions": { + "permissions": "Permissions", + "edit": "Edit", + "delete": "Delete", + "cancel": "Cancel", + "save": "Save" + }, + "permissionDialog": { + "title": "Admin permissions", + "rolesTitle": "Roles", + "rolesDescription": "Saved as default-site roles and merged with direct permissions as effective permissions.", + "rolePermissionCount": "Contains {{count}} functional permissions", + "directTitle": "Direct permissions", + "directDescription": "Expand by menu or domain and check specific prd.* items; in most cases role changes are enough.", + "selectedRoles": "Selected roles:", + "saveRoles": "Save roles", + "saveDirect": "Save direct permissions" + }, + "accountDialog": { + "createTitle": "Create admin", + "editTitle": "Edit account", + "createDescription": "Assign at least one default-site role. Login usernames may contain letters, numbers, dots, underscores, and hyphens only, and are stored in lowercase.", + "editDescription": "Login username cannot be changed. Leave password empty to keep it unchanged.", + "username": "Login username", + "usernamePlaceholder": "For example: ops_admin", + "nickname": "Nickname", + "nicknamePlaceholder": "Display name", + "emailOptional": "Email (optional)", + "emailPlaceholder": "Leave empty if not needed", + "password": "Password", + "passwordOptional": "Password (optional)", + "passwordPlaceholderCreate": "At least 8 characters", + "passwordPlaceholderEdit": "Leave empty to keep unchanged", + "rolesRequired": "Roles (default site, at least one)", + "rolesDescription": "After creation, you can continue adjusting roles or grant direct permissions in Permissions.", + "noRoles": "No roles available yet. Wait for the list to finish loading and try again." + }, + "delete": { + "currentUserBlocked": "You cannot delete the currently signed-in account", + "rowActionTitle": "Delete this admin", + "confirmTitle": "Confirm deletion", + "confirmDescription": "Delete admin {{name}}? This action cannot be undone." + } +} diff --git a/src/i18n/locales/en/audit.json b/src/i18n/locales/en/audit.json index 1e45426..eee6271 100644 --- a/src/i18n/locales/en/audit.json +++ b/src/i18n/locales/en/audit.json @@ -1,3 +1,14 @@ { - "title": "Audit Logs" + "title": "Audit Logs", + "moduleCode": "Module code", + "actionCode": "Action code", + "operatorType": "Operator type", + "exactMatch": "Exact match", + "operatorTypePlaceholder": "For example admin / system", + "operator": "Operator", + "module": "Module", + "action": "Action", + "target": "Target", + "time": "Time", + "empty": "No data" } diff --git a/src/i18n/locales/en/auth.json b/src/i18n/locales/en/auth.json index d3ab0a3..2da6251 100644 --- a/src/i18n/locales/en/auth.json +++ b/src/i18n/locales/en/auth.json @@ -1,3 +1,24 @@ { - "title": "Login" + "title": "Login", + "loginTitle": "Admin Login", + "account": "Account", + "accountPlaceholder": "Login account", + "password": "Password", + "passwordPlaceholder": "Password", + "captcha": "Captcha", + "captchaPlaceholder": "Enter captcha", + "captchaLoading": "Loading captcha", + "captchaRefresh": "Click to refresh captcha", + "captchaFetch": "Click to get captcha", + "apiMissingTitle": "API base URL not configured", + "apiMissingDescriptionPrefix": "Set", + "apiMissingDescriptionSuffix": "in the environment (Laravel base URL, for example http://127.0.0.1:8000).", + "submit": "Log in", + "submitting": "Signing in…", + "captchaLoadFailed": "Failed to load captcha. Check the API or network.", + "apiBaseMissingToast": "NEXT_PUBLIC_LOTTERY_API_BASE_URL is not configured", + "captchaRequired": "Refresh the captcha first", + "welcome": "Welcome, {{name}}", + "networkFailed": "Network request failed", + "loginFailed": "Login failed" } diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 6093ceb..73f1cc0 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -4,7 +4,7 @@ "ne": "नेपाली", "zh": "中文", "title": "Interface language", - "changed": "Language" + "changed": "Language switched to {{language}}" }, "app": { "title": "Lottery Admin" @@ -15,6 +15,65 @@ "search": "Search", "apply": "Apply", "loading": "Loading...", - "submitting": "Submitting..." + "submitting": "Submitting...", + "logout": "Log out", + "close": "Close", + "viewAll": "View all", + "viewDetails": "View details", + "reviewNow": "Review now", + "create": "Create", + "createTask": "Create task", + "clear": "Clear", + "done": "Done" + }, + "date": { + "placeholder": "Select date", + "rangePlaceholder": "Select date range", + "rangeHint": "Select a start date, then an end date. For a single day, click the same date twice. Click Done to close." + }, + "pagination": { + "perPage": "Per page", + "selectPlaceholder": "Select", + "summary": "{{total}} total, page {{page}} / {{lastPage}}", + "previous": "Previous", + "next": "Next" + }, + "states": { + "noData": "No data", + "loading": "Loading…", + "comingSoon": "Feature under development" + }, + "errors": { + "loadFailed": "Failed to load" + }, + "toolbar": { + "defaultAdmin": "Administrator", + "notifications": "Notifications", + "notificationsComingSoon": "Notifications are under development", + "accountSettings": "Account settings", + "loggedOut": "Signed out" + }, + "nav": { + "home": "Home", + "dashboard": "Dashboard", + "admin_users": "Admin Users", + "players": "Players", + "wallet": "Wallet", + "draws": "Draws", + "config": "Configuration", + "risk": "Risk", + "settlement": "Settlement", + "jackpot": "Jackpot", + "reconcile": "Reconcile", + "tickets": "Tickets", + "reports": "Reports", + "audit": "Audit Logs", + "settings": "Settings" + }, + "sidebar": { + "workspace": "Workspace" + }, + "auth": { + "checking": "Checking sign-in status…" } } diff --git a/src/i18n/locales/en/config.json b/src/i18n/locales/en/config.json new file mode 100644 index 0000000..8ae28fc --- /dev/null +++ b/src/i18n/locales/en/config.json @@ -0,0 +1,83 @@ +{ + "title": "Configuration Center", + "nav": { + "aria": "Operations configuration sub-navigation", + "sidebarTitle": "Operations configuration", + "groups": { + "betting": "Betting and display", + "risk_wallet": "Risk and funds" + }, + "items": { + "plays": "Play types and limits", + "odds": "Odds", + "rebate": "Commission / rebate", + "risk-cap": "Payout caps", + "wallet": "Wallet thresholds" + } + }, + "versionStatus": { + "active": "Active", + "draft": "Draft", + "archived": "Archived" + }, + "versionSwitcher": { + "sheetTitle": "Switch configuration version", + "sheetDescription": "Choose a version to view on this page. Drafts are editable, while active and archived versions are read-only.", + "loading": "Loading…", + "noneSelected": "No version selected", + "switch": "Switch version", + "empty": "No version records yet.", + "count": "{{count}} items", + "effectiveAt": "Effective at: {{value}}", + "note": "Note: {{value}}", + "current": "Current", + "selected": "Selected", + "view": "View", + "rollback": "Rollback", + "delete": "Delete", + "deleteConfirmTitle": "Delete this version?", + "deleteConfirmDescription": "Version ID {{id}} (version_no {{version}}) will be permanently deleted. Active versions cannot be deleted." + }, + "versionActions": { + "publishCurrent": "Set as current version", + "refreshing": "Refreshing", + "refresh": "Refresh versions", + "newDraft": "New draft", + "saveDraft": "Save draft" + }, + "wallet": { + "title": "Wallet transfer limit settings", + "description": "Amounts use the game's minor currency unit (for example, under NPR, 100 = 1.00 NPR). The minimum amount must be at least 1 minor unit.", + "loadFailed": "Failed to load", + "saveSuccess": "Saved successfully", + "saveFailed": "Save failed", + "fields": { + "inMin": "Minimum transfer-in amount", + "inMax": "Maximum transfer-in amount", + "outMin": "Minimum transfer-out amount", + "outMax": "Maximum transfer-out amount" + }, + "placeholders": { + "min": "For example: 1.00", + "max": "For example: 10000.00" + }, + "hints": { + "inMin": "Per-order minimum from main wallet to lottery wallet", + "inMax": "Per-order maximum from main wallet to lottery wallet", + "outMin": "Per-order minimum from lottery wallet to main wallet", + "outMax": "Per-order maximum from lottery wallet to main wallet" + }, + "discard": "Discard changes" + }, + "play": { + "batchGroups": { + "d2": "2D Global", + "d3": "3D Global", + "d4": "4D Global", + "big-small": "Big / Small", + "position": "Position Plays", + "box": "Box Plays", + "jackpot": "Jackpot" + } + } +} diff --git a/src/i18n/locales/en/dashboard.json b/src/i18n/locales/en/dashboard.json index 27b0225..08da297 100644 --- a/src/i18n/locales/en/dashboard.json +++ b/src/i18n/locales/en/dashboard.json @@ -1,3 +1,57 @@ { - "title": "Dashboard" + "title": "Dashboard", + "refresh": "Refresh", + "notice": "Notice", + "todayBetTotal": "Current draw total bet", + "currentDrawFinanceSummary": "Finance summary for the current hall draw", + "currentPayout": "Current payout", + "payoutSummary": "Winning payout + Jackpot", + "currentProfit": "Current platform profit", + "profitFormula": "Bet - payout (approx.)", + "currentDraw": "Current draw", + "drawSequence": "Round {{sequence}}", + "drawDetails": "Draw details", + "ticketCount": "Ticket item count", + "relatedBetAmount": "Related bet amount", + "riskCapUsage": "Risk cap usage", + "lockedAndCap": "Locked {{locked}} / Cap {{cap}}", + "occupancyDetails": "Occupancy details", + "hotNumbersTop10": "Top 10 hot numbers", + "playDimension": "Play dimension", + "soldOutDistribution": "Sold-out distribution", + "soldOutTotal": "Total sold out", + "pendingReviewResults": "Pending result review", + "abnormalTransferOrders": "Abnormal transfer orders", + "viewTransferOrders": "View transfer orders", + "noSoldOutNumbers": "No sold-out numbers", + "noPoolData": "No pool data for this dimension", + "numbersByUsage": "Numbers by usage", + "capUsage": "Cap usage", + "tabs": { + "4d": "4D", + "3d": "3D", + "2d": "2D", + "special": "Special" + }, + "soldOutBuckets": { + "d4": "4D", + "d3": "3D", + "d2": "2D", + "special": "Special", + "other": "Other" + }, + "quickLinks": { + "createDrawPlan": "Create draw plan", + "drawSchedule": "Open sale / draws", + "results": "Results", + "tickets": "Ticket management", + "walletTransactions": "Wallet transactions", + "reports": "Reports", + "auditLogs": "Audit logs" + }, + "warnings": { + "drawPermission": "This account has no draw view/manage permission. Finance and risk data were not returned.", + "walletPermission": "This account has no wallet reconciliation permission. Abnormal transfer count was not returned.", + "loadFailed": "Failed to load. Check the API and login state." + } } diff --git a/src/i18n/locales/en/draws.json b/src/i18n/locales/en/draws.json new file mode 100644 index 0000000..b69b861 --- /dev/null +++ b/src/i18n/locales/en/draws.json @@ -0,0 +1,132 @@ +{ + "title": "Draws", + "statusListTitle": "Draw list", + "generatePlan": "Generate draw plan", + "generating": "Generating…", + "generateSuccess": "Generated {{created}} draws, buffer {{upcoming}}/{{target}}", + "generateFailed": "Generation failed", + "drawNo": "Draw no.", + "status": "Status", + "startTime": "Start time", + "closeTime": "Close time", + "drawTime": "Draw time", + "betTotal": "Total bet", + "payoutTotal": "Total payout", + "profitLoss": "Profit/Loss", + "actions": "Actions", + "queryDraw": "Search draw", + "reset": "Reset", + "fuzzyDrawNo": "Fuzzy draw no.", + "viewDetails": "View details", + "invalidDrawId": "Invalid draw ID", + "loadFailed": "Failed to load. Check login and API configuration.", + "drawDetail": "Draw details", + "businessDate": "Business date", + "sequenceNo": "Sequence no.", + "plannedDraw": "Planned draw", + "coolingEndTime": "Cooling ends at", + "resultSource": "Result source", + "currentResultVersion": "Current result version", + "settleVersion": "Settlement version", + "isReopened": "Reopened", + "yes": "Yes", + "no": "No", + "batchStats": "Batch stats", + "batchTotal": "Total batches", + "pendingReview": "Pending review", + "published": "Published", + "viewFinance": "View draw finance", + "drawActions": "Draw actions", + "drawActionsDesc": "Manual close / cancel / RNG / reopen / settlement all call backend APIs directly.", + "manualClose": "Manual close", + "cancelDraw": "Cancel draw", + "cancelBeforeDraw": "Cancel before draw", + "rngDraw": "RNG draw", + "rngAutoGenerate": "RNG auto generate", + "reopen": "Reopen", + "cooldownReopen": "Reopen in cooldown", + "runSettlement": "Run settlement", + "processing": "Processing…", + "actionSuccess": "{{name}} succeeded", + "actionFailed": "{{name}} failed", + "hallPreviewStatus": "Hall preview {{status}}", + "financeOverview": "Draw finance overview", + "orderAndItemCount": "Orders / Ticket items", + "actualBet": "Actual bet deducted", + "currentPayout": "Current payout total", + "grossProfit": "Approx. gross profit", + "settlementBatchList": "Settlement batch list (filter by draw)", + "relatedSettlementBatches": "Related settlement batches", + "noSettlementBatches": "No settlement batch records.", + "ticketCount": "Tickets", + "winCount": "Wins", + "finishedAt": "Finished at", + "resultsTitle": "Results", + "reviewAndPublish": "Review / publish", + "viewReviewQueue": "View review queue", + "noPublishedBatch": "No published batches.", + "version": "Version v{{version}}", + "sourceType": "Source {{source}}", + "manualEntry": "Manual", + "rng": "RNG", + "rngSummary": "RNG hash {{hash}}", + "confirmedAt": "Confirmed at {{time}}", + "prize": "Prize", + "tail3": "Last 3", + "tail2": "Last 2", + "headTail": "Head/Tail", + "manualResultEntry": "Manual result entry", + "currentStatusAndDraft": "Current status {{status}}. Saving creates a pending batch and does not publish it.", + "enter23Numbers": "Please enter all 23 groups of 4 digits", + "draftSaved": "Draft v{{version}} saved, waiting to be published", + "saveFailed": "Failed to save", + "clear": "Clear", + "saveDraft": "Save draft", + "saving": "Saving…", + "pendingBatches": "Pending batches", + "noPendingBatches": "There are no pending_review batches.", + "batchId": "Batch ID", + "numberCount": "Number count", + "reviewAndPublishAction": "Review and publish", + "noPublishPermission": "No publish permission", + "batchNotFound": "Batch not found", + "batchNotFoundDesc": "Return to the review list and confirm the batch ID.", + "backToReviewQueue": "Back to review queue", + "publishTitle": "Publish", + "cannotPublish": "Cannot publish", + "cannotPublishDesc": "Current batch status is '{{status}}'.", + "checkBeforePublish": "Check the numbers before publishing", + "checkBeforePublishDesc": "Publish only after confirming the numbers.", + "publishedView": "View published result", + "confirmPublish": "Confirm publish", + "submitting": "Submitting…", + "publishSuccess": "Published · {{drawNo}} · status {{status}}", + "publishFailed": "Publish failed", + "sourceTypeFull": "Source: {{source}} · Items: {{count}}/23 · RNG hash: {{hash}}", + "subnav": { + "status": "Draw status", + "results": "Results", + "finance": "Draw finance", + "review": "Review & publish" + }, + "statusOptions": { + "all": "All", + "pending": "Pending", + "open": "Open", + "closing": "Closing", + "closed": "Closed", + "drawing": "Drawing", + "review": "Review", + "cooldown": "Cooldown", + "settling": "Settling", + "settled": "Settled", + "cancelled": "Cancelled" + }, + "resultSlots": { + "first": "1st prize", + "second": "2nd prize", + "third": "3rd prize", + "starter": "Starter {{index}}", + "consolation": "Consolation {{index}}" + } +} diff --git a/src/i18n/locales/en/jackpot.json b/src/i18n/locales/en/jackpot.json new file mode 100644 index 0000000..a74ceb7 --- /dev/null +++ b/src/i18n/locales/en/jackpot.json @@ -0,0 +1,46 @@ +{ + "title": "Jackpot", + "configTitle": "Jackpot pool configuration", + "loadFailed": "Failed to load", + "saveSuccess": "Saved", + "saveFailed": "Save failed", + "invalidDrawId": "Enter a valid draw ID", + "manualBurstSuccess": "Jackpot burst triggered manually", + "manualBurstFailed": "Manual burst failed", + "noPoolData": "No pool data", + "displayBalance": "Display balance {{amount}}", + "currentAmount": "Current pool balance (minor unit)", + "contributionRate": "Contribution rate 0-1", + "triggerThreshold": "Burst threshold (minor unit)", + "payoutRate": "Burst payout rate 0-1", + "forceTriggerGap": "Force burst gap (settled draws)", + "minBetAmount": "Minimum bet amount (minor unit)", + "comboTriggerPlays": "Combo trigger plays (comma separated)", + "status": "Status", + "disabled": "Disabled", + "enabled": "Enabled", + "saving": "Saving…", + "save": "Save", + "manualBurstDrawId": "Manual burst draw ID", + "manualBurstAmount": "Burst amount (empty for all)", + "processing": "Processing…", + "manualBurst": "Manual burst", + "filter": "Filter", + "drawNo": "Draw no.", + "optional": "Optional", + "apply": "Apply", + "payoutRecords": "Jackpot payout records", + "contributionRecords": "Jackpot contribution records", + "subnavLabel": "Jackpot sub navigation", + "subnavPools": "Pool configuration", + "subnavRecords": "Records", + "payoutLoadFailed": "Failed to load payout records", + "contributionLoadFailed": "Failed to load contribution records", + "trigger": "Trigger", + "payoutAmount": "Payout amount", + "winnerCount": "Winner count", + "time": "Time", + "ticketNo": "Ticket", + "player": "Player", + "contributionAmount": "Contribution amount" +} diff --git a/src/i18n/locales/en/players.json b/src/i18n/locales/en/players.json new file mode 100644 index 0000000..08c194a --- /dev/null +++ b/src/i18n/locales/en/players.json @@ -0,0 +1,49 @@ +{ + "title": "Players", + "listTitle": "Player list", + "createPlayer": "Create player", + "searchPlaceholder": "Search by player ID / username / nickname", + "search": "Search", + "refresh": "Refresh", + "loadFailed": "Failed to load player list", + "siteCodeRequired": "Enter the site code", + "sitePlayerIdRequired": "Enter the site player ID", + "createFailed": "Failed to create player", + "createSuccess": "Created player {{name}}", + "noChanges": "No changes", + "updateFailed": "Failed to update player", + "updateSuccess": "Updated {{name}}", + "deleteFailed": "Delete failed", + "deleteSuccess": "Deleted player {{name}}", + "statusNormal": "Normal", + "statusFrozen": "Frozen", + "statusBanned": "Banned", + "site": "Site", + "sitePlayerId": "Site player ID", + "username": "Username", + "nickname": "Nickname", + "currency": "Currency", + "balance": "Balance", + "available": "Available", + "status": "Status", + "lastLogin": "Last login", + "actions": "Actions", + "edit": "Edit", + "delete": "Delete", + "createDialogTitle": "Create player", + "editDialogTitle": "Edit player", + "createDialogDesc": "Manually register a main-site player to the lottery platform. Usually this is created automatically through SSO login.", + "editDialogDesc": "Edit player information.", + "siteCode": "Site code", + "siteCodePlaceholder": "For example main_site", + "sitePlayerIdLabel": "Site player ID", + "sitePlayerIdPlaceholder": "Unique identifier returned by the main site", + "usernamePlaceholderOptional": "Optional", + "nicknamePlaceholderOptional": "Optional", + "defaultCurrency": "Default currency", + "cancel": "Cancel", + "save": "Save", + "saving": "Saving…", + "confirmDelete": "Confirm delete", + "confirmDeleteDesc": "Delete player {{name}}? This action cannot be undone." +} diff --git a/src/i18n/locales/en/reconcile.json b/src/i18n/locales/en/reconcile.json new file mode 100644 index 0000000..a63bf6c --- /dev/null +++ b/src/i18n/locales/en/reconcile.json @@ -0,0 +1,45 @@ +{ + "title": "Reconcile", + "createTitle": "Create reconcile job", + "createDesc": "Abnormal flows are checked automatically by scheduled jobs. This section allows finance to trigger jobs manually: choose reconcile type and time range, and optionally fill in target references (player IDs, transfer numbers, or idempotency keys, one per line). Jobs and items are persisted for audit and future automation.", + "reconcileType": "Reconcile type", + "walletTransfer": "Wallet transfer (main site ⇄ lottery)", + "startTime": "Start time", + "endTime": "End time", + "scope": "Scope (optional)", + "scopePlaceholder": "One reference per line, for example player ID, wallet transfer number, or idempotency key.\nLeave empty to create a scoped job record without explicit refs.", + "scopeHint": "When reconciling with wallet transactions in pending_reconcile status, paste the transfer number or idempotency key above.", + "advancedToggleOpen": "Show advanced options (custom items JSON)", + "advancedToggleClose": "Hide advanced options (custom items JSON)", + "advancedJson": "Items JSON (overrides generated rows from the scope above)", + "createTask": "Create reconcile job", + "submitting": "Submitting…", + "loadFailed": "Failed to load", + "loadItemsFailed": "Failed to load details", + "periodRequired": "Enter both reconcile start and end time", + "periodInvalid": "Invalid time range", + "periodOrderInvalid": "End time must be later than or equal to start time", + "advancedJsonInvalid": "The advanced JSON cannot be parsed", + "createSuccess": "Reconcile job created", + "createFailed": "Failed to create job", + "noCreatePermission": "Current account cannot create reconcile jobs.", + "jobsTitle": "Reconcile jobs", + "jobsDesc": "Click a row to view paginated item details.", + "refresh": "Refresh", + "jobNo": "Job no.", + "type": "Type", + "status": "Status", + "period": "Period", + "createdAt": "Created at", + "detailsTitle": "Job details", + "sideARef": "Lottery ref", + "sideBRef": "Main site ref", + "differenceAmount": "Difference (cent)", + "noDetails": "No details", + "statusCompleted": "Completed", + "statusRunning": "Running", + "statusFailed": "Failed", + "itemMismatch": "Mismatch", + "itemMatched": "Matched", + "itemPendingCheck": "Pending check" +} diff --git a/src/i18n/locales/en/reports.json b/src/i18n/locales/en/reports.json index 4f60ac8..eca2f0d 100644 --- a/src/i18n/locales/en/reports.json +++ b/src/i18n/locales/en/reports.json @@ -1,3 +1,34 @@ { - "title": "Reports" + "title": "Reports", + "createExport": "Create export", + "reportType": "Report type", + "exportFormat": "Export format", + "filterJson": "filter_json (optional)", + "parseFilterFailed": "Failed to parse filter JSON", + "createSuccess": "Export job created", + "createFailed": "Failed to create job", + "downloadFailed": "Download failed", + "taskList": "Job list", + "jobId": "Job no.", + "type": "Type", + "format": "Format", + "status": "Status", + "output": "Output", + "download": "Download", + "createdAt": "Created at", + "id": "ID", + "empty": "No data", + "reportTypes": { + "draw_profit_summary": "Draw profit summary", + "daily_profit_summary": "Daily profit summary", + "player_win_loss": "Player win/loss report", + "wallet_transfer_report": "Wallet transfer report", + "hot_number_risk_report": "Hot number risk report", + "play_dimension_report": "Play dimension report", + "sold_out_number_report": "Sold-out number report", + "rebate_commission_report": "Rebate and commission report", + "audit_operation_report": "Audit operation report", + "wallet_txns_daily": "Wallet transactions daily", + "transfer_orders_daily": "Transfer orders daily" + } } diff --git a/src/i18n/locales/en/risk.json b/src/i18n/locales/en/risk.json new file mode 100644 index 0000000..45ffab1 --- /dev/null +++ b/src/i18n/locales/en/risk.json @@ -0,0 +1,91 @@ +{ + "title": "Risk", + "center": "Risk center", + "drawNo": "Draw no.", + "status": "Status", + "closeTime": "Close time", + "actions": "Actions", + "all": "All", + "search": "Search", + "refresh": "Refresh", + "fuzzyDrawNo": "Fuzzy draw no.", + "loadDrawListFailed": "Failed to load draw list", + "enterRisk": "Enter risk", + "poolsTitle": "Risk pools", + "searchNumber": "Search number", + "searchNumberPlaceholder": "For example 8888", + "riskFilter": "Risk filter", + "sort": "Sort", + "filterAll": "All", + "filterSoldOut": "Sold out", + "filterHighRisk": ">80%", + "sortUsageDesc": "Usage ratio ↓", + "sortLockedDesc": "Locked amount ↓", + "sortRemainingAsc": "Remaining ↑", + "sortNumberAsc": "Number ↑", + "loadPoolsFailed": "Failed to load risk pools", + "capAmount": "Cap", + "lockedAmount": "Locked", + "remainingAmount": "Remaining", + "usageRatio": "Usage", + "poolStatus": "Status", + "soldOut": "Sold out", + "warning": "Warning", + "normal": "Normal", + "recover": "Recover", + "close": "Close", + "view": "View", + "manualCloseSuccess": "Number betting closed manually", + "recoverSuccess": "Number betting recovered", + "actionFailed": "Action failed", + "detailTitle": "Risk pool details", + "loadDetailFailed": "Failed to load risk pool details", + "backToList": "Back to list", + "backToAllPools": "Back to all risk pools", + "numberTitle": "Number {{number}}", + "drawMeta": "Draw {{drawNo}}", + "totalCap": "Cap amount", + "lockedWorstCase": "Locked (worst-case payout reserved)", + "remainingSellable": "Remaining sellable", + "isSoldOut": "Sold out", + "yes": "Yes", + "no": "No", + "occupationLogs": "Occupancy / release logs", + "time": "Time", + "action": "Action", + "amount": "Amount", + "source": "Source", + "ticketNo": "Ticket no.", + "playCode": "Play", + "loadLogsFailed": "Failed to load lock logs", + "lockLogsTitle": "Risk lock logs", + "drawInfoLoadFailed": "Failed to load draw info", + "loadingDraw": "Loading draw…", + "headerTitle": "Risk · Draw {{drawNo}}", + "databaseStatus": "Database status", + "hallPreviewStatus": "(Hall preview: {{status}})", + "subnavOccupancy": "Occupancy", + "subnavHot": "Hot numbers", + "subnavSoldOut": "Sold-out list", + "subnavPools": "All risk pools", + "changeDraw": "Change draw", + "number4d": "Number (4 digits)", + "optional": "Optional", + "actionFilter": "Action", + "noLimit": "No limit", + "lock": "Lock", + "release": "Release", + "applyFilter": "Apply filter", + "statusOptions": { + "pending": "Pending", + "open": "Open", + "closing": "Closing", + "closed": "Closed", + "drawing": "Drawing", + "review": "Review", + "cooldown": "Cooldown", + "settling": "Settling", + "settled": "Settled", + "cancelled": "Cancelled" + } +} diff --git a/src/i18n/locales/en/settlement.json b/src/i18n/locales/en/settlement.json new file mode 100644 index 0000000..e6cb5e1 --- /dev/null +++ b/src/i18n/locales/en/settlement.json @@ -0,0 +1,54 @@ +{ + "title": "Settlement", + "filter": "Filter", + "drawNo": "Draw no.", + "status": "Status", + "apply": "Apply", + "batchList": "Settlement batches", + "loadFailed": "Failed to load", + "exportFailed": "Export failed", + "actionSuccess": "{{name}} succeeded", + "actionFailed": "{{name}} failed", + "placeholderDrawNo": "For example 20260511-001", + "reviewStatus": "Review status", + "ticketCount": "Ticket count", + "winCount": "Win count", + "payoutTotal": "Total payout", + "jackpot": "Jackpot", + "finishedAt": "Finished at", + "details": "Details", + "approve": "Approve", + "pass": "Pass", + "reject": "Reject", + "payout": "Payout", + "export": "Export", + "backToList": "Back to batch list", + "errorTitle": "Error", + "retry": "Retry", + "batchSummary": "Batch #{{id}}", + "summaryMeta": "Draw {{drawNo}} · draw status {{drawStatus}} · result batch v{{version}}", + "settlementStatus": "Settlement status", + "reviewState": "Review status", + "ticketTotal": "Ticket count", + "winTotal": "Win count", + "payoutAmount": "Payout total", + "jackpotPayout": "Jackpot payout", + "startedAt": "Started", + "endedAt": "Ended", + "runPayout": "Run payout", + "exportSettlementReport": "Export settlement report", + "loadingSummary": "Loading summary…", + "detailTitle": "Settlement details", + "ticketNo": "Ticket no.", + "playCode": "Play", + "player": "Player", + "matchedTier": "Matched tier", + "regularPayout": "Regular payout", + "loadingDetails": "Loading details…", + "statusOptions": { + "all": "All", + "running": "Running", + "completed": "Completed", + "failed": "Failed" + } +} diff --git a/src/i18n/locales/en/tickets.json b/src/i18n/locales/en/tickets.json new file mode 100644 index 0000000..1f231bc --- /dev/null +++ b/src/i18n/locales/en/tickets.json @@ -0,0 +1,19 @@ +{ + "title": "Tickets", + "playerTicketQuery": "Player ticket query", + "playerId": "Player ID", + "invalidPlayerId": "Enter a valid player ID", + "drawNoOptional": "Draw no. (optional)", + "drawNoPlaceholder": "For example 20260520-001", + "query": "Query", + "loadFailed": "Failed to load", + "ticketNo": "Ticket no.", + "orderNo": "Order no.", + "drawNo": "Draw no.", + "playCode": "Play", + "number": "Number", + "actualDeduct": "Actual deduct", + "status": "Status", + "failReason": "Fail reason", + "winAmount": "Win amount" +} diff --git a/src/i18n/locales/en/wallet.json b/src/i18n/locales/en/wallet.json new file mode 100644 index 0000000..bafbd09 --- /dev/null +++ b/src/i18n/locales/en/wallet.json @@ -0,0 +1,69 @@ +{ + "title": "Wallet", + "subnavLabel": "Wallet sub pages", + "subnavTransactions": "Wallet transactions", + "subnavTransferOrders": "Transfer orders", + "noPermission": "Current account has no access to this page", + "copySuccess": "{{label}} copied to clipboard", + "copyFailed": "Copy failed. Check browser permissions or copy manually.", + "statusProcessing": "Processing", + "statusSuccess": "Success", + "statusFailed": "Failed", + "statusPendingReconcile": "Pending reconcile", + "statusReversed": "Reversed", + "statusManuallyProcessed": "Manually processed", + "statusPosted": "Posted", + "filterAll": "All", + "transferIn": "Main site transfer in", + "transferOut": "Main site transfer out", + "transferOutRefund": "Transfer-out refund", + "transferOrders": "Transfer orders", + "walletTransactions": "Wallet transactions", + "playerWalletQuery": "Player wallet query", + "localTransferNo": "Local transfer no.", + "externalRefNo": "Main site ref no.", + "playerAccount": "Player account", + "playerAccountPlaceholder": "Main site player ID or username (fuzzy)", + "playerId": "Player ID", + "playerIdOptional": "Optional, higher priority than account", + "requestDateRange": "Request date range", + "status": "Status", + "options": "Options", + "abnormalOnly": "Abnormal only", + "abnormalOnlyPending": "Abnormal only (pending reconcile)", + "search": "Search", + "resetFilters": "Reset filters", + "refreshCurrentPage": "Refresh current page", + "loadFailed": "Failed to load", + "direction": "Direction", + "amount": "Amount", + "failReason": "Fail reason", + "requestTime": "Requested at", + "finishedTime": "Finished at", + "actions": "Actions", + "reverse": "Reverse", + "manualProcess": "Manual process", + "processing": "Processing…", + "reverseSuccess": "Reversed successfully", + "manualProcessSuccess": "Manually processed successfully", + "actionFailed": "Action failed", + "txnNo": "Txn no.", + "bizType": "Business type", + "type": "Type", + "queryFailed": "Query failed", + "invalidPlayerId": "Enter a valid player ID", + "querying": "Querying…", + "query": "Query", + "sitePlayer": "Site player", + "walletType": "Type", + "currency": "Currency", + "balanceMinor": "Balance (minor unit)", + "availableBalance": "Available (estimated)", + "noWalletRows": "No wallet rows. Players with no bets or transfers may have no records.", + "copyTransferNo": "Local transfer no.", + "copyExternalRefNo": "Main site ref no.", + "copyTxnNo": "Txn no.", + "copyExternalTxnRefNo": "Main site ref no.", + "in": "In", + "out": "Out" +} diff --git a/src/i18n/locales/ne/adminUsers.json b/src/i18n/locales/ne/adminUsers.json new file mode 100644 index 0000000..b03cec1 --- /dev/null +++ b/src/i18n/locales/ne/adminUsers.json @@ -0,0 +1,83 @@ +{ + "title": "प्रशासक", + "listTitle": "प्रशासक सूची", + "createAdmin": "प्रशासक सिर्जना", + "searchPlaceholder": "प्रयोगकर्ता नाम / उपनाम / इमेलबाट खोज्नुहोस्", + "loadFailed": "प्रशासक सूची लोड असफल भयो", + "nicknameRequired": "उपनाम लेख्नुहोस्", + "newPasswordMin": "नयाँ पासवर्ड कम्तीमा 8 वर्ण हुनुपर्छ", + "roleRequired": "कम्तीमा एउटा भूमिका छान्नुहोस्", + "usernameRequired": "लगइन प्रयोगकर्ता नाम लेख्नुहोस्", + "passwordMin": "पासवर्ड कम्तीमा 8 वर्ण हुनुपर्छ", + "createSuccess": "प्रशासक {{name}} सिर्जना भयो", + "updateSuccess": "{{name}} अपडेट भयो", + "saveAccountFailed": "खाता सुरक्षित गर्न असफल", + "deleteSuccess": "{{name}} मेटाइयो", + "deleteFailed": "मेटाउन असफल", + "allPermissions": "सबै अनुमति", + "saveRoleSuccess": "{{name}} को भूमिका अपडेट भयो", + "saveRoleFailed": "भूमिका सुरक्षित गर्न असफल", + "savePermissionSuccess": "{{name}} को अनुमति अपडेट भयो", + "savePermissionFailed": "अनुमति सुरक्षित गर्न असफल", + "saving": "सेभ हुँदैछ…", + "deleting": "मेटिँदैछ…", + "common": { + "none": "कुनै छैन" + }, + "table": { + "account": "खाता", + "nickname": "उपनाम", + "status": "स्थिति", + "roles": "भूमिका", + "direct": "प्रत्यक्ष", + "effective": "प्रभावी", + "actions": "कार्य" + }, + "status": { + "enabled": "सक्रिय", + "disabled": "निष्क्रिय" + }, + "actions": { + "permissions": "अनुमति", + "edit": "सम्पादन", + "delete": "मेटाउनुहोस्", + "cancel": "रद्द गर्नुहोस्", + "save": "सेभ गर्नुहोस्" + }, + "permissionDialog": { + "title": "प्रशासक अनुमति", + "rolesTitle": "भूमिका", + "rolesDescription": "पूर्वनिर्धारित साइट भूमिकाको रूपमा सुरक्षित हुन्छ र प्रत्यक्ष अनुमतिसँग जोडिएर प्रभावी अनुमति बन्छ।", + "rolePermissionCount": "{{count}} वटा कार्य अनुमति समावेश", + "directTitle": "प्रत्यक्ष अनुमति", + "directDescription": "मेनु वा व्यवसाय क्षेत्र अनुसार विस्तार गरी prd.* अनुमति छान्नुहोस्; धेरैजसो अवस्थामा भूमिका बदल्नु पर्याप्त हुन्छ।", + "selectedRoles": "हाल छनोट गरिएका भूमिका:", + "saveRoles": "भूमिका सेभ गर्नुहोस्", + "saveDirect": "प्रत्यक्ष अनुमति सेभ गर्नुहोस्" + }, + "accountDialog": { + "createTitle": "प्रशासक सिर्जना", + "editTitle": "खाता सम्पादन", + "createDescription": "कम्तीमा एउटा पूर्वनिर्धारित साइट भूमिका तोक्नुपर्छ। लगइन नाममा अक्षर, अंक, डट, अन्डरस्कोर र हाइफन मात्र प्रयोग गर्न सकिन्छ र सेभ भएपछि साना अक्षरमा राखिन्छ।", + "editDescription": "लगइन नाम परिवर्तन गर्न मिल्दैन। पासवर्ड खाली छोडेमा परिवर्तन हुँदैन।", + "username": "लगइन नाम", + "usernamePlaceholder": "उदाहरण: ops_admin", + "nickname": "उपनाम", + "nicknamePlaceholder": "देखिने नाम", + "emailOptional": "इमेल (वैकल्पिक)", + "emailPlaceholder": "नचाहिए खाली छोड्नुहोस्", + "password": "पासवर्ड", + "passwordOptional": "पासवर्ड (वैकल्पिक)", + "passwordPlaceholderCreate": "कम्तीमा 8 वर्ण", + "passwordPlaceholderEdit": "परिवर्तन नगर्न खाली छोड्नुहोस्", + "rolesRequired": "भूमिका (पूर्वनिर्धारित साइट, कम्तीमा एक)", + "rolesDescription": "सिर्जना भएपछि अनुमतिमा गएर भूमिका वा प्रत्यक्ष अनुमति थप समायोजन गर्न सकिन्छ।", + "noRoles": "अहिले भूमिका डाटा छैन। सूची लोड भएपछि फेरि प्रयास गर्नुहोस्।" + }, + "delete": { + "currentUserBlocked": "हाल लगइन गरिएको खाता मेटाउन मिल्दैन", + "rowActionTitle": "यो प्रशासक मेटाउनुहोस्", + "confirmTitle": "मेटाउने पुष्टि", + "confirmDescription": "प्रशासक {{name}} मेटाउने? यो कार्य फिर्ता लिन सकिँदैन।" + } +} diff --git a/src/i18n/locales/ne/audit.json b/src/i18n/locales/ne/audit.json index 856e3fb..3910cc9 100644 --- a/src/i18n/locales/ne/audit.json +++ b/src/i18n/locales/ne/audit.json @@ -1,3 +1,14 @@ { - "title": "अडिट लग" + "title": "अडिट लग", + "moduleCode": "मोड्युल कोड", + "actionCode": "कार्य कोड", + "operatorType": "अपरेटर प्रकार", + "exactMatch": "ठ्याक्कै मिलान", + "operatorTypePlaceholder": "जस्तै admin / system", + "operator": "अपरेटर", + "module": "मोड्युल", + "action": "कार्य", + "target": "लक्ष्य", + "time": "समय", + "empty": "डाटा छैन" } diff --git a/src/i18n/locales/ne/auth.json b/src/i18n/locales/ne/auth.json index 03aaae9..49cf42e 100644 --- a/src/i18n/locales/ne/auth.json +++ b/src/i18n/locales/ne/auth.json @@ -1,3 +1,24 @@ { - "title": "लगइन" + "title": "लगइन", + "loginTitle": "एडमिन लगइन", + "account": "खाता", + "accountPlaceholder": "लगइन खाता", + "password": "पासवर्ड", + "passwordPlaceholder": "पासवर्ड", + "captcha": "क्याप्चा", + "captchaPlaceholder": "क्याप्चा लेख्नुहोस्", + "captchaLoading": "क्याप्चा लोड हुँदैछ", + "captchaRefresh": "क्याप्चा रिफ्रेस गर्न क्लिक गर्नुहोस्", + "captchaFetch": "क्याप्चा लिन क्लिक गर्नुहोस्", + "apiMissingTitle": "API ठेगाना सेट गरिएको छैन", + "apiMissingDescriptionPrefix": "परिवेशमा", + "apiMissingDescriptionSuffix": "सेट गर्नुहोस् (Laravel root URL, जस्तै http://127.0.0.1:8000)।", + "submit": "लगइन", + "submitting": "लगइन हुँदैछ…", + "captchaLoadFailed": "क्याप्चा लोड गर्न सकिएन। API वा नेटवर्क जाँच गर्नुहोस्।", + "apiBaseMissingToast": "NEXT_PUBLIC_LOTTERY_API_BASE_URL सेट गरिएको छैन", + "captchaRequired": "पहिले क्याप्चा रिफ्रेस गर्नुहोस्", + "welcome": "स्वागत छ, {{name}}", + "networkFailed": "नेटवर्क अनुरोध असफल भयो", + "loginFailed": "लगइन असफल भयो" } diff --git a/src/i18n/locales/ne/common.json b/src/i18n/locales/ne/common.json index c0710cb..7e02efa 100644 --- a/src/i18n/locales/ne/common.json +++ b/src/i18n/locales/ne/common.json @@ -4,7 +4,7 @@ "ne": "नेपाली", "zh": "中文", "title": "इन्टरफेस भाषा", - "changed": "भाषा" + "changed": "भाषा {{language}} मा परिवर्तन भयो" }, "app": { "title": "Lottery Admin" @@ -15,6 +15,65 @@ "search": "खोज", "apply": "लागू गर्नुहोस्", "loading": "लोड हुँदैछ...", - "submitting": "पेश हुँदैछ..." + "submitting": "पेश हुँदैछ...", + "logout": "लगआउट", + "close": "बन्द गर्नुहोस्", + "viewAll": "सबै हेर्नुहोस्", + "viewDetails": "विवरण हेर्नुहोस्", + "reviewNow": "अहिले समीक्षा गर्नुहोस्", + "create": "सिर्जना गर्नुहोस्", + "createTask": "टास्क सिर्जना गर्नुहोस्", + "clear": "खाली गर्नुहोस्", + "done": "समाप्त" + }, + "date": { + "placeholder": "मिति छान्नुहोस्", + "rangePlaceholder": "मिति दायरा छान्नुहोस्", + "rangeHint": "सुरु मिति छान्नुहोस्, त्यसपछि अन्त्य मिति। एउटै दिनका लागि सोही मितिमा दुई पटक क्लिक गर्नुहोस्। बन्द गर्न Done थिच्नुहोस्।" + }, + "pagination": { + "perPage": "प्रति पृष्ठ", + "selectPlaceholder": "छान्नुहोस्", + "summary": "कुल {{total}}; पृष्ठ {{page}} / {{lastPage}}", + "previous": "अघिल्लो", + "next": "अर्को" + }, + "states": { + "noData": "डाटा छैन", + "loading": "लोड हुँदैछ…", + "comingSoon": "सुविधा विकासमा छ" + }, + "errors": { + "loadFailed": "लोड असफल भयो" + }, + "toolbar": { + "defaultAdmin": "प्रशासक", + "notifications": "सूचना", + "notificationsComingSoon": "सूचना सुविधा विकासमा छ", + "accountSettings": "खाता सेटिङ", + "loggedOut": "लगआउट भयो" + }, + "nav": { + "home": "गृह", + "dashboard": "ड्यासबोर्ड", + "admin_users": "प्रशासक सूची", + "players": "खेलाडी सूची", + "wallet": "वालेट", + "draws": "ड्रअहरू", + "config": "कन्फिगरेसन", + "risk": "जोखिम", + "settlement": "सेटलमेन्ट", + "jackpot": "Jackpot", + "reconcile": "मिलान", + "tickets": "टिकटहरू", + "reports": "रिपोर्टहरू", + "audit": "अडिट लग", + "settings": "सेटिङ" + }, + "sidebar": { + "workspace": "कार्यस्थान" + }, + "auth": { + "checking": "लगइन स्थिति जाँच हुँदैछ…" } } diff --git a/src/i18n/locales/ne/config.json b/src/i18n/locales/ne/config.json new file mode 100644 index 0000000..8b28f6f --- /dev/null +++ b/src/i18n/locales/ne/config.json @@ -0,0 +1,83 @@ +{ + "title": "कन्फिगरेसन केन्द्र", + "nav": { + "aria": "सञ्चालन कन्फिगरेसन उप-नेभिगेसन", + "sidebarTitle": "सञ्चालन कन्फिगरेसन", + "groups": { + "betting": "बेटिङ र प्रदर्शन", + "risk_wallet": "जोखिम र कोष" + }, + "items": { + "plays": "खेल प्रकार र सीमा", + "odds": "अड्स", + "rebate": "कमिसन / रिबेट", + "risk-cap": "पेमेन्ट क्याप", + "wallet": "वालेट थ्रेसहोल्ड" + } + }, + "versionStatus": { + "active": "सक्रिय", + "draft": "ड्राफ्ट", + "archived": "अभिलेख" + }, + "versionSwitcher": { + "sheetTitle": "कन्फिगरेसन संस्करण बदल्नुहोस्", + "sheetDescription": "यस पृष्ठमा हेर्न एउटा संस्करण छान्नुहोस्। ड्राफ्ट सम्पादनयोग्य छ; सक्रिय र अभिलेख संस्करण केवल पढ्न मिल्ने छन्।", + "loading": "लोड हुँदैछ…", + "noneSelected": "कुनै संस्करण छानिएको छैन", + "switch": "संस्करण बदल्नुहोस्", + "empty": "संस्करण रेकर्ड छैन।", + "count": "{{count}} वटा", + "effectiveAt": "लागू समय: {{value}}", + "note": "टिप्पणी: {{value}}", + "current": "हाल हेर्दै", + "selected": "छानिएको", + "view": "हेर्नुहोस्", + "rollback": "रोलब्याक", + "delete": "मेटाउनुहोस्", + "deleteConfirmTitle": "यो संस्करण मेटाउने?", + "deleteConfirmDescription": "संस्करण ID {{id}} (version_no {{version}}) स्थायी रूपमा मेटाइनेछ। सक्रिय संस्करण मेटाउन मिल्दैन।" + }, + "versionActions": { + "publishCurrent": "हालको संस्करण बनाउनुहोस्", + "refreshing": "रिफ्रेस हुँदैछ", + "refresh": "संस्करण रिफ्रेस", + "newDraft": "नयाँ ड्राफ्ट", + "saveDraft": "ड्राफ्ट सेभ गर्नुहोस्" + }, + "wallet": { + "title": "वालेट ट्रान्सफर सीमा सेटिङ", + "description": "रकम खेलको सानो मुद्रा एकाइमा हुन्छ (उदाहरण, NPR मा 100 = 1.00 NPR)। न्यूनतम रकम कम्तीमा 1 सानो एकाइ हुनुपर्छ।", + "loadFailed": "लोड असफल भयो", + "saveSuccess": "सफलतापूर्वक सेभ भयो", + "saveFailed": "सेभ असफल भयो", + "fields": { + "inMin": "न्यूनतम ट्रान्सफर-इन रकम", + "inMax": "अधिकतम ट्रान्सफर-इन रकम", + "outMin": "न्यूनतम ट्रान्सफर-आउट रकम", + "outMax": "अधिकतम ट्रान्सफर-आउट रकम" + }, + "placeholders": { + "min": "उदाहरण: 1.00", + "max": "उदाहरण: 10000.00" + }, + "hints": { + "inMin": "मुख्य वालेटबाट लटरी वालेटमा प्रति अर्डर न्यूनतम", + "inMax": "मुख्य वालेटबाट लटरी वालेटमा प्रति अर्डर अधिकतम", + "outMin": "लटरी वालेटबाट मुख्य वालेटमा प्रति अर्डर न्यूनतम", + "outMax": "लटरी वालेटबाट मुख्य वालेटमा प्रति अर्डर अधिकतम" + }, + "discard": "परिवर्तन त्याग्नुहोस्" + }, + "play": { + "batchGroups": { + "d2": "2D ग्लोबल", + "d3": "3D ग्लोबल", + "d4": "4D ग्लोबल", + "big-small": "Big / Small", + "position": "स्थिति खेलहरू", + "box": "बक्स खेलहरू", + "jackpot": "Jackpot" + } + } +} diff --git a/src/i18n/locales/ne/dashboard.json b/src/i18n/locales/ne/dashboard.json index e4481a9..64e39d9 100644 --- a/src/i18n/locales/ne/dashboard.json +++ b/src/i18n/locales/ne/dashboard.json @@ -1,3 +1,57 @@ { - "title": "ड्यासबोर्ड" + "title": "ड्यासबोर्ड", + "refresh": "रिफ्रेस", + "notice": "सूचना", + "todayBetTotal": "हालको ड्रअ कुल बेट", + "currentDrawFinanceSummary": "हालको हल ड्रअको वित्तीय सारांश", + "currentPayout": "हालको भुक्तानी", + "payoutSummary": "जितेको भुक्तानी + Jackpot", + "currentProfit": "हालको प्लेटफर्म नाफा", + "profitFormula": "बेट - भुक्तानी (अनुमानित)", + "currentDraw": "हालको ड्रअ", + "drawSequence": "राउन्ड {{sequence}}", + "drawDetails": "ड्रअ विवरण", + "ticketCount": "टिकट वस्तु संख्या", + "relatedBetAmount": "सम्बन्धित बेट रकम", + "riskCapUsage": "जोखिम क्याप प्रयोग", + "lockedAndCap": "लक {{locked}} / क्याप {{cap}}", + "occupancyDetails": "अकुपेन्सी विवरण", + "hotNumbersTop10": "शीर्ष 10 हट नम्बर", + "playDimension": "प्ले डाइमेन्सन", + "soldOutDistribution": "बिक्री समाप्त वितरण", + "soldOutTotal": "कुल बिक्री समाप्त", + "pendingReviewResults": "समीक्षा बाँकी परिणाम", + "abnormalTransferOrders": "असामान्य ट्रान्सफर अर्डर", + "viewTransferOrders": "ट्रान्सफर अर्डर हेर्नुहोस्", + "noSoldOutNumbers": "बिक्री समाप्त नम्बर छैन", + "noPoolData": "यस डाइमेन्सनमा पूल डाटा छैन", + "numbersByUsage": "प्रयोग अनुसार नम्बर", + "capUsage": "क्याप प्रयोग", + "tabs": { + "4d": "4D", + "3d": "3D", + "2d": "2D", + "special": "विशेष" + }, + "soldOutBuckets": { + "d4": "4D", + "d3": "3D", + "d2": "2D", + "special": "विशेष", + "other": "अन्य" + }, + "quickLinks": { + "createDrawPlan": "ड्रअ योजना सिर्जना", + "drawSchedule": "खुला बिक्री / ड्रअ", + "results": "परिणाम", + "tickets": "टिकट व्यवस्थापन", + "walletTransactions": "वालेट कारोबार", + "reports": "रिपोर्ट", + "auditLogs": "अडिट लग" + }, + "warnings": { + "drawPermission": "यो खातासँग ड्रअ हेर्ने वा व्यवस्थापन अनुमति छैन। वित्तीय र जोखिम डाटा फिर्ता आएन।", + "walletPermission": "यो खातासँग वालेट मिलान हेर्ने अनुमति छैन। असामान्य ट्रान्सफर संख्या फिर्ता आएन।", + "loadFailed": "लोड असफल भयो। API र लगइन अवस्था जाँच गर्नुहोस्।" + } } diff --git a/src/i18n/locales/ne/draws.json b/src/i18n/locales/ne/draws.json new file mode 100644 index 0000000..2de274b --- /dev/null +++ b/src/i18n/locales/ne/draws.json @@ -0,0 +1,132 @@ +{ + "title": "ड्रअ", + "statusListTitle": "ड्रअ सूची", + "generatePlan": "ड्रअ योजना सिर्जना", + "generating": "सिर्जना हुँदैछ…", + "generateSuccess": "{{created}} ड्रअ सिर्जना भयो, बफर {{upcoming}}/{{target}}", + "generateFailed": "सिर्जना असफल भयो", + "drawNo": "ड्रअ नं.", + "status": "स्थिति", + "startTime": "सुरु समय", + "closeTime": "बन्द समय", + "drawTime": "ड्रअ समय", + "betTotal": "कुल बेट", + "payoutTotal": "कुल भुक्तानी", + "profitLoss": "नाफा/नोक्सानी", + "actions": "कार्य", + "queryDraw": "ड्रअ खोज्नुहोस्", + "reset": "रिसेट", + "fuzzyDrawNo": "फजी ड्रअ नं.", + "viewDetails": "विवरण हेर्नुहोस्", + "invalidDrawId": "अवैध ड्रअ ID", + "loadFailed": "लोड असफल भयो। लगइन र API कन्फिग जाँच गर्नुहोस्।", + "drawDetail": "ड्रअ विवरण", + "businessDate": "व्यवसाय मिति", + "sequenceNo": "क्रम संख्या", + "plannedDraw": "योजनाबद्ध ड्रअ", + "coolingEndTime": "कुलिङ समाप्ति", + "resultSource": "नतिजा स्रोत", + "currentResultVersion": "हालको नतिजा संस्करण", + "settleVersion": "सेटलमेन्ट संस्करण", + "isReopened": "फेरि खोलिएको", + "yes": "हो", + "no": "होइन", + "batchStats": "ब्याच तथ्यांक", + "batchTotal": "कुल ब्याच", + "pendingReview": "समीक्षा बाँकी", + "published": "प्रकाशित", + "viewFinance": "ड्रअ वित्त हेर्नुहोस्", + "drawActions": "ड्रअ कार्य", + "drawActionsDesc": "म्यानुअल बन्द / रद्द / RNG / पुनःखोलाइ / सेटलमेन्ट सबै सीधै ब्याकएन्ड API मा जान्छ।", + "manualClose": "म्यानुअल बन्द", + "cancelDraw": "ड्रअ रद्द", + "cancelBeforeDraw": "ड्रअ अघि रद्द", + "rngDraw": "RNG ड्रअ", + "rngAutoGenerate": "RNG स्वचालित सिर्जना", + "reopen": "पुनःखोल्नुहोस्", + "cooldownReopen": "कुलिङमा पुनःखोल्नुहोस्", + "runSettlement": "सेटलमेन्ट चलाउनुहोस्", + "processing": "प्रक्रियामा…", + "actionSuccess": "{{name}} सफल भयो", + "actionFailed": "{{name}} असफल भयो", + "hallPreviewStatus": "हल पूर्वावलोकन {{status}}", + "financeOverview": "ड्रअ वित्तीय सारांश", + "orderAndItemCount": "अर्डर / टिकट आइटम", + "actualBet": "वास्तविक कटौती बेट", + "currentPayout": "हालको कुल भुक्तानी", + "grossProfit": "अनुमानित कुल नाफा", + "settlementBatchList": "सेटलमेन्ट ब्याच सूची (ड्रअ अनुसार)", + "relatedSettlementBatches": "सम्बन्धित सेटलमेन्ट ब्याच", + "noSettlementBatches": "सेटलमेन्ट ब्याच अभिलेख छैन।", + "ticketCount": "टिकट", + "winCount": "जित", + "finishedAt": "समाप्त समय", + "resultsTitle": "परिणाम", + "reviewAndPublish": "समीक्षा / प्रकाशित", + "viewReviewQueue": "समीक्षा सूची हेर्नुहोस्", + "noPublishedBatch": "प्रकाशित ब्याच छैन।", + "version": "संस्करण v{{version}}", + "sourceType": "स्रोत {{source}}", + "manualEntry": "म्यानुअल", + "rng": "RNG", + "rngSummary": "RNG ह्यास {{hash}}", + "confirmedAt": "पुष्टि समय {{time}}", + "prize": "पुरस्कार", + "tail3": "अन्तिम 3", + "tail2": "अन्तिम 2", + "headTail": "हेड/टेल", + "manualResultEntry": "म्यानुअल परिणाम प्रविष्टि", + "currentStatusAndDraft": "हालको स्थिति {{status}} · सेभ गरेपछि pending batch बन्छ, सिधै प्रकाशित हुँदैन", + "enter23Numbers": "कृपया 23 वटा 4-अङ्क समूह पूरा भर्नुहोस्", + "draftSaved": "ड्राफ्ट v{{version}} सुरक्षित भयो, प्रकाशनको प्रतिक्षामा", + "saveFailed": "सेभ असफल भयो", + "clear": "खाली गर्नुहोस्", + "saveDraft": "ड्राफ्ट सुरक्षित गर्नुहोस्", + "saving": "सेभ हुँदैछ…", + "pendingBatches": "बाँकी ब्याच", + "noPendingBatches": "pending_review ब्याच छैन।", + "batchId": "ब्याच ID", + "numberCount": "नम्बर संख्या", + "reviewAndPublishAction": "जाँचेर प्रकाशित गर्नुहोस्", + "noPublishPermission": "प्रकाशन अनुमति छैन", + "batchNotFound": "ब्याच भेटिएन", + "batchNotFoundDesc": "समीक्षा सूचीमा फर्केर batch ID जाँच गर्नुहोस्।", + "backToReviewQueue": "समीक्षा सूचीमा फर्कनुहोस्", + "publishTitle": "प्रकाशित", + "cannotPublish": "प्रकाशित गर्न मिल्दैन", + "cannotPublishDesc": "हालको ब्याच स्थिति '{{status}}' हो।", + "checkBeforePublish": "प्रकाशन अघि नम्बर जाँच गर्नुहोस्", + "checkBeforePublishDesc": "सही भएपछि मात्र प्रकाशित गर्नुहोस्।", + "publishedView": "प्रकाशित नतिजा हेर्नुहोस्", + "confirmPublish": "प्रकाशन पुष्टि गर्नुहोस्", + "submitting": "पेश हुँदैछ…", + "publishSuccess": "प्रकाशित भयो · {{drawNo}} · स्थिति {{status}}", + "publishFailed": "प्रकाशन असफल भयो", + "sourceTypeFull": "स्रोत: {{source}} · संख्या: {{count}}/23 · RNG ह्यास: {{hash}}", + "subnav": { + "status": "ड्रअ स्थिति", + "results": "परिणाम", + "finance": "ड्रअ वित्त", + "review": "समीक्षा र प्रकाशन" + }, + "statusOptions": { + "all": "सबै", + "pending": "सुरु नभएको", + "open": "बेट खुला", + "closing": "बन्द हुँदै", + "closed": "बन्द", + "drawing": "ड्रअ हुँदै", + "review": "समीक्षा", + "cooldown": "कुलडाउन", + "settling": "सेटल हुँदै", + "settled": "सेटल भयो", + "cancelled": "रद्द" + }, + "resultSlots": { + "first": "पहिलो पुरस्कार", + "second": "दोस्रो पुरस्कार", + "third": "तेस्रो पुरस्कार", + "starter": "विशेष {{index}}", + "consolation": "सान्त्वना {{index}}" + } +} diff --git a/src/i18n/locales/ne/jackpot.json b/src/i18n/locales/ne/jackpot.json new file mode 100644 index 0000000..156bee4 --- /dev/null +++ b/src/i18n/locales/ne/jackpot.json @@ -0,0 +1,46 @@ +{ + "title": "Jackpot", + "configTitle": "Jackpot पूल कन्फिगरेसन", + "loadFailed": "लोड असफल भयो", + "saveSuccess": "सुरक्षित भयो", + "saveFailed": "सुरक्षित गर्न असफल", + "invalidDrawId": "मान्य ड्रअ ID लेख्नुहोस्", + "manualBurstSuccess": "Jackpot म्यानुअल रूपमा ट्रिगर भयो", + "manualBurstFailed": "म्यानुअल बर्स्ट असफल भयो", + "noPoolData": "पूल डाटा छैन", + "displayBalance": "प्रदर्शित ब्यालेन्स {{amount}}", + "currentAmount": "हालको पूल ब्यालेन्स (सानो एकाइ)", + "contributionRate": "योगदान अनुपात 0-1", + "triggerThreshold": "बर्स्ट थ्रेसहोल्ड (सानो एकाइ)", + "payoutRate": "बर्स्ट भुक्तानी अनुपात 0-1", + "forceTriggerGap": "बलपूर्वक बर्स्ट अन्तर (सेटल ड्रअ)", + "minBetAmount": "न्यूनतम बेट रकम (सानो एकाइ)", + "comboTriggerPlays": "कम्बो ट्रिगर प्ले (comma-separated)", + "status": "स्थिति", + "disabled": "बन्द", + "enabled": "खुला", + "saving": "सुरक्षित हुँदैछ…", + "save": "सुरक्षित गर्नुहोस्", + "manualBurstDrawId": "म्यानुअल बर्स्ट ड्रअ ID", + "manualBurstAmount": "बर्स्ट रकम (खाली भए सबै)", + "processing": "प्रक्रियामा…", + "manualBurst": "म्यानुअल बर्स्ट", + "filter": "फिल्टर", + "drawNo": "ड्रअ नं.", + "optional": "वैकल्पिक", + "apply": "लागू गर्नुहोस्", + "payoutRecords": "Jackpot भुक्तानी रेकर्ड", + "contributionRecords": "Jackpot योगदान रेकर्ड", + "subnavLabel": "Jackpot उपनेभिगेसन", + "subnavPools": "पूल कन्फिगरेसन", + "subnavRecords": "रेकर्ड", + "payoutLoadFailed": "भुक्तानी रेकर्ड लोड असफल भयो", + "contributionLoadFailed": "योगदान रेकर्ड लोड असफल भयो", + "trigger": "ट्रिगर", + "payoutAmount": "भुक्तानी रकम", + "winnerCount": "विजेता संख्या", + "time": "समय", + "ticketNo": "टिकट", + "player": "खेलाडी", + "contributionAmount": "योगदान रकम" +} diff --git a/src/i18n/locales/ne/players.json b/src/i18n/locales/ne/players.json new file mode 100644 index 0000000..2c795eb --- /dev/null +++ b/src/i18n/locales/ne/players.json @@ -0,0 +1,49 @@ +{ + "title": "खेलाडी", + "listTitle": "खेलाडी सूची", + "createPlayer": "खेलाडी सिर्जना", + "searchPlaceholder": "खेलाडी ID / प्रयोगकर्ता नाम / उपनामबाट खोज्नुहोस्", + "search": "खोज", + "refresh": "रिफ्रेस", + "loadFailed": "खेलाडी सूची लोड असफल भयो", + "siteCodeRequired": "साइट कोड लेख्नुहोस्", + "sitePlayerIdRequired": "साइट खेलाडी ID लेख्नुहोस्", + "createFailed": "खेलाडी सिर्जना असफल भयो", + "createSuccess": "खेलाडी {{name}} सिर्जना भयो", + "noChanges": "कुनै परिवर्तन छैन", + "updateFailed": "खेलाडी अपडेट असफल भयो", + "updateSuccess": "{{name}} अपडेट भयो", + "deleteFailed": "मेटाउन असफल", + "deleteSuccess": "खेलाडी {{name}} मेटाइयो", + "statusNormal": "सामान्य", + "statusFrozen": "फ्रिज", + "statusBanned": "प्रतिबन्धित", + "site": "साइट", + "sitePlayerId": "साइट खेलाडी ID", + "username": "प्रयोगकर्ता नाम", + "nickname": "उपनाम", + "currency": "मुद्रा", + "balance": "ब्यालेन्स", + "available": "उपलब्ध", + "status": "स्थिति", + "lastLogin": "अन्तिम लगइन", + "actions": "कार्य", + "edit": "सम्पादन", + "delete": "मेटाउनुहोस्", + "createDialogTitle": "खेलाडी सिर्जना", + "editDialogTitle": "खेलाडी सम्पादन", + "createDialogDesc": "मुख्य साइटको खेलाडीलाई लटरी प्लेटफर्ममा म्यानुअल दर्ता गर्नुहोस्। प्रायः SSO लगइनबाट स्वतः सिर्जना हुन्छ।", + "editDialogDesc": "खेलाडी जानकारी सम्पादन गर्नुहोस्।", + "siteCode": "साइट कोड", + "siteCodePlaceholder": "जस्तै main_site", + "sitePlayerIdLabel": "साइट खेलाडी ID", + "sitePlayerIdPlaceholder": "मुख्य साइटले फिर्ता गरेको अद्वितीय चिन्ह", + "usernamePlaceholderOptional": "वैकल्पिक", + "nicknamePlaceholderOptional": "वैकल्पिक", + "defaultCurrency": "पूर्वनिर्धारित मुद्रा", + "cancel": "रद्द गर्नुहोस्", + "save": "सुरक्षित गर्नुहोस्", + "saving": "सुरक्षित हुँदैछ…", + "confirmDelete": "मेटाउने पुष्टि", + "confirmDeleteDesc": "खेलाडी {{name}} मेटाउने? यो कार्य फिर्ता गर्न मिल्दैन।" +} diff --git a/src/i18n/locales/ne/reconcile.json b/src/i18n/locales/ne/reconcile.json new file mode 100644 index 0000000..a2aad01 --- /dev/null +++ b/src/i18n/locales/ne/reconcile.json @@ -0,0 +1,45 @@ +{ + "title": "मिलान", + "createTitle": "म्यानुअल मिलान कार्य", + "createDesc": "असामान्य फ्लोहरू scheduled task ले स्वतः जाँच्छ। यहाँ वित्तले म्यानुअल रूपमा मिलान कार्य सुरु गर्न सक्छ: प्रकार र समय सीमा छान्नुहोस्, अनि आवश्यक परे player ID, transfer no, वा idempotency key जस्ता सन्दर्भहरू प्रति लाइन लेख्नुहोस्।", + "reconcileType": "मिलान प्रकार", + "walletTransfer": "वालेट ट्रान्सफर (मुख्य साइट ⇄ लटरी)", + "startTime": "सुरु समय", + "endTime": "अन्त्य समय", + "scope": "दायरा (वैकल्पिक)", + "scopePlaceholder": "प्रति लाइन एउटा सन्दर्भ, जस्तै player ID, wallet transfer no, वा idempotency key.\nखाली छोडेमा केवल कार्य रेकर्ड सिर्जना हुन्छ।", + "scopeHint": "pending_reconcile स्थितिको वालेट कारोबारसँग मिलान गर्दा transfer no वा idempotency key माथि टाँस्नुहोस्।", + "advancedToggleOpen": "उन्नत विकल्प देखाउनुहोस् (custom items JSON)", + "advancedToggleClose": "उन्नत विकल्प लुकाउनुहोस् (custom items JSON)", + "advancedJson": "Items JSON (माथिको दायराबाट बनेका row हरूलाई override गर्छ)", + "createTask": "मिलान कार्य सिर्जना", + "submitting": "पेश हुँदैछ…", + "loadFailed": "लोड असफल भयो", + "loadItemsFailed": "विवरण लोड असफल भयो", + "periodRequired": "सुरु र अन्त्य समय दुवै लेख्नुहोस्", + "periodInvalid": "अवैध समय दायरा", + "periodOrderInvalid": "अन्त्य समय सुरु समयभन्दा पछाडि वा बराबर हुनुपर्छ", + "advancedJsonInvalid": "उन्नत JSON parse गर्न सकिएन", + "createSuccess": "मिलान कार्य सिर्जना भयो", + "createFailed": "कार्य सिर्जना असफल भयो", + "noCreatePermission": "हालको खातासँग मिलान कार्य सिर्जना गर्ने अनुमति छैन।", + "jobsTitle": "मिलान कार्यहरू", + "jobsDesc": "विवरण हेर्न row क्लिक गर्नुहोस्।", + "refresh": "रिफ्रेस", + "jobNo": "कार्य नं.", + "type": "प्रकार", + "status": "स्थिति", + "period": "अवधि", + "createdAt": "सिर्जना समय", + "detailsTitle": "कार्य विवरण", + "sideARef": "लटरी साइड सन्दर्भ", + "sideBRef": "मुख्य साइट सन्दर्भ", + "differenceAmount": "अन्तर (cent)", + "noDetails": "विवरण छैन", + "statusCompleted": "सम्पन्न", + "statusRunning": "चलिरहेको", + "statusFailed": "असफल", + "itemMismatch": "मेल खाएन", + "itemMatched": "मेल खायो", + "itemPendingCheck": "जाँच बाँकी" +} diff --git a/src/i18n/locales/ne/reports.json b/src/i18n/locales/ne/reports.json index 603dcec..f6d73f4 100644 --- a/src/i18n/locales/ne/reports.json +++ b/src/i18n/locales/ne/reports.json @@ -1,3 +1,34 @@ { - "title": "रिपोर्ट" + "title": "रिपोर्ट", + "createExport": "निर्यात सिर्जना", + "reportType": "रिपोर्ट प्रकार", + "exportFormat": "निर्यात ढाँचा", + "filterJson": "filter_json (वैकल्पिक)", + "parseFilterFailed": "फिल्टर JSON पार्स गर्न सकिएन", + "createSuccess": "निर्यात कार्य सिर्जना भयो", + "createFailed": "कार्य सिर्जना असफल भयो", + "downloadFailed": "डाउनलोड असफल भयो", + "taskList": "कार्य सूची", + "jobId": "कार्य नं.", + "type": "प्रकार", + "format": "ढाँचा", + "status": "स्थिति", + "output": "आउटपुट", + "download": "डाउनलोड", + "createdAt": "सिर्जना समय", + "id": "ID", + "empty": "डाटा छैन", + "reportTypes": { + "draw_profit_summary": "ड्रअ नाफा सारांश", + "daily_profit_summary": "दैनिक नाफा सारांश", + "player_win_loss": "खेलाडी जित/हार रिपोर्ट", + "wallet_transfer_report": "वालेट ट्रान्सफर रिपोर्ट", + "hot_number_risk_report": "हट नम्बर जोखिम रिपोर्ट", + "play_dimension_report": "प्ले डाइमेन्सन रिपोर्ट", + "sold_out_number_report": "बिक्री समाप्त नम्बर रिपोर्ट", + "rebate_commission_report": "रिबेट र कमिसन रिपोर्ट", + "audit_operation_report": "अडिट अपरेशन रिपोर्ट", + "wallet_txns_daily": "वालेट कारोबार दैनिक", + "transfer_orders_daily": "ट्रान्सफर अर्डर दैनिक" + } } diff --git a/src/i18n/locales/ne/risk.json b/src/i18n/locales/ne/risk.json new file mode 100644 index 0000000..5908006 --- /dev/null +++ b/src/i18n/locales/ne/risk.json @@ -0,0 +1,91 @@ +{ + "title": "जोखिम", + "center": "जोखिम केन्द्र", + "drawNo": "ड्रअ नं.", + "status": "स्थिति", + "closeTime": "बन्द समय", + "actions": "कार्य", + "all": "सबै", + "search": "खोज", + "refresh": "रिफ्रेस", + "fuzzyDrawNo": "फजी ड्रअ नं.", + "loadDrawListFailed": "ड्रअ सूची लोड असफल भयो", + "enterRisk": "जोखिममा जानुहोस्", + "poolsTitle": "जोखिम पूल", + "searchNumber": "नम्बर खोज्नुहोस्", + "searchNumberPlaceholder": "जस्तै 8888", + "riskFilter": "जोखिम फिल्टर", + "sort": "क्रमबद्ध", + "filterAll": "सबै", + "filterSoldOut": "बिक्री समाप्त", + "filterHighRisk": ">80%", + "sortUsageDesc": "प्रयोग अनुपात ↓", + "sortLockedDesc": "लक रकम ↓", + "sortRemainingAsc": "बाँकी ↑", + "sortNumberAsc": "नम्बर ↑", + "loadPoolsFailed": "जोखिम पूल लोड असफल भयो", + "capAmount": "क्याप", + "lockedAmount": "लक गरिएको", + "remainingAmount": "बाँकी", + "usageRatio": "प्रयोग अनुपात", + "poolStatus": "स्थिति", + "soldOut": "बिक्री समाप्त", + "warning": "चेतावनी", + "normal": "सामान्य", + "recover": "पुनर्स्थापना", + "close": "बन्द", + "view": "हेर्नुहोस्", + "manualCloseSuccess": "नम्बर बेटिङ म्यानुअल रूपमा बन्द गरियो", + "recoverSuccess": "नम्बर बेटिङ पुनर्स्थापित गरियो", + "actionFailed": "कार्य असफल भयो", + "detailTitle": "जोखिम पूल विवरण", + "loadDetailFailed": "जोखिम पूल विवरण लोड असफल भयो", + "backToList": "सूचीमा फर्कनुहोस्", + "backToAllPools": "सबै जोखिम पूलमा फर्कनुहोस्", + "numberTitle": "नम्बर {{number}}", + "drawMeta": "ड्रअ {{drawNo}}", + "totalCap": "क्याप रकम", + "lockedWorstCase": "लक गरिएको (अधिकतम भुक्तानी सुरक्षित)", + "remainingSellable": "बाँकी बिक्रीयोग्य", + "isSoldOut": "बिक्री समाप्त", + "yes": "हो", + "no": "होइन", + "occupationLogs": "यो नम्बरको लक / रिलिज लग", + "time": "समय", + "action": "कार्य", + "amount": "रकम", + "source": "स्रोत", + "ticketNo": "टिकट नं.", + "playCode": "प्ले", + "loadLogsFailed": "लक लग लोड असफल भयो", + "lockLogsTitle": "जोखिम लक लग", + "drawInfoLoadFailed": "ड्रअ जानकारी लोड असफल भयो", + "loadingDraw": "ड्रअ लोड हुँदैछ…", + "headerTitle": "जोखिम · ड्रअ {{drawNo}}", + "databaseStatus": "डेटाबेस स्थिति", + "hallPreviewStatus": "(हल पूर्वावलोकन: {{status}})", + "subnavOccupancy": "अकुपेन्सी", + "subnavHot": "हट नम्बर", + "subnavSoldOut": "बिक्री समाप्त सूची", + "subnavPools": "सबै जोखिम पूल", + "changeDraw": "ड्रअ परिवर्तन गर्नुहोस्", + "number4d": "नम्बर (4 अङ्क)", + "optional": "वैकल्पिक", + "actionFilter": "कार्य", + "noLimit": "सीमा छैन", + "lock": "लक", + "release": "रिलिज", + "applyFilter": "फिल्टर लागू गर्नुहोस्", + "statusOptions": { + "pending": "सुरु नभएको", + "open": "खुला", + "closing": "बन्द हुँदै", + "closed": "बन्द", + "drawing": "ड्रअ हुँदै", + "review": "समीक्षा", + "cooldown": "कुलडाउन", + "settling": "सेटल हुँदै", + "settled": "सेटल भयो", + "cancelled": "रद्द" + } +} diff --git a/src/i18n/locales/ne/settlement.json b/src/i18n/locales/ne/settlement.json new file mode 100644 index 0000000..6df0923 --- /dev/null +++ b/src/i18n/locales/ne/settlement.json @@ -0,0 +1,54 @@ +{ + "title": "सेटलमेन्ट", + "filter": "फिल्टर", + "drawNo": "ड्रअ नं.", + "status": "स्थिति", + "apply": "लागू गर्नुहोस्", + "batchList": "सेटलमेन्ट ब्याच", + "loadFailed": "लोड असफल भयो", + "exportFailed": "निर्यात असफल भयो", + "actionSuccess": "{{name}} सफल भयो", + "actionFailed": "{{name}} असफल भयो", + "placeholderDrawNo": "जस्तै 20260511-001", + "reviewStatus": "समीक्षा स्थिति", + "ticketCount": "टिकट संख्या", + "winCount": "जित संख्या", + "payoutTotal": "कुल भुक्तानी", + "jackpot": "Jackpot", + "finishedAt": "समाप्त समय", + "details": "विवरण", + "approve": "स्वीकृत", + "pass": "पास", + "reject": "अस्वीकृत", + "payout": "भुक्तानी", + "export": "निर्यात", + "backToList": "ब्याच सूचीमा फर्कनुहोस्", + "errorTitle": "त्रुटि", + "retry": "पुन: प्रयास", + "batchSummary": "ब्याच #{{id}}", + "summaryMeta": "ड्रअ {{drawNo}} · ड्रअ स्थिति {{drawStatus}} · परिणाम ब्याच v{{version}}", + "settlementStatus": "सेटलमेन्ट स्थिति", + "reviewState": "समीक्षा स्थिति", + "ticketTotal": "टिकट संख्या", + "winTotal": "जित संख्या", + "payoutAmount": "कुल भुक्तानी", + "jackpotPayout": "Jackpot भुक्तानी", + "startedAt": "सुरु", + "endedAt": "समाप्त", + "runPayout": "भुक्तानी चलाउनुहोस्", + "exportSettlementReport": "सेटलमेन्ट रिपोर्ट निर्यात", + "loadingSummary": "सारांश लोड हुँदैछ…", + "detailTitle": "सेटलमेन्ट विवरण", + "ticketNo": "टिकट नं.", + "playCode": "प्ले", + "player": "खेलाडी", + "matchedTier": "मिलेको स्तर", + "regularPayout": "सामान्य भुक्तानी", + "loadingDetails": "विवरण लोड हुँदैछ…", + "statusOptions": { + "all": "सबै", + "running": "चलिरहेको", + "completed": "सम्पन्न", + "failed": "असफल" + } +} diff --git a/src/i18n/locales/ne/tickets.json b/src/i18n/locales/ne/tickets.json new file mode 100644 index 0000000..10a0e86 --- /dev/null +++ b/src/i18n/locales/ne/tickets.json @@ -0,0 +1,19 @@ +{ + "title": "टिकट", + "playerTicketQuery": "खेलाडी टिकट खोज", + "playerId": "खेलाडी ID", + "invalidPlayerId": "मान्य खेलाडी ID लेख्नुहोस्", + "drawNoOptional": "ड्रअ नं. (वैकल्पिक)", + "drawNoPlaceholder": "जस्तै 20260520-001", + "query": "खोज", + "loadFailed": "लोड असफल भयो", + "ticketNo": "टिकट नं.", + "orderNo": "अर्डर नं.", + "drawNo": "ड्रअ नं.", + "playCode": "प्ले", + "number": "नम्बर", + "actualDeduct": "कटौती", + "status": "स्थिति", + "failReason": "असफल कारण", + "winAmount": "जित रकम" +} diff --git a/src/i18n/locales/ne/wallet.json b/src/i18n/locales/ne/wallet.json new file mode 100644 index 0000000..2c0ac75 --- /dev/null +++ b/src/i18n/locales/ne/wallet.json @@ -0,0 +1,69 @@ +{ + "title": "वालेट", + "subnavLabel": "वालेट उपपृष्ठहरू", + "subnavTransactions": "वालेट कारोबार", + "subnavTransferOrders": "ट्रान्सफर अर्डर", + "noPermission": "हालको खातासँग यो पृष्ठमा पहुँच अनुमति छैन", + "copySuccess": "{{label}} क्लिपबोर्डमा प्रतिलिपि भयो", + "copyFailed": "प्रतिलिपि असफल भयो। ब्राउजर अनुमति जाँच गर्नुहोस् वा म्यानुअल रूपमा कपी गर्नुहोस्।", + "statusProcessing": "प्रक्रियामा", + "statusSuccess": "सफल", + "statusFailed": "असफल", + "statusPendingReconcile": "मिलान बाँकी", + "statusReversed": "रिभर्स भयो", + "statusManuallyProcessed": "म्यानुअल रूपमा प्रक्रिया गरियो", + "statusPosted": "पोस्ट गरियो", + "filterAll": "सबै", + "transferIn": "मुख्य साइटबाट भित्र", + "transferOut": "मुख्य साइटतर्फ बाहिर", + "transferOutRefund": "ट्रान्सफर-आउट फिर्ता", + "transferOrders": "ट्रान्सफर अर्डर", + "walletTransactions": "वालेट कारोबार", + "playerWalletQuery": "खेलाडी वालेट खोज", + "localTransferNo": "स्थानीय ट्रान्सफर नं.", + "externalRefNo": "मुख्य साइट सन्दर्भ नं.", + "playerAccount": "खेलाडी खाता", + "playerAccountPlaceholder": "मुख्य साइट खेलाडी ID वा प्रयोगकर्ता नाम (fuzzy)", + "playerId": "खेलाडी ID", + "playerIdOptional": "वैकल्पिक, खाताभन्दा प्राथमिक", + "requestDateRange": "अनुरोध मिति दायरा", + "status": "स्थिति", + "options": "विकल्प", + "abnormalOnly": "असामान्य मात्र", + "abnormalOnlyPending": "असामान्य मात्र (मिलान बाँकी)", + "search": "खोज", + "resetFilters": "फिल्टर रिसेट", + "refreshCurrentPage": "हालको पृष्ठ रिफ्रेस", + "loadFailed": "लोड असफल भयो", + "direction": "दिशा", + "amount": "रकम", + "failReason": "असफल कारण", + "requestTime": "अनुरोध समय", + "finishedTime": "समाप्त समय", + "actions": "कार्य", + "reverse": "रिभर्स", + "manualProcess": "म्यानुअल प्रक्रिया", + "processing": "प्रक्रियामा…", + "reverseSuccess": "रिभर्स सफल भयो", + "manualProcessSuccess": "म्यानुअल प्रक्रिया सफल भयो", + "actionFailed": "कार्य असफल भयो", + "txnNo": "कारोबार नं.", + "bizType": "व्यवसाय प्रकार", + "type": "प्रकार", + "queryFailed": "खोज असफल भयो", + "invalidPlayerId": "मान्य खेलाडी ID लेख्नुहोस्", + "querying": "खोजिँदैछ…", + "query": "खोज", + "sitePlayer": "साइट खेलाडी", + "walletType": "प्रकार", + "currency": "मुद्रा", + "balanceMinor": "ब्यालेन्स (सानो एकाइ)", + "availableBalance": "उपलब्ध (अनुमानित)", + "noWalletRows": "वालेट रेकर्ड छैन। बेट वा ट्रान्सफर नगरेका खेलाडीमा रेकर्ड नहुन सक्छ।", + "copyTransferNo": "स्थानीय ट्रान्सफर नं.", + "copyExternalRefNo": "मुख्य साइट सन्दर्भ नं.", + "copyTxnNo": "कारोबार नं.", + "copyExternalTxnRefNo": "मुख्य साइट सन्दर्भ नं.", + "in": "भित्र", + "out": "बाहिर" +} diff --git a/src/i18n/locales/zh/adminUsers.json b/src/i18n/locales/zh/adminUsers.json new file mode 100644 index 0000000..460fc43 --- /dev/null +++ b/src/i18n/locales/zh/adminUsers.json @@ -0,0 +1,83 @@ +{ + "title": "管理员", + "listTitle": "管理员用户列表", + "createAdmin": "新建管理员", + "searchPlaceholder": "按用户名 / 昵称 / 邮箱搜索", + "loadFailed": "加载管理员列表失败", + "nicknameRequired": "请填写昵称", + "newPasswordMin": "新密码至少 8 位", + "roleRequired": "请至少选择一个角色", + "usernameRequired": "请填写登录账号", + "passwordMin": "密码至少 8 位", + "createSuccess": "已创建管理员 {{name}}", + "updateSuccess": "已更新 {{name}}", + "saveAccountFailed": "保存账号失败", + "deleteSuccess": "已删除 {{name}}", + "deleteFailed": "删除失败", + "allPermissions": "全部权限", + "saveRoleSuccess": "已更新 {{name}} 的角色", + "saveRoleFailed": "保存角色失败", + "savePermissionSuccess": "已更新 {{name}} 的权限", + "savePermissionFailed": "保存权限失败", + "saving": "保存中…", + "deleting": "删除中…", + "common": { + "none": "无" + }, + "table": { + "account": "账号", + "nickname": "昵称", + "status": "状态", + "roles": "角色", + "direct": "直接权限", + "effective": "生效权限", + "actions": "操作" + }, + "status": { + "enabled": "启用", + "disabled": "禁用" + }, + "actions": { + "permissions": "权限", + "edit": "编辑", + "delete": "删除", + "cancel": "取消", + "save": "保存" + }, + "permissionDialog": { + "title": "管理员权限", + "rolesTitle": "角色", + "rolesDescription": "保存至默认站点,与「直接权限」叠加为有效权限。", + "rolePermissionCount": "含 {{count}} 项功能权限", + "directTitle": "直接权限", + "directDescription": "按菜单或业务域展开,勾选具体的 prd.*;多数情况只调角色即可。", + "selectedRoles": "当前勾选的角色:", + "saveRoles": "保存角色", + "saveDirect": "保存直接权限" + }, + "accountDialog": { + "createTitle": "新建管理员", + "editTitle": "编辑账号", + "createDescription": "须为账号指定至少一个默认站点角色。登录账号仅可使用字母、数字、点、下划线与连字符,保存后为小写。", + "editDescription": "登录账号不可修改。留空密码表示不修改。", + "username": "登录账号", + "usernamePlaceholder": "例如 ops_admin", + "nickname": "昵称", + "nicknamePlaceholder": "显示名称", + "emailOptional": "邮箱(可选)", + "emailPlaceholder": "留空则不填", + "password": "密码", + "passwordOptional": "密码(可选)", + "passwordPlaceholderCreate": "至少 8 位", + "passwordPlaceholderEdit": "不修改请留空", + "rolesRequired": "角色(默认站点,至少一项)", + "rolesDescription": "创建后即可在「权限」中继续调整角色或直接授权。", + "noRoles": "暂无角色数据,请等待列表加载完成后重试。" + }, + "delete": { + "currentUserBlocked": "不能删除当前登录账号", + "rowActionTitle": "删除该管理员", + "confirmTitle": "确认删除", + "confirmDescription": "确定删除管理员 {{name}}?此操作不可撤销。" + } +} diff --git a/src/i18n/locales/zh/audit.json b/src/i18n/locales/zh/audit.json index a747239..8da6524 100644 --- a/src/i18n/locales/zh/audit.json +++ b/src/i18n/locales/zh/audit.json @@ -1,3 +1,14 @@ { - "title": "审计日志" + "title": "审计日志", + "moduleCode": "模块编码", + "actionCode": "动作编码", + "operatorType": "操作者类型", + "exactMatch": "精确匹配", + "operatorTypePlaceholder": "如 admin / system", + "operator": "操作者", + "module": "模块", + "action": "动作", + "target": "目标", + "time": "时间", + "empty": "无数据" } diff --git a/src/i18n/locales/zh/auth.json b/src/i18n/locales/zh/auth.json index 7100ea4..76fd417 100644 --- a/src/i18n/locales/zh/auth.json +++ b/src/i18n/locales/zh/auth.json @@ -1,3 +1,24 @@ { - "title": "登录" + "title": "登录", + "loginTitle": "后台登录", + "account": "账号", + "accountPlaceholder": "登录账号", + "password": "密码", + "passwordPlaceholder": "密码", + "captcha": "验证码", + "captchaPlaceholder": "图中字符", + "captchaLoading": "加载验证码中", + "captchaRefresh": "点击刷新验证码", + "captchaFetch": "点击获取", + "apiMissingTitle": "未配置 API 地址", + "apiMissingDescriptionPrefix": "请在环境中设置", + "apiMissingDescriptionSuffix": "(Laravel 根 URL,如 http://127.0.0.1:8000)。", + "submit": "登录", + "submitting": "登录中…", + "captchaLoadFailed": "无法获取验证码,请检查接口或网络", + "apiBaseMissingToast": "未配置 NEXT_PUBLIC_LOTTERY_API_BASE_URL", + "captchaRequired": "请先刷新验证码", + "welcome": "欢迎,{{name}}", + "networkFailed": "网络请求失败", + "loginFailed": "登录失败" } diff --git a/src/i18n/locales/zh/common.json b/src/i18n/locales/zh/common.json index c14be4d..f7c049f 100644 --- a/src/i18n/locales/zh/common.json +++ b/src/i18n/locales/zh/common.json @@ -4,7 +4,7 @@ "ne": "नेपाली", "zh": "中文", "title": "界面语言", - "changed": "语言" + "changed": "语言已切换为 {{language}}" }, "app": { "title": "彩票后台" @@ -15,6 +15,65 @@ "search": "搜索", "apply": "应用", "loading": "加载中...", - "submitting": "提交中..." + "submitting": "提交中...", + "logout": "退出登录", + "close": "关闭", + "viewAll": "查看全部", + "viewDetails": "查看详情", + "reviewNow": "立即审核", + "create": "创建", + "createTask": "创建任务", + "clear": "清除", + "done": "完成" + }, + "date": { + "placeholder": "选择日期", + "rangePlaceholder": "选择日期范围", + "rangeHint": "先选开始日,再选结束日(单日可对同一天点两次);点「完成」关闭面板。" + }, + "pagination": { + "perPage": "每页条数", + "selectPlaceholder": "请选择", + "summary": "共 {{total}} 条;第 {{page}} / {{lastPage}} 页", + "previous": "上一页", + "next": "下一页" + }, + "states": { + "noData": "暂无数据", + "loading": "加载中…", + "comingSoon": "功能开发中" + }, + "errors": { + "loadFailed": "加载失败" + }, + "toolbar": { + "defaultAdmin": "管理员", + "notifications": "通知", + "notificationsComingSoon": "通知功能开发中", + "accountSettings": "账号设置", + "loggedOut": "已退出登录" + }, + "nav": { + "home": "首页", + "dashboard": "仪表盘", + "admin_users": "管理列表", + "players": "玩家列表", + "wallet": "钱包流水", + "draws": "期号列表", + "config": "运营配置", + "risk": "风控", + "settlement": "结算", + "jackpot": "Jackpot", + "reconcile": "对账", + "tickets": "玩家注单", + "reports": "报表导出", + "audit": "审计日志", + "settings": "系统设置" + }, + "sidebar": { + "workspace": "工作台" + }, + "auth": { + "checking": "正在校验登录状态…" } } diff --git a/src/i18n/locales/zh/config.json b/src/i18n/locales/zh/config.json new file mode 100644 index 0000000..1ab32d4 --- /dev/null +++ b/src/i18n/locales/zh/config.json @@ -0,0 +1,83 @@ +{ + "title": "配置中心", + "nav": { + "aria": "运营配置子导航", + "sidebarTitle": "运营配置导航", + "groups": { + "betting": "投注与展示", + "risk_wallet": "风控与资金" + }, + "items": { + "plays": "玩法与限额", + "odds": "赔率", + "rebate": "佣金 / 回水", + "risk-cap": "赔付封顶", + "wallet": "钱包阈值" + } + }, + "versionStatus": { + "active": "生效中", + "draft": "草稿", + "archived": "已归档" + }, + "versionSwitcher": { + "sheetTitle": "切换配置版本", + "sheetDescription": "选择一条版本在本页查看;草稿可编辑,生效中与已归档为只读。", + "loading": "加载中…", + "noneSelected": "未选择版本", + "switch": "切换版本", + "empty": "暂无版本记录。", + "count": "{{count}} 条", + "effectiveAt": "生效时间:{{value}}", + "note": "备注:{{value}}", + "current": "当前查看", + "selected": "已选中", + "view": "查看", + "rollback": "回滚", + "delete": "删除", + "deleteConfirmTitle": "确认删除版本?", + "deleteConfirmDescription": "将永久删除版本 ID {{id}}(version_no {{version}})。生效中的版本不可删除。" + }, + "versionActions": { + "publishCurrent": "启用为当前版本", + "refreshing": "刷新中", + "refresh": "刷新版本", + "newDraft": "新建草稿", + "saveDraft": "保存草稿" + }, + "wallet": { + "title": "钱包转账限额配置", + "description": "金额单位为游戏币种最小单位(如 NPR 下 100 = 1.00 NPR)。最小金额至少为 1 最小单位。", + "loadFailed": "加载失败", + "saveSuccess": "保存成功", + "saveFailed": "保存失败", + "fields": { + "inMin": "转入最小金额", + "inMax": "转入最大金额", + "outMin": "转出最小金额", + "outMax": "转出最大金额" + }, + "placeholders": { + "min": "例如 1.00", + "max": "例如 10000.00" + }, + "hints": { + "inMin": "主站钱包转入彩票钱包的单笔下限", + "inMax": "主站钱包转入彩票钱包的单笔上限", + "outMin": "彩票钱包转出主站钱包的单笔下限", + "outMax": "彩票钱包转出主站钱包的单笔上限" + }, + "discard": "放弃更改" + }, + "play": { + "batchGroups": { + "d2": "2D 全局", + "d3": "3D 全局", + "d4": "4D 全局", + "big-small": "Big / Small", + "position": "位置类玩法", + "box": "包号类玩法", + "jackpot": "Jackpot" + } + } +} diff --git a/src/i18n/locales/zh/dashboard.json b/src/i18n/locales/zh/dashboard.json index d0fd75e..82bcc8a 100644 --- a/src/i18n/locales/zh/dashboard.json +++ b/src/i18n/locales/zh/dashboard.json @@ -1,3 +1,57 @@ { - "title": "仪表盘" + "title": "仪表盘", + "refresh": "刷新", + "notice": "提示", + "todayBetTotal": "当期投注总额", + "currentDrawFinanceSummary": "当前大厅期财务汇总", + "currentPayout": "当期派彩", + "payoutSummary": "中奖派彩 + Jackpot", + "currentProfit": "当期平台盈亏", + "profitFormula": "投注 − 派彩(近似)", + "currentDraw": "当前期号", + "drawSequence": "第 {{sequence}} 期", + "drawDetails": "期号详情", + "ticketCount": "本期注单笔数", + "relatedBetAmount": "关联投注额", + "riskCapUsage": "风险封顶占用", + "lockedAndCap": "已占用 {{locked}} / 封顶 {{cap}}", + "occupancyDetails": "占用明细", + "hotNumbersTop10": "热门号码 Top 10", + "playDimension": "玩法维度", + "soldOutDistribution": "售罄分布", + "soldOutTotal": "售罄合计", + "pendingReviewResults": "待审核开奖", + "abnormalTransferOrders": "异常转账单", + "viewTransferOrders": "查看转账单", + "noSoldOutNumbers": "暂无售罄号码", + "noPoolData": "该维度暂无池数据", + "numbersByUsage": "号码(按占用率)", + "capUsage": "封顶占用", + "tabs": { + "4d": "4D", + "3d": "3D", + "2d": "2D", + "special": "特别" + }, + "soldOutBuckets": { + "d4": "4D", + "d3": "3D", + "d2": "2D", + "special": "特别号", + "other": "其他" + }, + "quickLinks": { + "createDrawPlan": "创建期计划", + "drawSchedule": "开售 / 期号", + "results": "开奖结果", + "tickets": "注单管理", + "walletTransactions": "钱包流水", + "reports": "报表中心", + "auditLogs": "审计日志" + }, + "warnings": { + "drawPermission": "当前账号无开奖查看/管理权限,财务与风控数据未返回。", + "walletPermission": "当前账号无钱包对账查看权限,异常转账计数未返回。", + "loadFailed": "加载失败,请检查 API 与登录状态。" + } } diff --git a/src/i18n/locales/zh/draws.json b/src/i18n/locales/zh/draws.json new file mode 100644 index 0000000..9f871c7 --- /dev/null +++ b/src/i18n/locales/zh/draws.json @@ -0,0 +1,132 @@ +{ + "title": "期号", + "statusListTitle": "期号列表", + "generatePlan": "批量生成期开奖计划", + "generating": "生成中…", + "generateSuccess": "已生成 {{created}} 期,当前缓冲 {{upcoming}}/{{target}}", + "generateFailed": "生成失败", + "drawNo": "期号", + "status": "状态", + "startTime": "开始时间", + "closeTime": "封盘时间", + "drawTime": "开奖时间", + "betTotal": "下注总额", + "payoutTotal": "派彩总额", + "profitLoss": "盈亏", + "actions": "操作", + "queryDraw": "查询期号", + "reset": "重置", + "fuzzyDrawNo": "模糊匹配期号", + "viewDetails": "查看详情", + "invalidDrawId": "无效的期号 ID", + "loadFailed": "加载失败,请检查登录与 API 配置", + "drawDetail": "开奖详情", + "businessDate": "业务日", + "sequenceNo": "流水序号", + "plannedDraw": "计划开奖", + "coolingEndTime": "冷静期结束", + "resultSource": "结果来源", + "currentResultVersion": "当前结果版本", + "settleVersion": "结算版本", + "isReopened": "是否重开", + "yes": "是", + "no": "否", + "batchStats": "批次统计", + "batchTotal": "总批次", + "pendingReview": "待审核", + "published": "已发布", + "viewFinance": "查看期号收支", + "drawActions": "期号操作", + "drawActionsDesc": "手动封盘 / 取消 / RNG / 重开 / 触发结算均直接调用后台接口。", + "manualClose": "手动封盘", + "cancelDraw": "取消期号", + "cancelBeforeDraw": "未开奖前取消", + "rngDraw": "RNG开奖", + "rngAutoGenerate": "RNG 自动生成", + "reopen": "重开", + "cooldownReopen": "冷静期重开", + "runSettlement": "触发结算", + "processing": "处理中…", + "actionSuccess": "{{name}}成功", + "actionFailed": "{{name}}失败", + "hallPreviewStatus": "大厅预览 {{status}}", + "financeOverview": "期号收支概览", + "orderAndItemCount": "订单数 / 注项数", + "actualBet": "当期实扣投注", + "currentPayout": "当期派彩合计", + "grossProfit": "近似毛损益", + "settlementBatchList": "结算批次列表(按期号筛选)", + "relatedSettlementBatches": "本关联期结算批次", + "noSettlementBatches": "暂无结算批次记录。", + "ticketCount": "票数", + "winCount": "中奖数", + "finishedAt": "完成时间", + "resultsTitle": "开奖结果", + "reviewAndPublish": "去审核 / 发布", + "viewReviewQueue": "查看审核队列", + "noPublishedBatch": "暂无已发布批次。", + "version": "版本 v{{version}}", + "sourceType": "生成方式 {{source}}", + "manualEntry": "人工录入", + "rng": "RNG", + "rngSummary": "RNG 摘要 {{hash}}", + "confirmedAt": "确认时间 {{time}}", + "prize": "奖项", + "tail3": "尾3", + "tail2": "尾2", + "headTail": "头/尾", + "manualResultEntry": "人工录入开奖结果", + "currentStatusAndDraft": "当前状态 {{status}} · 保存后生成待确认批次,不会直接发布", + "enter23Numbers": "请完整输入 23 组 4 位数字", + "draftSaved": "已保存草稿 v{{version}},等待确认发布", + "saveFailed": "保存失败", + "clear": "清空", + "saveDraft": "保存草稿", + "saving": "保存中…", + "pendingBatches": "待确认批次", + "noPendingBatches": "当前没有待审核(pending_review)批次。", + "batchId": "批次 ID", + "numberCount": "号码条数", + "reviewAndPublishAction": "核对并发布", + "noPublishPermission": "无发布权限", + "batchNotFound": "未找到批次", + "batchNotFoundDesc": "请返回审核列表确认 batch id。", + "backToReviewQueue": "返回审核队列", + "publishTitle": "发布", + "cannotPublish": "不可发布", + "cannotPublishDesc": "当前批次状态为「{{status}}」。", + "checkBeforePublish": "请核对以下号码后再发布", + "checkBeforePublishDesc": "确认无误后点击发布。", + "publishedView": "查看已发布展示", + "confirmPublish": "确认发布", + "submitting": "提交中…", + "publishSuccess": "已发布 · {{drawNo}} · 状态 {{status}}", + "publishFailed": "发布失败", + "sourceTypeFull": "生成方式:{{source}} · 号码条数:{{count}}/23 · RNG 摘要:{{hash}}", + "subnav": { + "status": "期号状态", + "results": "开奖结果", + "finance": "期号收支", + "review": "审核与发布" + }, + "statusOptions": { + "all": "不限", + "pending": "未开始", + "open": "可下注", + "closing": "封盘中", + "closed": "已封盘待开奖", + "drawing": "开奖处理中", + "review": "待人工审核", + "cooldown": "冷静期", + "settling": "结算处理中", + "settled": "已结算", + "cancelled": "已取消" + }, + "resultSlots": { + "first": "头奖", + "second": "二奖", + "third": "三奖", + "starter": "特别奖 {{index}}", + "consolation": "安慰奖 {{index}}" + } +} diff --git a/src/i18n/locales/zh/jackpot.json b/src/i18n/locales/zh/jackpot.json new file mode 100644 index 0000000..da89e3d --- /dev/null +++ b/src/i18n/locales/zh/jackpot.json @@ -0,0 +1,46 @@ +{ + "title": "奖池", + "configTitle": "Jackpot 奖池配置", + "loadFailed": "加载失败", + "saveSuccess": "已保存", + "saveFailed": "保存失败", + "invalidDrawId": "请填写有效的期号 ID", + "manualBurstSuccess": "已手动触发爆池", + "manualBurstFailed": "手动爆池失败", + "noPoolData": "暂无奖池数据", + "displayBalance": "展示余额 {{amount}}", + "currentAmount": "当前池余额(最小单位)", + "contributionRate": "蓄水比例 0–1", + "triggerThreshold": "爆池阈值(最小单位)", + "payoutRate": "爆池派彩比例 0–1", + "forceTriggerGap": "强制爆池间隔(已结算期数)", + "minBetAmount": "最低下注额(最小单位)", + "comboTriggerPlays": "组合触发玩法(逗号分隔)", + "status": "开关", + "disabled": "关闭", + "enabled": "开启", + "saving": "保存中…", + "save": "保存", + "manualBurstDrawId": "手动爆池期号 ID", + "manualBurstAmount": "爆池金额(空为全部)", + "processing": "处理中…", + "manualBurst": "手动爆池", + "filter": "筛选", + "drawNo": "期号", + "optional": "可选", + "apply": "应用", + "payoutRecords": "Jackpot 派彩记录", + "contributionRecords": "Jackpot 蓄水记录", + "subnavLabel": "Jackpot 子导航", + "subnavPools": "奖池配置", + "subnavRecords": "记录", + "payoutLoadFailed": "派彩记录加载失败", + "contributionLoadFailed": "蓄水记录加载失败", + "trigger": "触发", + "payoutAmount": "派彩额", + "winnerCount": "中奖人数", + "time": "时间", + "ticketNo": "注单", + "player": "玩家", + "contributionAmount": "蓄水额" +} diff --git a/src/i18n/locales/zh/players.json b/src/i18n/locales/zh/players.json new file mode 100644 index 0000000..cbef2f9 --- /dev/null +++ b/src/i18n/locales/zh/players.json @@ -0,0 +1,49 @@ +{ + "title": "玩家", + "listTitle": "玩家列表", + "createPlayer": "新建玩家", + "searchPlaceholder": "按玩家 ID / 用户名 / 昵称搜索", + "search": "搜索", + "refresh": "刷新", + "loadFailed": "加载玩家列表失败", + "siteCodeRequired": "请填写主站编号", + "sitePlayerIdRequired": "请填写主站玩家 ID", + "createFailed": "创建玩家失败", + "createSuccess": "已创建玩家 {{name}}", + "noChanges": "没有变更", + "updateFailed": "更新玩家失败", + "updateSuccess": "已更新 {{name}}", + "deleteFailed": "删除失败", + "deleteSuccess": "已删除玩家 {{name}}", + "statusNormal": "正常", + "statusFrozen": "冻结", + "statusBanned": "封禁", + "site": "主站", + "sitePlayerId": "主站玩家ID", + "username": "用户名", + "nickname": "昵称", + "currency": "币种", + "balance": "余额", + "available": "可用", + "status": "状态", + "lastLogin": "最后登录", + "actions": "操作", + "edit": "编辑", + "delete": "删除", + "createDialogTitle": "新建玩家", + "editDialogTitle": "编辑玩家", + "createDialogDesc": "手动注册一个主站玩家到彩票平台,通常由 SSO 登录自动创建。", + "editDialogDesc": "编辑玩家信息。", + "siteCode": "主站编号", + "siteCodePlaceholder": "例如 main_site", + "sitePlayerIdLabel": "主站玩家 ID", + "sitePlayerIdPlaceholder": "主站返回的唯一标识", + "usernamePlaceholderOptional": "选填", + "nicknamePlaceholderOptional": "选填", + "defaultCurrency": "默认币种", + "cancel": "取消", + "save": "保存", + "saving": "保存中…", + "confirmDelete": "确认删除", + "confirmDeleteDesc": "确定要删除玩家 {{name}} 吗?此操作不可恢复。" +} diff --git a/src/i18n/locales/zh/reconcile.json b/src/i18n/locales/zh/reconcile.json new file mode 100644 index 0000000..ab1ba13 --- /dev/null +++ b/src/i18n/locales/zh/reconcile.json @@ -0,0 +1,45 @@ +{ + "title": "对账", + "createTitle": "人工发起对账", + "createDesc": "异常流水由定时任务自动核对。此处供财务按产品文档手动触发:选择对账类型与时间范围;可选填写待核对对象(玩家标识、划转单号或幂等键,每行一条)。任务与明细落库留痕,后续可接自动差异引擎。", + "reconcileType": "对账类型", + "walletTransfer": "钱包划转(主站 ⇄ 彩票)", + "startTime": "对账开始时间", + "endTime": "对账结束时间", + "scope": "限定范围(可选)", + "scopePlaceholder": "每行一条待核对引用,例如:玩家 ID、钱包划转单号、幂等键等。\n留空表示本时间段内不额外指定单据(仅任务留痕)。", + "scopeHint": "与「钱包流水」中待对账(pending_reconcile)流水对照使用时,可将单号或幂等键粘贴至上方。", + "advancedToggleOpen": "展开高级选项(自定义明细 JSON)", + "advancedToggleClose": "收起高级选项(自定义明细 JSON)", + "advancedJson": "明细 JSON(将覆盖上方「限定范围」生成的行)", + "createTask": "创建对账任务", + "submitting": "提交中…", + "loadFailed": "加载失败", + "loadItemsFailed": "加载明细失败", + "periodRequired": "请填写对账时间范围(开始与结束)", + "periodInvalid": "时间无效,请检查所选日期与时间", + "periodOrderInvalid": "结束时间需晚于或等于开始时间", + "advancedJsonInvalid": "高级选项中的 JSON 无法解析", + "createSuccess": "已创建对账任务", + "createFailed": "创建失败", + "noCreatePermission": "当前账号无新建对账任务权限。", + "jobsTitle": "对账任务", + "jobsDesc": "点击一行查看差异明细与分页。", + "refresh": "刷新", + "jobNo": "任务号", + "type": "类型", + "status": "状态", + "period": "对账周期", + "createdAt": "创建时间", + "detailsTitle": "任务明细", + "sideARef": "彩票侧引用", + "sideBRef": "主站侧引用", + "differenceAmount": "差额(分)", + "noDetails": "无明细", + "statusCompleted": "已完成", + "statusRunning": "执行中", + "statusFailed": "失败", + "itemMismatch": "不一致", + "itemMatched": "一致", + "itemPendingCheck": "待核对" +} diff --git a/src/i18n/locales/zh/reports.json b/src/i18n/locales/zh/reports.json index cc07ce5..63b0fc5 100644 --- a/src/i18n/locales/zh/reports.json +++ b/src/i18n/locales/zh/reports.json @@ -1,3 +1,34 @@ { - "title": "报表" + "title": "报表", + "createExport": "新建导出", + "reportType": "报表类型", + "exportFormat": "导出格式", + "filterJson": "filter_json(可选)", + "parseFilterFailed": "筛选 JSON 无法解析", + "createSuccess": "已创建导出任务", + "createFailed": "创建失败", + "downloadFailed": "下载失败", + "taskList": "任务列表", + "jobId": "任务号", + "type": "类型", + "format": "格式", + "status": "状态", + "output": "输出", + "download": "下载", + "createdAt": "创建时间", + "id": "ID", + "empty": "无数据", + "reportTypes": { + "draw_profit_summary": "期号盈亏", + "daily_profit_summary": "每日盈亏汇总", + "player_win_loss": "玩家输赢报表", + "wallet_transfer_report": "玩家转入转出报表", + "hot_number_risk_report": "热门号码风险报表", + "play_dimension_report": "玩法维度报表", + "sold_out_number_report": "售罄号码报表", + "rebate_commission_report": "佣金回水报表", + "audit_operation_report": "后台操作审计报表", + "wallet_txns_daily": "钱包流水日报", + "transfer_orders_daily": "转账单日报" + } } diff --git a/src/i18n/locales/zh/risk.json b/src/i18n/locales/zh/risk.json new file mode 100644 index 0000000..6e27dbc --- /dev/null +++ b/src/i18n/locales/zh/risk.json @@ -0,0 +1,91 @@ +{ + "title": "风控", + "center": "风控中心", + "drawNo": "期号", + "status": "状态", + "closeTime": "封盘时间", + "actions": "操作", + "all": "全部", + "search": "搜索", + "refresh": "刷新", + "fuzzyDrawNo": "模糊匹配期号", + "loadDrawListFailed": "加载期号列表失败", + "enterRisk": "进入风控", + "poolsTitle": "风险池", + "searchNumber": "搜索号码", + "searchNumberPlaceholder": "如 8888", + "riskFilter": "风险筛选", + "sort": "排序", + "filterAll": "全部", + "filterSoldOut": "售罄", + "filterHighRisk": ">80%", + "sortUsageDesc": "占用比 ↓(热门)", + "sortLockedDesc": "已占用额 ↓", + "sortRemainingAsc": "剩余额 ↑(紧俏)", + "sortNumberAsc": "号码 ↑", + "loadPoolsFailed": "加载风险池失败", + "capAmount": "封顶", + "lockedAmount": "已占用", + "remainingAmount": "剩余", + "usageRatio": "占用比", + "poolStatus": "状态", + "soldOut": "售罄", + "warning": "预警", + "normal": "正常", + "recover": "恢复", + "close": "关闭", + "view": "查看", + "manualCloseSuccess": "已手动关闭号码下注", + "recoverSuccess": "已恢复号码下注", + "actionFailed": "操作失败", + "detailTitle": "风险池详情", + "loadDetailFailed": "加载风险池详情失败", + "backToList": "返回列表", + "backToAllPools": "返回全部风险池", + "numberTitle": "号码 {{number}}", + "drawMeta": "期号 {{drawNo}}", + "totalCap": "封顶额", + "lockedWorstCase": "已占用(最坏赔付预留)", + "remainingSellable": "剩余可售", + "isSoldOut": "售罄", + "yes": "是", + "no": "否", + "occupationLogs": "本号码占用 / 释放流水", + "time": "时间", + "action": "动作", + "amount": "金额", + "source": "来源", + "ticketNo": "注单号", + "playCode": "玩法", + "loadLogsFailed": "加载占用流水失败", + "lockLogsTitle": "风险占用流水", + "drawInfoLoadFailed": "无法加载期号信息", + "loadingDraw": "加载期号…", + "headerTitle": "风控 · 第 {{drawNo}} 期", + "databaseStatus": "数据库状态", + "hallPreviewStatus": "(大厅展示态:{{status}})", + "subnavOccupancy": "风险占用", + "subnavHot": "热门号码", + "subnavSoldOut": "售罄列表", + "subnavPools": "全部风险池", + "changeDraw": "更换期号", + "number4d": "号码(4 位)", + "optional": "可选", + "actionFilter": "动作", + "noLimit": "不限", + "lock": "锁定 lock", + "release": "释放 release", + "applyFilter": "应用筛选", + "statusOptions": { + "pending": "未开始", + "open": "可下注", + "closing": "封盘中", + "closed": "已封盘待开奖", + "drawing": "开奖处理中", + "review": "待审核", + "cooldown": "冷静期", + "settling": "结算中", + "settled": "已结算", + "cancelled": "已取消" + } +} diff --git a/src/i18n/locales/zh/settlement.json b/src/i18n/locales/zh/settlement.json new file mode 100644 index 0000000..6f26a4e --- /dev/null +++ b/src/i18n/locales/zh/settlement.json @@ -0,0 +1,54 @@ +{ + "title": "结算", + "filter": "筛选", + "drawNo": "期号", + "status": "状态", + "apply": "应用", + "batchList": "结算批次", + "loadFailed": "加载失败", + "exportFailed": "导出失败", + "actionSuccess": "{{name}}成功", + "actionFailed": "{{name}}失败", + "placeholderDrawNo": "如 20260511-001", + "reviewStatus": "审核状态", + "ticketCount": "注单数", + "winCount": "中奖笔数", + "payoutTotal": "派彩合计", + "jackpot": "Jackpot", + "finishedAt": "完成时间", + "details": "明细", + "approve": "审核通过", + "pass": "通过", + "reject": "驳回", + "payout": "派彩", + "export": "导出", + "backToList": "返回批次列表", + "errorTitle": "错误", + "retry": "重试", + "batchSummary": "批次 #{{id}}", + "summaryMeta": "期号 {{drawNo}} · 期状态 {{drawStatus}} · 结果批次 v{{version}}", + "settlementStatus": "结算状态", + "reviewState": "审核状态", + "ticketTotal": "注单数", + "winTotal": "中奖笔数", + "payoutAmount": "派彩合计", + "jackpotPayout": "Jackpot 划出", + "startedAt": "开始", + "endedAt": "结束", + "runPayout": "执行派彩", + "exportSettlementReport": "导出结算报表", + "loadingSummary": "加载摘要…", + "detailTitle": "注单结算明细", + "ticketNo": "注单号", + "playCode": "玩法", + "player": "玩家", + "matchedTier": "匹配档", + "regularPayout": "常规派彩", + "loadingDetails": "加载明细…", + "statusOptions": { + "all": "不限", + "running": "进行中", + "completed": "已完成", + "failed": "失败" + } +} diff --git a/src/i18n/locales/zh/tickets.json b/src/i18n/locales/zh/tickets.json new file mode 100644 index 0000000..46799a9 --- /dev/null +++ b/src/i18n/locales/zh/tickets.json @@ -0,0 +1,19 @@ +{ + "title": "注单", + "playerTicketQuery": "玩家注单查询", + "playerId": "玩家 ID", + "invalidPlayerId": "请输入有效玩家 ID", + "drawNoOptional": "期号 draw_no(可选)", + "drawNoPlaceholder": "如 20260520-001", + "query": "查询", + "loadFailed": "加载失败", + "ticketNo": "注单号", + "orderNo": "订单号", + "drawNo": "期号", + "playCode": "玩法", + "number": "号码", + "actualDeduct": "实扣", + "status": "状态", + "failReason": "失败原因", + "winAmount": "中奖" +} diff --git a/src/i18n/locales/zh/wallet.json b/src/i18n/locales/zh/wallet.json new file mode 100644 index 0000000..5ad8b76 --- /dev/null +++ b/src/i18n/locales/zh/wallet.json @@ -0,0 +1,69 @@ +{ + "title": "钱包", + "subnavLabel": "钱包子页", + "subnavTransactions": "钱包流水", + "subnavTransferOrders": "转账单", + "noPermission": "当前账号无访问该页的权限", + "copySuccess": "{{label}}已复制到剪贴板", + "copyFailed": "复制失败,请检查浏览器权限或手动选择文本", + "statusProcessing": "处理中", + "statusSuccess": "成功", + "statusFailed": "失败", + "statusPendingReconcile": "待对账", + "statusReversed": "已冲正", + "statusManuallyProcessed": "已人工处理", + "statusPosted": "已记账", + "filterAll": "不限", + "transferIn": "主站转入", + "transferOut": "主站转出", + "transferOutRefund": "转出失败回补", + "transferOrders": "转账单", + "walletTransactions": "钱包流水", + "playerWalletQuery": "玩家钱包查询", + "localTransferNo": "本地单号", + "externalRefNo": "主站流水号", + "playerAccount": "玩家账号", + "playerAccountPlaceholder": "主站玩家 ID 或用户名(模糊)", + "playerId": "玩家 ID", + "playerIdOptional": "可选,优先于账号", + "requestDateRange": "请求日期范围", + "status": "状态", + "options": "选项", + "abnormalOnly": "仅异常单", + "abnormalOnlyPending": "仅异常(待对账)", + "search": "搜索", + "resetFilters": "重置筛选", + "refreshCurrentPage": "刷新当前页", + "loadFailed": "加载失败", + "direction": "方向", + "amount": "金额", + "failReason": "失败原因", + "requestTime": "请求时间", + "finishedTime": "完成时间", + "actions": "操作", + "reverse": "冲正", + "manualProcess": "人工处理", + "processing": "处理中…", + "reverseSuccess": "冲正成功", + "manualProcessSuccess": "人工处理成功", + "actionFailed": "操作失败", + "txnNo": "流水号", + "bizType": "类型(业务)", + "type": "类型", + "queryFailed": "查询失败", + "invalidPlayerId": "请输入有效玩家 ID", + "querying": "查询中…", + "query": "查询", + "sitePlayer": "站点玩家", + "walletType": "类型", + "currency": "币种", + "balanceMinor": "余额(最小单位)", + "availableBalance": "可用(推算)", + "noWalletRows": "暂无钱包行(从未下过注或未划转也可能无记录)", + "copyTransferNo": "本地单号", + "copyExternalRefNo": "主站流水号", + "copyTxnNo": "流水号", + "copyExternalTxnRefNo": "主站流水号", + "in": "入", + "out": "出" +} diff --git a/src/modules/_config/admin-nav.ts b/src/modules/_config/admin-nav.ts index 756cebf..832106a 100644 --- a/src/modules/_config/admin-nav.ts +++ b/src/modules/_config/admin-nav.ts @@ -1,7 +1,8 @@ /** - * 导航与路由的单一事实来源;新增业务模块时先改这里,再增加 `app/admin/(shell)/.../page.tsx`。 + * Single source of truth for admin navigation and routes. * - * `requiredAny` 与登录接口返回的 `admin.permissions`(Laravel `prd.*`)对齐;缺省表示任意已登录用户可见。 + * `requiredAny` matches `admin.permissions` from the login response (Laravel `prd.*`). + * When omitted, the item is visible to any signed-in admin. */ export const ADMIN_BASE = "/admin" as const; @@ -24,21 +25,21 @@ export type AdminNavItem = { | "audit" | "admin_users"; activeMatchPrefix?: string; - /** 拥有任一权限 slug 即显示侧栏项 */ + /** Show the nav item when the user has any of these permission slugs. */ requiredAny?: readonly string[]; }; export const adminShellNavItems: AdminNavItem[] = [ - { segment: "dashboard", label: "仪表盘", href: "/admin" }, + { segment: "dashboard", label: "Dashboard", href: "/admin" }, { segment: "admin_users", - label: "管理列表", + label: "Admin Users", href: "/admin/admin-users", requiredAny: ["prd.admin_user.manage"], }, { segment: "players", - label: "玩家列表", + label: "Players", href: "/admin/players", requiredAny: [ "prd.users.manage", @@ -48,7 +49,7 @@ export const adminShellNavItems: AdminNavItem[] = [ }, { segment: "wallet", - label: "钱包流水", + label: "Wallet", href: "/admin/wallet/transactions", activeMatchPrefix: "/admin/wallet", requiredAny: [ @@ -62,13 +63,13 @@ export const adminShellNavItems: AdminNavItem[] = [ }, { segment: "draws", - label: "期号列表", + label: "Draws", href: "/admin/draws", requiredAny: ["prd.draw_result.manage", "prd.draw_result.view"], }, { segment: "config", - label: "运营配置", + label: "Configuration", href: "/admin/config", requiredAny: [ "prd.play_switch.manage", @@ -83,13 +84,13 @@ export const adminShellNavItems: AdminNavItem[] = [ }, { segment: "risk", - label: "风控", + label: "Risk", href: "/admin/risk", requiredAny: ["prd.draw_result.view", "prd.draw_result.manage"], }, { segment: "settlement", - label: "结算", + label: "Settlement", href: "/admin/settlement-batches", requiredAny: [ "prd.payout.manage", @@ -106,7 +107,7 @@ export const adminShellNavItems: AdminNavItem[] = [ }, { segment: "reconcile", - label: "对账", + label: "Reconcile", href: "/admin/reconcile", requiredAny: [ "prd.wallet_reconcile.manage", @@ -116,7 +117,7 @@ export const adminShellNavItems: AdminNavItem[] = [ }, { segment: "tickets", - label: "玩家注单", + label: "Tickets", href: "/admin/tickets", requiredAny: [ "prd.users.view_cs", @@ -132,7 +133,7 @@ export const adminShellNavItems: AdminNavItem[] = [ }, { segment: "reports", - label: "报表导出", + label: "Reports", href: "/admin/reports", requiredAny: [ "prd.report.all", @@ -143,9 +144,9 @@ export const adminShellNavItems: AdminNavItem[] = [ }, { segment: "audit", - label: "审计日志", + label: "Audit Logs", href: "/admin/audit-logs", requiredAny: ["prd.audit.all", "prd.audit.self", "prd.audit.finance"], }, - { segment: "settings", label: "系统设置", href: "/admin/settings" }, + { segment: "settings", label: "Settings", href: "/admin/settings" }, ]; diff --git a/src/modules/admin-users/admin-users-console.tsx b/src/modules/admin-users/admin-users-console.tsx index 9b98709..ce4d25e 100644 --- a/src/modules/admin-users/admin-users-console.tsx +++ b/src/modules/admin-users/admin-users-console.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { ChevronDown } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { @@ -40,6 +41,7 @@ import { useAdminProfile } from "@/stores/admin-session"; import type { AdminPermissionCatalogData, AdminUserPermissionRow } from "@/types/api/index"; export function AdminUsersConsole(): React.ReactElement { + const { t } = useTranslation(["adminUsers", "common"]); const profile = useAdminProfile(); const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(25); @@ -59,7 +61,7 @@ export function AdminUsersConsole(): React.ReactElement { const [saving, setSaving] = useState(false); const [savingRoles, setSavingRoles] = useState(false); const [permissionOpen, setPermissionOpen] = useState(false); - /** `false` = 折叠;缺省为展开 */ + /** `false` = collapsed; default expanded */ const [directMenuExpanded, setDirectMenuExpanded] = useState>({}); const [accountOpen, setAccountOpen] = useState(false); @@ -129,16 +131,16 @@ export function AdminUsersConsole(): React.ReactElement { async function submitAccount(): Promise { const nick = formNickname.trim(); if (nick === "") { - toast.error("请填写昵称"); + toast.error(t("nicknameRequired")); return; } if (accountMode === "edit" && formPassword !== "" && formPassword.length < 8) { - toast.error("新密码至少 8 位"); + toast.error(t("newPasswordMin")); return; } if (accountMode === "create" && formCreateRoles.length === 0) { - toast.error("请至少选择一个角色"); + toast.error(t("roleRequired")); return; } @@ -147,11 +149,11 @@ export function AdminUsersConsole(): React.ReactElement { if (accountMode === "create") { const u = formUsername.trim(); if (u === "") { - toast.error("请填写登录账号"); + toast.error(t("usernameRequired")); return; } if (formPassword.length < 8) { - toast.error("密码至少 8 位"); + toast.error(t("passwordMin")); return; } const created = await postAdminUser({ @@ -164,7 +166,7 @@ export function AdminUsersConsole(): React.ReactElement { }); setItems((prev) => [created, ...prev]); setTotal((t) => t + 1); - toast.success(`已创建管理员 ${created.username}`); + toast.success(t("createSuccess", { name: created.username })); handleAccountDialogOpenChange(false); } else { const id = editingAccountId; @@ -186,11 +188,11 @@ export function AdminUsersConsole(): React.ReactElement { } const updated = await putAdminUser(id, body); setItems((prev) => prev.map((row) => (row.id === updated.id ? updated : row))); - toast.success(`已更新 ${updated.username}`); + toast.success(t("updateSuccess", { name: updated.username })); handleAccountDialogOpenChange(false); } } catch (e) { - const msg = e instanceof LotteryApiBizError ? e.message : "保存账号失败"; + const msg = e instanceof LotteryApiBizError ? e.message : t("saveAccountFailed"); toast.error(msg); } finally { setAccountSaving(false); @@ -206,10 +208,10 @@ export function AdminUsersConsole(): React.ReactElement { await deleteAdminUser(deleteTarget.id); setItems((prev) => prev.filter((r) => r.id !== deleteTarget.id)); setTotal((t) => Math.max(0, t - 1)); - toast.success(`已删除 ${deleteTarget.username}`); + toast.success(t("deleteSuccess", { name: deleteTarget.username })); setDeleteTarget(null); } catch (e) { - const msg = e instanceof LotteryApiBizError ? e.message : "删除失败"; + const msg = e instanceof LotteryApiBizError ? e.message : t("deleteFailed"); toast.error(msg); } finally { setDeleteBusy(false); @@ -229,10 +231,10 @@ export function AdminUsersConsole(): React.ReactElement { } const flat = catalog?.permissions ?? []; if (flat.length > 0) { - return [{ key: "all", label: "全部权限", permissions: flat }]; + return [{ key: "all", label: t("allPermissions"), permissions: flat }]; } return []; - }, [catalog]); + }, [catalog, t]); function isDirectGroupOpen(key: string): boolean { return directMenuExpanded[key] !== false; @@ -271,7 +273,7 @@ export function AdminUsersConsole(): React.ReactElement { setTotal(listData.meta.total); setLastPage(Math.max(1, listData.meta.last_page)); } catch (e) { - const msg = e instanceof LotteryApiBizError ? e.message : "加载管理员列表失败"; + const msg = e instanceof LotteryApiBizError ? e.message : t("loadFailed"); setErr(msg); setItems([]); setTotal(0); @@ -279,7 +281,7 @@ export function AdminUsersConsole(): React.ReactElement { } finally { setLoading(false); } - }, [page, perPage, query]); + }, [page, perPage, query, t]); useEffect(() => { queueMicrotask(() => { @@ -324,9 +326,9 @@ export function AdminUsersConsole(): React.ReactElement { : row, ), ); - toast.success(`已更新 ${result.username} 的角色`); + toast.success(t("saveRoleSuccess", { name: result.username })); } catch (e) { - const msg = e instanceof LotteryApiBizError ? e.message : "保存角色失败"; + const msg = e instanceof LotteryApiBizError ? e.message : t("saveRoleFailed"); toast.error(msg); } finally { setSavingRoles(false); @@ -354,9 +356,9 @@ export function AdminUsersConsole(): React.ReactElement { : row, ), ); - toast.success(`已更新 ${result.username} 的权限`); + toast.success(t("savePermissionSuccess", { name: result.username })); } catch (e) { - const msg = e instanceof LotteryApiBizError ? e.message : "保存权限失败"; + const msg = e instanceof LotteryApiBizError ? e.message : t("savePermissionFailed"); toast.error(msg); } finally { setSaving(false); @@ -368,15 +370,15 @@ export function AdminUsersConsole(): React.ReactElement {
- 管理员用户列表 + {t("listTitle")}
setKeyword(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { @@ -392,37 +394,37 @@ export function AdminUsersConsole(): React.ReactElement { setQuery(keyword.trim()); }} > - 搜索 + {t("actions.search", { ns: "common" })}
{err ?

{err}

: null} {loading && items.length === 0 ? ( -

加载中…

+

{t("states.loading", { ns: "common" })}

) : null}
ID - 账号 - 昵称 - 状态 - 角色 - 直接权限 - 有效权限 - 操作 + {t("table.account")} + {t("table.nickname")} + {t("table.status")} + {t("table.roles")} + {t("table.direct")} + {t("table.effective")} + {t("table.actions")} {items.length === 0 ? ( - 暂无数据 + {t("states.noData", { ns: "common" })} ) : ( @@ -439,18 +441,18 @@ export function AdminUsersConsole(): React.ReactElement { {row.status === 0 ? ( - 启用 + {t("status.enabled")} ) : ( - 禁用 + {t("status.disabled")} )}
{row.roles.length === 0 ? ( - + {t("common.none")} ) : ( row.roles.map((slug) => ( @@ -472,7 +474,7 @@ export function AdminUsersConsole(): React.ReactElement { openPermissionEditor(row); }} > - 权限 + {t("actions.permissions")}
@@ -526,7 +530,7 @@ export function AdminUsersConsole(): React.ReactElement { className="flex h-[min(88vh,800px)] max-h-[90vh] w-[calc(100%-2rem)] max-w-[calc(100%-2rem)] flex-col gap-0 overflow-hidden p-0 sm:h-[min(85vh,780px)] sm:max-w-3xl" > - 管理员权限 + {t("permissionDialog.title")} {selectedUser ? ( <> @@ -541,9 +545,9 @@ export function AdminUsersConsole(): React.ReactElement {
-

角色

+

{t("permissionDialog.rolesTitle")}

- 保存至默认站点,与「直接权限」叠加为有效权限。 + {t("permissionDialog.rolesDescription")}

@@ -559,7 +563,7 @@ export function AdminUsersConsole(): React.ReactElement { {r.name} {r.slug} - 含 {r.permission_slugs.length} 项功能权限 + {t("permissionDialog.rolePermissionCount", { count: r.permission_slugs.length })} @@ -570,15 +574,15 @@ export function AdminUsersConsole(): React.ReactElement {
-

直接权限

+

{t("permissionDialog.directTitle")}

- 按菜单/业务域展开,勾选具体的 prd.*;多数情况只调角色即可。 + {t("permissionDialog.directDescription")}

- 当前勾选的角色: + {t("permissionDialog.selectedRoles")} {draftRoles.length === 0 ? ( - + {t("common.none")} ) : ( {draftRoles.map((slug) => ( @@ -653,7 +657,7 @@ export function AdminUsersConsole(): React.ReactElement { handlePermissionDialogOpenChange(false); }} > - 关闭 + {t("actions.close", { ns: "common" })}
@@ -681,63 +685,69 @@ export function AdminUsersConsole(): React.ReactElement { - {accountMode === "create" ? "新建管理员" : "编辑账号"} + + {accountMode === "create" ? t("accountDialog.createTitle") : t("accountDialog.editTitle")} + {accountMode === "create" - ? "须为账号指定至少一个默认站点角色。登录账号仅可使用字母、数字、点、下划线与连字符,保存后为小写。" - : "登录账号不可修改。留空密码表示不修改。"} + ? t("accountDialog.createDescription") + : t("accountDialog.editDescription")}
-
登录账号
+
{t("accountDialog.username")}
setFormUsername(e.target.value)} />
-
昵称
+
{t("accountDialog.nickname")}
setFormNickname(e.target.value)} />
-
邮箱(可选)
+
{t("accountDialog.emailOptional")}
setFormEmail(e.target.value)} />
- 密码{accountMode === "edit" ? "(可选)" : ""} + {accountMode === "edit" ? t("accountDialog.passwordOptional") : t("accountDialog.password")}
setFormPassword(e.target.value)} />
{accountMode === "create" ? (
-
角色(默认站点,至少一项)
+
{t("accountDialog.rolesRequired")}

- 创建后即可在「权限」中继续调整角色或直接授权。 + {t("accountDialog.rolesDescription")}

{(catalog?.roles ?? []).length === 0 ? (

- 暂无角色数据,请等待列表加载完成后重试。 + {t("accountDialog.noRoles")}

) : ( (catalog?.roles ?? []).map((r) => { @@ -760,14 +770,14 @@ export function AdminUsersConsole(): React.ReactElement {
) : null}
-
状态
+
{t("table.status")}
@@ -777,10 +787,10 @@ export function AdminUsersConsole(): React.ReactElement { variant="outline" onClick={() => handleAccountDialogOpenChange(false)} > - 取消 + {t("actions.cancel")}
@@ -789,14 +799,10 @@ export function AdminUsersConsole(): React.ReactElement { !open && setDeleteTarget(null)}> - 确认删除 + {t("delete.confirmTitle")} {deleteTarget ? ( - <> - 确定删除管理员{" "} - {deleteTarget.username} - ?此操作不可撤销。 - + <>{t("delete.confirmDescription", { name: deleteTarget.username })} ) : null} @@ -807,7 +813,7 @@ export function AdminUsersConsole(): React.ReactElement { disabled={deleteBusy} onClick={() => setDeleteTarget(null)} > - 取消 + {t("actions.cancel")}
diff --git a/src/modules/admin-users/meta.ts b/src/modules/admin-users/meta.ts index 0d69ec8..c312c63 100644 --- a/src/modules/admin-users/meta.ts +++ b/src/modules/admin-users/meta.ts @@ -1,5 +1,5 @@ export const adminUsersModuleMeta = { segment: "admin_users", - title: "管理列表", + title: "Admins", description: "", } as const; diff --git a/src/modules/audit/audit-logs-console.tsx b/src/modules/audit/audit-logs-console.tsx index 28394cf..4d6f8e1 100644 --- a/src/modules/audit/audit-logs-console.tsx +++ b/src/modules/audit/audit-logs-console.tsx @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { getAdminAuditLogs } from "@/api/admin-audit"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; @@ -21,6 +22,7 @@ import { LotteryApiBizError } from "@/types/api/errors"; import type { AdminAuditLogListData } from "@/types/api/admin-audit"; export function AuditLogsConsole(): React.ReactElement { + const { t } = useTranslation(["audit", "common"]); const formatTs = useAdminDateTimeFormatter(); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); @@ -47,12 +49,12 @@ export function AuditLogsConsole(): React.ReactElement { }); setData(d); } catch (e) { - setErr(e instanceof LotteryApiBizError ? e.message : "加载失败"); + setErr(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" })); setData(null); } finally { setLoading(false); } - }, [page, perPage, appliedModule, appliedAction, appliedOpType]); + }, [page, perPage, appliedModule, appliedAction, appliedOpType, t]); useEffect(() => { queueMicrotask(() => { @@ -66,39 +68,39 @@ export function AuditLogsConsole(): React.ReactElement {
- 审计日志 + {t("title")}
- + setModuleCode(e.target.value)} - placeholder="精确匹配" + placeholder={t("exactMatch")} />
- + setActionCode(e.target.value)} - placeholder="精确匹配" + placeholder={t("exactMatch")} />
- + setOperatorType(e.target.value)} - placeholder="如 admin / system" + placeholder={t("operatorTypePlaceholder")} />
@@ -111,14 +113,14 @@ export function AuditLogsConsole(): React.ReactElement { setPage(1); }} > - 搜索 + {t("actions.search", { ns: "common" })}
{err ?

{err}

: null} {loading && !data ? ( -

加载中…

+

{t("states.loading", { ns: "common" })}

) : null} {data ? ( @@ -128,18 +130,18 @@ export function AuditLogsConsole(): React.ReactElement { ID - 操作者 - 模块 - 动作 - 目标 - 时间 + {t("operator")} + {t("module")} + {t("action")} + {t("target")} + {t("time")} {data.items.length === 0 ? ( - 无数据 + {t("empty")} ) : ( diff --git a/src/modules/audit/meta.ts b/src/modules/audit/meta.ts index 97fec79..6177863 100644 --- a/src/modules/audit/meta.ts +++ b/src/modules/audit/meta.ts @@ -1,5 +1,5 @@ export const auditLogsModuleMeta = { segment: "audit-logs", - title: "审计日志", + title: "Audit Logs", description: "", } as const; diff --git a/src/modules/auth/meta.ts b/src/modules/auth/meta.ts index 23e0315..27551a3 100644 --- a/src/modules/auth/meta.ts +++ b/src/modules/auth/meta.ts @@ -1,5 +1,5 @@ export const authModuleMeta = { segment: "login", - title: "登录", + title: "Login", description: "", } as const; diff --git a/src/modules/config/config-nav-model.ts b/src/modules/config/config-nav-model.ts index e65d116..dd4a22d 100644 --- a/src/modules/config/config-nav-model.ts +++ b/src/modules/config/config-nav-model.ts @@ -1,71 +1,49 @@ /** - * 运营配置子导航与面包屑的单一数据源。 - * 新增配置页:在此追加条目,并增加 `app/admin/(shell)/config/.../page.tsx`。 + * Single source of truth for config sub-navigation and breadcrumb routes. + * Add new config pages here and create the matching `app/admin/(shell)/config/.../page.tsx`. */ export type ConfigNavGroup = { id: string; - label: string; items: readonly { href: string; - title: string; - description: string; + key: string; }[]; }; export const CONFIG_NAV_GROUPS: readonly ConfigNavGroup[] = [ { id: "betting", - label: "投注与展示", items: [ { href: "/admin/config/plays", - title: "玩法与限额", - description: "目录开关、单玩法限额、版本发布", + key: "plays", }, { href: "/admin/config/odds", - title: "赔率", - description: "按玩法与奖级维护乘数与币种", + key: "odds", }, { href: "/admin/config/rebate", - title: "佣金 / 回水", - description: "从赔率草稿批量调整回水比例", + key: "rebate", }, ], }, { id: "risk_wallet", - label: "风控与资金", items: [ { href: "/admin/config/risk-cap", - title: "赔付封顶", - description: "按号码维度的封顶版本", + key: "risk-cap", }, { href: "/admin/config/wallet", - title: "钱包阈值", - description: "转入转出上下限(系统设置)", + key: "wallet", }, ], }, ] as const; -const CONFIG_ROUTE_LABEL_ENTRIES: readonly [string, string][] = [ - ["plays", "玩法与限额"], - ["odds", "赔率"], - ["rebate", "佣金 / 回水"], - ["risk-cap", "赔付封顶"], - ["wallet", "钱包阈值"], -]; - -/** 面包屑第三段 slug → 中文 */ -export const CONFIG_ROUTE_LABELS: Readonly> = Object.fromEntries( - CONFIG_ROUTE_LABEL_ENTRIES, -) as Readonly>; - export function flattenConfigNavHrefs(): string[] { const out: string[] = []; for (const g of CONFIG_NAV_GROUPS) { diff --git a/src/modules/config/config-status-badge.tsx b/src/modules/config/config-status-badge.tsx index 87658ba..f81fe41 100644 --- a/src/modules/config/config-status-badge.tsx +++ b/src/modules/config/config-status-badge.tsx @@ -1,13 +1,9 @@ +import { useTranslation } from "react-i18next"; import { Badge } from "@/components/ui/badge"; -const LABELS: Record = { - draft: "草稿", - active: "生效中", - archived: "已归档", -}; - export function ConfigStatusBadge({ status }: { status: string }) { - const label = LABELS[status] ?? status; + const { t } = useTranslation("config"); + const label = t(`versionStatus.${status}`, { defaultValue: status }); const className = status === "active" ? "border-emerald-500/20 bg-emerald-500/12 text-emerald-700 dark:text-emerald-300" diff --git a/src/modules/config/config-subnav.tsx b/src/modules/config/config-subnav.tsx index b95392a..cdf6a9a 100644 --- a/src/modules/config/config-subnav.tsx +++ b/src/modules/config/config-subnav.tsx @@ -2,15 +2,16 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useTranslation } from "react-i18next"; import { cn } from "@/lib/utils"; -const LINKS: { href: string; label: string; match?: "exact" | "prefix" }[] = [ - { href: "/admin/config/plays", label: "玩法配置" }, - { href: "/admin/config/odds", label: "赔率配置" }, - { href: "/admin/config/rebate", label: "佣金 / 回水" }, - { href: "/admin/config/risk-cap", label: "风控封顶" }, - { href: "/admin/config/wallet", label: "钱包配置" }, +const LINKS: { href: string; key: string; match?: "exact" | "prefix" }[] = [ + { href: "/admin/config/plays", key: "plays" }, + { href: "/admin/config/odds", key: "odds" }, + { href: "/admin/config/rebate", key: "rebate" }, + { href: "/admin/config/risk-cap", key: "risk-cap" }, + { href: "/admin/config/wallet", key: "wallet" }, ]; function linkActive(pathname: string, href: string, match: "exact" | "prefix"): boolean { @@ -21,14 +22,15 @@ function linkActive(pathname: string, href: string, match: "exact" | "prefix"): } export function ConfigSubNav() { + const { t } = useTranslation("config"); const pathname = usePathname(); return (
@@ -161,10 +157,10 @@ export function ConfigVersionSwitcher({
- {sheetTitle} + {resolvedSheetTitle} - {sheetDescription} + {resolvedSheetDescription}
@@ -183,7 +179,7 @@ export function ConfigVersionSwitcher({
{sortedVersions.length === 0 ? ( - 暂无版本记录。 + {t("versionSwitcher.empty", { ns: "config" })} ) : ( STATUS_ORDER.map((status) => { @@ -204,11 +200,11 @@ export function ConfigVersionSwitcher({ )} />

- {versionStatusLabel(status)} + {t(`versionStatus.${status}`, { ns: "config" })}

- {rows.length} 条 + {t("versionSwitcher.count", { ns: "config", count: rows.length })}

@@ -244,13 +240,21 @@ export function ConfigVersionSwitcher({

- 生效时间:{v.effective_at ? formatDt(v.effective_at) : "—"} - {v.reason ? ` · 备注:${v.reason}` : ""} + {t("versionSwitcher.effectiveAt", { + ns: "config", + value: v.effective_at ? formatDt(v.effective_at) : "—", + })} + {v.reason + ? ` · ${t("versionSwitcher.note", { + ns: "config", + value: v.reason, + })}` + : ""}

{isCurrent ? ( - 当前查看 + {t("versionSwitcher.current", { ns: "config" })} ) : null} @@ -265,7 +269,9 @@ export function ConfigVersionSwitcher({ )} onClick={() => switchTo(v.id)} > - {isCurrent ? "已选中" : "查看"} + {isCurrent + ? t("versionSwitcher.selected", { ns: "config" }) + : t("versionSwitcher.view", { ns: "config" })} {onRollbackVersion && v.status !== "draft" ? ( ) : null} {onDeleteVersion && v.status !== "active" ? ( @@ -291,7 +297,7 @@ export function ConfigVersionSwitcher({ disabled={deletingId === v.id} onClick={() => setDeleteTarget(v)} > - 删除 + {t("versionSwitcher.delete", { ns: "config" })} ) : null} @@ -312,14 +318,18 @@ export function ConfigVersionSwitcher({ !open && setDeleteTarget(null)}> - 确认删除版本? + {t("versionSwitcher.deleteConfirmTitle", { ns: "config" })} - 将永久删除版本 ID {deleteTarget?.id}(version_no {deleteTarget?.version_no})。生效中的版本不可删除。 + {t("versionSwitcher.deleteConfirmDescription", { + ns: "config", + id: deleteTarget?.id, + version: deleteTarget?.version_no, + })} diff --git a/src/modules/config/config-workspace-shell.tsx b/src/modules/config/config-workspace-shell.tsx index c981b3f..6c50fb1 100644 --- a/src/modules/config/config-workspace-shell.tsx +++ b/src/modules/config/config-workspace-shell.tsx @@ -3,6 +3,7 @@ import type { ReactNode } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useTranslation } from "react-i18next"; import { cn } from "@/lib/utils"; import { CONFIG_NAV_GROUPS } from "@/modules/config/config-nav-model"; @@ -12,6 +13,7 @@ function navLinkActive(pathname: string, href: string): boolean { } export function ConfigWorkspaceShell({ children }: { children: ReactNode }) { + const { t } = useTranslation("config"); const pathname = usePathname() ?? ""; return ( @@ -21,15 +23,15 @@ export function ConfigWorkspaceShell({ children }: { children: ReactNode }) {

- 运营配置导航 + {t("nav.sidebarTitle")}

-
- 玩法名称 - 分类 - 状态 - 显示名称 - 排序 - 最小下注 - 最大下注 - 操作 + Play Code + Category + Status + Display Name + Order + Min Bet + Max Bet + Actions @@ -499,11 +494,11 @@ export function PlayConfigDocScreen() { onCheckedChange={(v) => { updateConfigRow(row.play_code, { is_enabled: v === true }); }} - aria-label={`启用 ${row.play_code}`} + aria-label={`Enable ${row.play_code}`} /> ) : ( - {row.is_enabled ? "启用" : "停用"} + {row.is_enabled ? "Enabled" : "Disabled"} )} @@ -593,10 +588,10 @@ export function PlayConfigDocScreen() { disabled={saving} onClick={() => openRuleEditor(row.play_code)} > - 规则说明 + Rule Text ) : ( - 只读 + Read only )} @@ -610,9 +605,9 @@ export function PlayConfigDocScreen() { - 规则说明(中文) + Rule Text (Chinese) - 玩法 {rulePlayCode ?? "—"};保存前内容仅写入草稿,需点「保存草稿」后随版本发布。 + Play {rulePlayCode ?? "—"}; changes are only stored in the draft until you save and publish it.
@@ -626,10 +621,10 @@ export function PlayConfigDocScreen() {
diff --git a/src/modules/config/doc/prize-scopes.ts b/src/modules/config/doc/prize-scopes.ts index 08fbc92..e43eecd 100644 --- a/src/modules/config/doc/prize-scopes.ts +++ b/src/modules/config/doc/prize-scopes.ts @@ -1,4 +1,4 @@ -/** 奖项档位顺序(含 starter / consolation)。 */ +/** Prize scope order, including starter and consolation. */ export const PRIZE_SCOPE_ORDER = [ "first", @@ -11,14 +11,14 @@ export const PRIZE_SCOPE_ORDER = [ export type PrizeScopeCode = (typeof PRIZE_SCOPE_ORDER)[number]; export const PRIZE_SCOPE_LABELS: Record = { - first: "头奖赔率", - second: "二奖赔率", - third: "三奖赔率", - starter: "特别奖赔率", - consolation: "安慰奖赔率", + first: "First Prize Odds", + second: "Second Prize Odds", + third: "Third Prize Odds", + starter: "Starter Prize Odds", + consolation: "Consolation Prize Odds", }; -/** 文档示意:特别奖 / 安慰奖按组数展示时的倍数提示(仅文案)。 */ +/** Display-only multiplier hints for starter and consolation grouped prizes. */ export const PRIZE_SCOPE_MULTIPLIER_HINT: Partial> = { starter: "× 10", consolation: "× 10", diff --git a/src/modules/config/doc/rebate-config-doc-screen.tsx b/src/modules/config/doc/rebate-config-doc-screen.tsx index 4d10486..5aeea0e 100644 --- a/src/modules/config/doc/rebate-config-doc-screen.tsx +++ b/src/modules/config/doc/rebate-config-doc-screen.tsx @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { @@ -47,6 +48,7 @@ function inferPercentFrom(dim: 2 | 3 | 4, rows: OddsItemRow[], typeList: AdminPl } export function RebateConfigDocScreen() { + const { t } = useTranslation(["config", "common"]); const formatDt = useAdminDateTimeFormatter(); const [types, setTypes] = useState([]); const [listRows, setListRows] = useState([]); @@ -67,20 +69,20 @@ export function RebateConfigDocScreen() { const d = await getAdminPlayTypes(); setTypes(d.items); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "加载玩法失败"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" })); setTypes([]); } - }, []); + }, [t]); const refreshList = useCallback(async () => { try { const d = await getAllConfigVersions(getOddsVersions); setListRows(d.items); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "加载版本失败"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" })); setListRows([]); } - }, []); + }, [t]); useEffect(() => { queueMicrotask(async () => { @@ -105,13 +107,13 @@ export function RebateConfigDocScreen() { setP3(inferPercentFrom(3, rows, typeList)); setP4(inferPercentFrom(4, rows, typeList)); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "加载明细失败"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" })); setDetail(null); setDraftRows([]); } finally { setLoadingDetail(false); } - }, []); + }, [t]); useEffect(() => { if (listRows.length === 0 || selectedId !== "") { @@ -194,10 +196,10 @@ export function RebateConfigDocScreen() { setP2(inferPercentFrom(2, rows, types)); setP3(inferPercentFrom(3, rows, types)); setP4(inferPercentFrom(4, rows, types)); - toast.success("已保存草稿"); + toast.success(t("versionActions.saveDraft", { ns: "config" })); void refreshList(); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "保存失败"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("wallet.saveFailed", { ns: "config" })); } finally { setSaving(false); } @@ -216,11 +218,11 @@ export function RebateConfigDocScreen() { setP2(inferPercentFrom(2, rows, types)); setP3(inferPercentFrom(3, rows, types)); setP4(inferPercentFrom(4, rows, types)); - toast.success("已发布赔率版本(含回水)"); + toast.success("Published odds version with rebate"); void refreshList(); setSelectedId(String(d.id)); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "发布失败"); + toast.error(e instanceof LotteryApiBizError ? e.message : "Publish failed"); } finally { setSaving(false); } @@ -234,7 +236,7 @@ export function RebateConfigDocScreen() { reason: `rebate draft ${new Date().toISOString()}`, clone_from_version_id: active?.id ?? null, }); - toast.success(`已创建草稿 v${d.version_no}`); + toast.success(`Created draft v${d.version_no}`); await refreshList(); setSelectedId(String(d.id)); const rows = d.items.map((it) => ({ ...it })); @@ -244,7 +246,7 @@ export function RebateConfigDocScreen() { setP3(inferPercentFrom(3, rows, types)); setP4(inferPercentFrom(4, rows, types)); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "创建草稿失败"); + toast.error(e instanceof LotteryApiBizError ? e.message : "Create draft failed"); } finally { setSaving(false); } @@ -255,10 +257,10 @@ export function RebateConfigDocScreen() { async function handleDeleteVersion(row: ConfigVersionSummary) { try { await deleteOddsVersion(row.id); - toast.success("已删除该版本"); + toast.success(t("versionSwitcher.delete", { ns: "config" })); await refreshList(); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "删除失败"); + toast.error(e instanceof LotteryApiBizError ? e.message : "Delete failed"); throw e; } } @@ -266,7 +268,7 @@ export function RebateConfigDocScreen() { return ( - 佣金 / 回水配置 + {t("nav.items.rebate", { ns: "config" })}
@@ -275,8 +277,8 @@ export function RebateConfigDocScreen() { selectedId={selectedId} onSelectedIdChange={setSelectedId} loading={loading} - sheetTitle="回水配置版本" - sheetDescription="回水写入赔率版本草稿;选择与赔率配置共用同一套版本。" + sheetTitle={`${t("nav.items.rebate", { ns: "config" })} ${t("versionSwitcher.sheetTitle", { ns: "config" })}`} + sheetDescription="Rebate is stored in the odds draft version and shares the same version set as odds." onDeleteVersion={handleDeleteVersion} className="w-auto min-w-0" /> @@ -286,7 +288,7 @@ export function RebateConfigDocScreen() { loadingList={loading} loadingDetail={loadingDetail} saving={saving} - publishLabel="发布生效" + publishLabel="Publish" onRefresh={() => void refreshList()} onNewDraft={() => void handleNewDraft()} onSaveDraft={() => void handleSave()} @@ -295,9 +297,9 @@ export function RebateConfigDocScreen() { {detail ? (

- 编辑版本 v{detail.version_no} · {detail.status === "draft" ? "草稿" : detail.status === "active" ? "生效中" : "已归档"} + Editing version v{detail.version_no} · {detail.status === "draft" ? "Draft" : detail.status === "active" ? "Active" : "Archived"} {!isDraft ? ( - — 请先新建草稿再改回水。 + - Create a draft before editing rebate. ) : null}

) : null} @@ -305,7 +307,7 @@ export function RebateConfigDocScreen() {
- + {isDraft ? (
- + {isDraft ? (
- + {isDraft ? (
- +

- 界面占位:后续可与风控 / 结算规则字段对齐并持久化。 + Placeholder field. It can later be aligned with risk and settlement rules and persisted.

- 生效时间(当前线上赔率版本) + Effective Time (current active odds version) {activeHead?.effective_at ? formatDt(activeHead.effective_at) : "—"}
{loading || loadingDetail ? ( -

加载中…

+

{t("states.loading", { ns: "common" })}

) : null} diff --git a/src/modules/config/doc/risk-cap-doc-screen.tsx b/src/modules/config/doc/risk-cap-doc-screen.tsx index 354c024..2072124 100644 --- a/src/modules/config/doc/risk-cap-doc-screen.tsx +++ b/src/modules/config/doc/risk-cap-doc-screen.tsx @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { @@ -70,6 +71,7 @@ function defaultRiskRowFromAmount(amount: number): DraftRiskRow { } export function RiskCapDocScreen() { + const { t } = useTranslation(["config", "adminUsers", "common"]); const formatDt = useAdminDateTimeFormatter(); const [list, setList] = useState([]); const [selectedId, setSelectedId] = useState(""); @@ -92,13 +94,13 @@ export function RiskCapDocScreen() { const d = await getAllConfigVersions(getRiskCapVersions); setList(d.items); } catch (e) { - const msg = e instanceof LotteryApiBizError ? e.message : "加载版本列表失败"; + const msg = e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" }); setError(msg); setList([]); } finally { setLoadingList(false); } - }, []); + }, [t]); useEffect(() => { queueMicrotask(() => { @@ -130,14 +132,14 @@ export function RiskCapDocScreen() { setDraftRows(mapped); syncDefaultCapFromRows(mapped); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "加载版本明细失败"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" })); setDetail(null); setDraftRows([]); syncDefaultCapFromRows([]); } finally { setLoadingDetail(false); } - }, []); + }, [t]); useEffect(() => { if (list.length === 0 || selectedId !== "") { @@ -187,19 +189,19 @@ export function RiskCapDocScreen() { return; } if (draftRows.length === 0) { - toast.error("至少保留一行封顶配置"); + toast.error("At least one cap row is required"); return; } for (const r of draftRows) { if (isDefaultRiskRow(r)) { if (r.cap_amount <= 0) { - toast.error("默认封顶金额必须大于 0"); + toast.error("Default cap amount must be greater than 0"); return; } continue; } if (!/^[0-9]{4}$/.test(r.normalized_number)) { - toast.error(`号码须为 4 位数字:${r.normalized_number}`); + toast.error(`Number must be 4 digits: ${r.normalized_number}`); return; } } @@ -222,10 +224,10 @@ export function RiskCapDocScreen() { })); setDraftRows(saved); syncDefaultCapFromRows(saved); - toast.success("已保存草稿"); + toast.success(t("versionActions.saveDraft", { ns: "config" })); void refreshList(); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "保存失败"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("wallet.saveFailed", { ns: "config" })); } finally { setSaving(false); } @@ -248,11 +250,11 @@ export function RiskCapDocScreen() { })); setDraftRows(pub); syncDefaultCapFromRows(pub); - toast.success("已启用为当前版本"); + toast.success(t("versionActions.publishCurrent", { ns: "config" })); void refreshList(); setSelectedId(String(d.id)); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "发布失败"); + toast.error(e instanceof LotteryApiBizError ? e.message : "Publish failed"); } finally { setSaving(false); } @@ -266,7 +268,7 @@ export function RiskCapDocScreen() { reason: `draft ${new Date().toISOString()}`, clone_from_version_id: active?.id ?? null, }); - toast.success(`已创建草稿 v${d.version_no}`); + toast.success(`Created draft v${d.version_no}`); await refreshList(); setSelectedId(String(d.id)); setDetail(d); @@ -280,7 +282,7 @@ export function RiskCapDocScreen() { setDraftRows(nd); syncDefaultCapFromRows(nd); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "创建草稿失败"); + toast.error(e instanceof LotteryApiBizError ? e.message : "Create draft failed"); } finally { setSaving(false); } @@ -289,7 +291,7 @@ export function RiskCapDocScreen() { function applyDefaultCap() { const n = Number.parseInt(defaultCapStr, 10); if (!Number.isFinite(n) || n <= 0) { - toast.error("请输入有效的封顶金额"); + toast.error("Enter a valid cap amount"); return; } setDraftRows((prev) => { @@ -297,7 +299,7 @@ export function RiskCapDocScreen() { return [defaultRiskRowFromAmount(n), ...next]; }); setSyncOpen(false); - toast.message("已写入本地草稿,记得保存草稿"); + toast.message("Saved into local draft. Save the draft to persist it."); } const occFiltered = useMemo(() => { @@ -316,10 +318,10 @@ export function RiskCapDocScreen() { async function handleDeleteVersion(row: ConfigVersionSummary) { try { await deleteRiskCapVersion(row.id); - toast.success("已删除该版本"); + toast.success(t("versionSwitcher.delete", { ns: "config" })); await refreshList(); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "删除失败"); + toast.error(e instanceof LotteryApiBizError ? e.message : "Delete failed"); throw e; } } @@ -328,11 +330,11 @@ export function RiskCapDocScreen() { - 风控封顶 + {t("nav.items.risk-cap", { ns: "config" })} {detail ? ( {" "} - · 版本 v{detail.version_no} + · v{detail.version_no} ) : null} @@ -344,7 +346,7 @@ export function RiskCapDocScreen() { selectedId={selectedId} onSelectedIdChange={setSelectedId} loading={loadingList} - sheetTitle="风控封顶版本" + sheetTitle={`${t("nav.items.risk-cap", { ns: "config" })} ${t("versionSwitcher.sheetTitle", { ns: "config" })}`} onDeleteVersion={handleDeleteVersion} className="w-auto min-w-0" /> @@ -362,9 +364,9 @@ export function RiskCapDocScreen() { {detail ? (

- 生效时间:{detail.effective_at ? formatDt(detail.effective_at) : "—"} · 备注:{detail.reason ?? "—"} + Effective at: {detail.effective_at ? formatDt(detail.effective_at) : "—"} · Note: {detail.reason ?? "—"} {!isDraft ? ( - — 只读,请先新建草稿。 + - Read only. Create a draft first. ) : null}

) : null} @@ -373,13 +375,13 @@ export function RiskCapDocScreen() { {error ?

{error}

: null}
-

默认封顶

+

Default Cap

- 未设置特殊封顶的号码,将使用此默认封顶模板。 + Numbers without a special cap use this default cap template.

- + {isDraft ? ( {isDraft ? ( ) : null}
@@ -406,7 +408,7 @@ export function RiskCapDocScreen() {
-

特殊封顶

+

Special Caps

{isDraft ? ( ) : null}
{loadingDetail ? ( -

加载明细…

+

Loading details…

) : specialRows.length === 0 ? ( -

无明细行。

+

No detail rows.

) : (
- 号码 - 封顶金额 - 已占用 - 剩余 - 售罄 - 操作 + Number + Cap Amount + Used + Remaining + Sold Out + Actions @@ -485,10 +487,10 @@ export function RiskCapDocScreen() { disabled={saving} onClick={() => removeRow(idx)} > - 删除 + {t("actions.delete", { ns: "adminUsers" })} ) : ( - 只读 + Read only )} @@ -500,42 +502,42 @@ export function RiskCapDocScreen() {
-

全部号码占用情况

+

All Number Occupancy

- 占位界面:筛选与导出待接入注单汇总;下列数据仍来源于当前草稿号码列表。 + Placeholder view: filters and exports still need ticket-summary integration. Data below still comes from the current draft list.

- + setOccSearch(e.target.value)} />
-
- 号码 - 已占用 - 剩余 - 占比 - 售罄 - 操作 + Number + Used + Remaining + Ratio + Sold Out + Actions @@ -548,7 +550,7 @@ export function RiskCapDocScreen() { @@ -562,17 +564,17 @@ export function RiskCapDocScreen() { - 同步默认封顶 + Sync Default Cap - 将把默认封顶模板设为 {defaultCapStr || "(空)"}。此操作仅修改草稿,确认后请保存草稿并发布。 + The default cap template will be set to {defaultCapStr || "(empty)"}. This only changes the draft. Save and publish after confirming. diff --git a/src/modules/config/doc/wallet-config-doc-screen.tsx b/src/modules/config/doc/wallet-config-doc-screen.tsx index 7a0f353..7c001e8 100644 --- a/src/modules/config/doc/wallet-config-doc-screen.tsx +++ b/src/modules/config/doc/wallet-config-doc-screen.tsx @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { @@ -42,6 +43,7 @@ interface Draft { } export function WalletConfigDocScreen() { + const { t } = useTranslation(["config", "adminUsers"]); const [draft, setDraft] = useState({ inMin: "", inMax: "", @@ -71,11 +73,11 @@ export function WalletConfigDocScreen() { setSaved(d); setDirty(false); } catch { - toast.error("加载失败"); + toast.error(t("wallet.loadFailed", { ns: "config" })); } finally { setLoading(false); } - }, []); + }, [t]); useEffect(() => { queueMicrotask(() => { @@ -95,11 +97,13 @@ export function WalletConfigDocScreen() { await updateAdminSetting(KEYS.IN_MAX, displayToMinorUnits(draft.inMax)); await updateAdminSetting(KEYS.OUT_MIN, displayToMinorUnits(draft.outMin)); await updateAdminSetting(KEYS.OUT_MAX, displayToMinorUnits(draft.outMax)); - toast.success("保存成功"); + toast.success(t("wallet.saveSuccess", { ns: "config" })); setSaved(draft); setDirty(false); } catch (error) { - toast.error(error instanceof LotteryApiBizError ? error.message : "保存失败"); + toast.error( + error instanceof LotteryApiBizError ? error.message : t("wallet.saveFailed", { ns: "config" }), + ); } finally { setSaving(false); } @@ -108,81 +112,81 @@ export function WalletConfigDocScreen() { return ( - 钱包转账限额配置 + {t("wallet.title", { ns: "config" })}

- 金额单位为游戏币种最小单位(如 NPR 下 100 = 1.00 NPR)。最小金额至少为 1 最小单位。 + {t("wallet.description", { ns: "config" })}

- + handleChange("inMin", e.target.value)} disabled={loading || saving} />

- 主站钱包转入彩票钱包的单笔下限 + {t("wallet.hints.inMin", { ns: "config" })}

- + handleChange("inMax", e.target.value)} disabled={loading || saving} />

- 主站钱包转入彩票钱包的单笔上限 + {t("wallet.hints.inMax", { ns: "config" })}

- + handleChange("outMin", e.target.value)} disabled={loading || saving} />

- 彩票钱包转出主站钱包的单笔下限 + {t("wallet.hints.outMin", { ns: "config" })}

- + handleChange("outMax", e.target.value)} disabled={loading || saving} />

- 彩票钱包转出主站钱包的单笔上限 + {t("wallet.hints.outMax", { ns: "config" })}

{dirty && ( )}
diff --git a/src/modules/config/meta.ts b/src/modules/config/meta.ts index 26c6394..b6dd4fb 100644 --- a/src/modules/config/meta.ts +++ b/src/modules/config/meta.ts @@ -1,29 +1,29 @@ export const configHubMeta = { - title: "配置中心", - description: "统一管理玩法目录、赔率、回水和风险封顶,先草稿、后发布、再生效。", + title: "Configuration Center", + description: "Manage play catalogs, odds, rebates, and risk caps with draft, publish, and activation stages.", } as const; export const configPlayConfigMeta = { - title: "玩法配置", - description: "维护玩法开关、限额和规则文案,目录变更会直接影响下注入口。", + title: "Play Configuration", + description: "Manage play switches, limits, and rule text. Catalog changes directly affect betting entry points.", } as const; export const configOddsMeta = { - title: "赔率配置", - description: "维护赔率、返水和佣金,发布前请重点核对数值范围与币种。", + title: "Odds Configuration", + description: "Manage odds, rebates, and commissions. Verify ranges and currency before publishing.", } as const; export const configRebateMeta = { - title: "佣金 / 回水", - description: "从赔率草稿中批量调整回水比例,适合按玩法维度统一修正。", + title: "Commission / Rebate", + description: "Batch-adjust rebate rates from the odds draft, suitable for dimension-wide updates.", } as const; export const configRiskCapMeta = { - title: "风控封顶", - description: "管理号码封顶版本和风险池阈值,发布前先确认号码与期号。", + title: "Risk Caps", + description: "Manage number cap versions and risk pool thresholds. Confirm number scope and draw before publishing.", } as const; export const configWalletMeta = { - title: "钱包配置", - description: "维护钱包相关阈值与转账策略。", + title: "Wallet Configuration", + description: "Manage wallet thresholds and transfer policies.", } as const; diff --git a/src/modules/dashboard/dashboard-console.tsx b/src/modules/dashboard/dashboard-console.tsx index c6df38d..b3e789e 100644 --- a/src/modules/dashboard/dashboard-console.tsx +++ b/src/modules/dashboard/dashboard-console.tsx @@ -4,6 +4,7 @@ import Link from "next/link"; import { useCallback, useEffect, useMemo, useState, type ReactElement, type ReactNode } from "react"; import { format } from "date-fns"; import { zhCN } from "date-fns/locale"; +import { useTranslation } from "react-i18next"; import { AlertTriangle, ClipboardList, @@ -30,7 +31,7 @@ import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance import type { AdminRiskPoolRow } from "@/types/api/admin-risk"; import type { DrawCurrentSnapshot } from "@/types/api/public-draw"; -type HotPlayTab = "4D" | "3D" | "2D" | "特别"; +type HotPlayTab = "4D" | "3D" | "2D" | "special"; type SoldOutBuckets = { d4: number; @@ -66,7 +67,7 @@ function formatSignedMoneyMinor(minor: number, currencyCode: string | null): str return `${s}${formatMoneyMinor(Math.abs(minor), currencyCode)}`; } -/** 与后端 {@see AdminDashboardSnapshotBuilder::soldOutBucketKey} 维度对齐 */ +/** Aligned with the bucket dimensions used by AdminDashboardSnapshotBuilder::soldOutBucketKey. */ function poolPlayCategory(normalizedNumber: string): HotPlayTab | "other" { const raw = normalizedNumber.trim(); const digits = raw.replace(/\D/g, ""); @@ -74,7 +75,7 @@ function poolPlayCategory(normalizedNumber: string): HotPlayTab | "other" { const hasLetter = /[A-Za-z]/.test(raw); if (hasLetter && digitLen < 3) { - return "特别"; + return "special"; } if (digitLen >= 4) { return "4D"; @@ -86,7 +87,7 @@ function poolPlayCategory(normalizedNumber: string): HotPlayTab | "other" { return "2D"; } if (hasLetter) { - return "特别"; + return "special"; } return "other"; } @@ -99,6 +100,7 @@ function topPoolsForTab(pools: AdminRiskPoolRow[], tab: HotPlayTab): AdminRiskPo } function RiskSemiGauge({ pct }: { pct: number }): ReactElement { + const { t } = useTranslation("dashboard"); const v = Math.min(100, Math.max(0, pct)); const r = 76; const arcLen = Math.PI * r; @@ -126,13 +128,14 @@ function RiskSemiGauge({ pct }: { pct: number }): ReactElement {

{v.toFixed(2)}%

-

封顶占用

+

{t("capUsage")}

); } function HotBarChart({ rows }: { rows: AdminRiskPoolRow[] }): ReactElement { + const { t } = useTranslation("dashboard"); const maxU = Math.max(0.0001, ...rows.map((r) => r.usage_ratio ?? 0)); return ( @@ -142,10 +145,10 @@ function HotBarChart({ rows }: { rows: AdminRiskPoolRow[] }): ReactElement { className="pointer-events-none absolute bottom-6 left-0 top-2 w-6 rotate-180 text-[10px] leading-tight text-muted-foreground [writing-mode:vertical-rl]" aria-hidden > - 占用率 + {t("capUsage")} {rows.length === 0 ? ( -

该维度暂无池数据

+

{t("noPoolData")}

) : ( rows.map((row) => { const u = row.usage_ratio ?? 0; @@ -170,25 +173,26 @@ function HotBarChart({ rows }: { rows: AdminRiskPoolRow[] }): ReactElement { }) )} -

号码(按占用率)

+

{t("numbersByUsage")}

); } function SoldOutDonut({ buckets }: { buckets: SoldOutBuckets }): ReactElement { + const { t } = useTranslation("dashboard"); const entries: { key: keyof SoldOutBuckets; label: string; color: string }[] = [ - { key: "d4", label: "4D", color: "oklch(0.32 0.08 260)" }, - { key: "d3", label: "3D", color: "oklch(0.48 0.12 250)" }, - { key: "d2", label: "2D", color: "oklch(0.78 0.14 95)" }, - { key: "special", label: "特别号", color: "oklch(0.55 0.22 25)" }, - { key: "other", label: "其他", color: "oklch(0.62 0.16 145)" }, + { key: "d4", label: t("soldOutBuckets.d4"), color: "oklch(0.32 0.08 260)" }, + { key: "d3", label: t("soldOutBuckets.d3"), color: "oklch(0.48 0.12 250)" }, + { key: "d2", label: t("soldOutBuckets.d2"), color: "oklch(0.78 0.14 95)" }, + { key: "special", label: t("soldOutBuckets.special"), color: "oklch(0.55 0.22 25)" }, + { key: "other", label: t("soldOutBuckets.other"), color: "oklch(0.62 0.16 145)" }, ]; const total = entries.reduce((s, e) => s + buckets[e.key], 0); if (total === 0) { return (
-

暂无售罄号码

+

{t("noSoldOutNumbers")}

); } @@ -227,7 +231,7 @@ function SoldOutDonut({ buckets }: { buckets: SoldOutBuckets }): ReactElement { />

{total}

-

售罄合计

+

{t("soldOutTotal")}

    @@ -246,6 +250,7 @@ function SoldOutDonut({ buckets }: { buckets: SoldOutBuckets }): ReactElement { } export function DashboardConsole(): ReactElement { + const { t } = useTranslation(["dashboard", "common"]); const [todayLabel] = useState(() => format(new Date(), "yyyy-MM-dd EEEE", { locale: zhCN })); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); @@ -304,21 +309,21 @@ export function DashboardConsole(): ReactElement { const noticeParts: string[] = d.warnings.map((w) => w.message); if (d.resolved_draw != null && !d.capabilities.draw_finance_risk) { - noticeParts.push("当前账号无开奖查看/管理权限,财务与风控数据未返回。"); + noticeParts.push(t("warnings.drawPermission")); } if (d.hall != null && !d.capabilities.wallet_transfer_view) { - noticeParts.push("当前账号无钱包对账查看权限,异常转账计数未返回。"); + noticeParts.push(t("warnings.walletPermission")); } setNotice(noticeParts.length > 0 ? noticeParts.join(" ") : null); } catch (e) { const msg = - e instanceof LotteryApiBizError ? e.message : "加载失败,请检查 API 与登录状态。"; + e instanceof LotteryApiBizError ? e.message : t("warnings.loadFailed"); setError(msg); } finally { setLoading(false); setRefreshing(false); } - }, []); + }, [t]); useEffect(() => { const t = window.setTimeout(() => { @@ -335,28 +340,27 @@ export function DashboardConsole(): ReactElement { const hallStatusLabel = hall?.status ?? "—"; const isOpenLike = hallStatusLabel.toLowerCase().includes("open") || - hallStatusLabel.includes("开售") || - hallStatusLabel.includes("开放"); + hallStatusLabel.toLowerCase().includes("sale"); const quickLinks: { href: string; label: string; icon: ReactNode }[] = [ - { href: "/admin/draws", label: "创建期计划", icon: }, - { href: "/admin/draws", label: "开售 / 期号", icon: }, + { href: "/admin/draws", label: t("quickLinks.createDrawPlan"), icon: }, + { href: "/admin/draws", label: t("quickLinks.drawSchedule"), icon: }, { href: drawId != null ? `/admin/draws/${drawId}/results` : "/admin/draws", - label: "开奖结果", + label: t("quickLinks.results"), icon: , }, - { href: "/admin/tickets", label: "注单管理", icon: }, - { href: "/admin/wallet/transactions", label: "钱包流水", icon: }, - { href: "/admin/reports", label: "报表中心", icon: }, - { href: "/admin/audit-logs", label: "审计日志", icon: }, + { href: "/admin/tickets", label: t("quickLinks.tickets"), icon: }, + { href: "/admin/wallet/transactions", label: t("quickLinks.walletTransactions"), icon: }, + { href: "/admin/reports", label: t("quickLinks.reports"), icon: }, + { href: "/admin/audit-logs", label: t("quickLinks.auditLogs"), icon: }, ]; return (
    -

    仪表盘

    +

    {t("title")}

    {todayLabel} @@ -369,26 +373,26 @@ export function DashboardConsole(): ReactElement { onClick={() => void load(true)} > - 刷新 + {t("refresh")}
    {error ? ( - 提示 + {t("notice")} {error} ) : null} {notice && !error ? ( - 提示 + {t("notice")} {notice} ) : null} - {/* Row 1 — 核心财务 KPI */} + {/* Row 1 - Core finance KPI */}
    {loading ? ( Array.from({ length: 3 }).map((_, i) => ( @@ -407,13 +411,13 @@ export function DashboardConsole(): ReactElement {
    -

    当期投注总额

    +

    {t("todayBetTotal")}

    {finance ? formatMoneyMinor(finance.total_bet_minor, currency) : "—"}

    - 当前大厅期财务汇总 + {t("currentDrawFinanceSummary")}

    @@ -424,13 +428,13 @@ export function DashboardConsole(): ReactElement {
    -

    当期派彩

    +

    {t("currentPayout")}

    {finance ? formatMoneyMinor(finance.total_payout_minor, currency) : "—"}

    - 中奖派彩 + Jackpot + {t("payoutSummary")}

    @@ -441,13 +445,13 @@ export function DashboardConsole(): ReactElement {
    -

    当期平台盈亏

    +

    {t("currentProfit")}

    {finance ? formatSignedMoneyMinor(finance.approx_house_gross_minor, currency) : "—"}

    - 投注 − 派彩(近似) + {t("profitFormula")}

    @@ -456,7 +460,7 @@ export function DashboardConsole(): ReactElement { )} - {/* Row 2 — 期号 / 投注 / 风险表 */} + {/* Row 2 - Draw / betting / risk */}
    {loading ? ( Array.from({ length: 3 }).map((_, i) => ( @@ -472,10 +476,10 @@ export function DashboardConsole(): ReactElement {
    -

    当前期号

    +

    {t("currentDraw")}

    {hall?.draw_no ?? "—"}

    - 第 {hall?.sequence_no ?? "—"} 期 + {t("drawSequence", { sequence: hall?.sequence_no ?? "—" })} · - 期号详情 + {t("drawDetails")} ) : null}

    @@ -507,12 +511,12 @@ export function DashboardConsole(): ReactElement {
    -

    本期注单笔数

    +

    {t("ticketCount")}

    {finance != null ? finance.ticket_item_count.toLocaleString("zh-CN") : "—"}

    - 关联投注额{" "} + {t("relatedBetAmount")}{" "} {finance ? formatMoneyMinor(finance.total_bet_minor, currency) : "—"} @@ -526,10 +530,12 @@ export function DashboardConsole(): ReactElement {

    -

    风险封顶占用

    +

    {t("riskCapUsage")}

    - 已占用 {formatMoneyMinor(riskLocked, currency)} / 封顶{" "} - {formatMoneyMinor(riskCap, currency)} + {t("lockedAndCap", { + locked: formatMoneyMinor(riskLocked, currency), + cap: formatMoneyMinor(riskCap, currency), + })}

    @@ -542,7 +548,7 @@ export function DashboardConsole(): ReactElement { )} href={`/admin/risk/draws/${drawId}/occupancy`} > - 占用明细 + {t("occupancyDetails")} ) : null}
    @@ -552,30 +558,35 @@ export function DashboardConsole(): ReactElement { )}
    - {/* Row 3 — 图表 */} + {/* Row 3 - Charts */}
    - 热门号码 Top 10 + {t("hotNumbersTop10")}
    -
    - {(["4D", "3D", "2D", "特别"] as const).map((tab) => ( +
    + {([ + { value: "4D", label: t("tabs.4d") }, + { value: "3D", label: t("tabs.3d") }, + { value: "2D", label: t("tabs.2d") }, + { value: "special", label: t("tabs.special") }, + ] as const).map((tab) => ( ))}
    @@ -587,7 +598,7 @@ export function DashboardConsole(): ReactElement { "h-7 border-[#2563eb]/30 px-2 text-xs text-[#2563eb] hover:bg-[#2563eb]/5", )} > - 查看全部 + {t("actions.viewAll", { ns: "common" })} ) : null}
    @@ -604,7 +615,7 @@ export function DashboardConsole(): ReactElement {
    - 售罄分布 + {t("soldOutDistribution")}
    {drawId != null ? ( - 查看全部 + {t("actions.viewAll", { ns: "common" })} ) : null}
    @@ -624,13 +635,13 @@ export function DashboardConsole(): ReactElement { ) : soldOutBuckets ? ( ) : ( -

    暂无数据

    +

    {t("states.noData", { ns: "common" })}

    )}
    - {/* Row 4 — 待办 */} + {/* Row 4 - To-do */}
    @@ -638,7 +649,7 @@ export function DashboardConsole(): ReactElement {
    -

    待审核开奖

    +

    {t("pendingReviewResults")}

    {pendingReview ?? "—"}

    @@ -652,7 +663,7 @@ export function DashboardConsole(): ReactElement { "shrink-0 border-[#c41e3a] text-[#c41e3a] hover:bg-[#c41e3a]/5", )} > - 立即审核 + {t("actions.reviewNow", { ns: "common" })} ) : null}
    @@ -662,7 +673,7 @@ export function DashboardConsole(): ReactElement {
    -

    异常转账单

    +

    {t("abnormalTransferOrders")}

    {abnormalTransferTotal ?? "—"}

    @@ -675,12 +686,12 @@ export function DashboardConsole(): ReactElement { "shrink-0 border-[#c41e3a] text-[#c41e3a] hover:bg-[#c41e3a]/5", )} > - 查看转账单 + {t("viewTransferOrders")}
    - {/* Row 5 — 快捷入口 */} + {/* Row 5 - Quick links */} {quickLinks.map((q) => ( diff --git a/src/modules/dashboard/meta.ts b/src/modules/dashboard/meta.ts index c1f3592..f7437cc 100644 --- a/src/modules/dashboard/meta.ts +++ b/src/modules/dashboard/meta.ts @@ -1,5 +1,5 @@ export const dashboardModuleMeta = { segment: "dashboard", - title: "仪表盘", + title: "Dashboard", description: "", } as const; diff --git a/src/modules/draws/draw-detail-console.tsx b/src/modules/draws/draw-detail-console.tsx index 7cbc6be..8bd0c78 100644 --- a/src/modules/draws/draw-detail-console.tsx +++ b/src/modules/draws/draw-detail-console.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { @@ -37,6 +38,7 @@ function Field({ label, children }: { label: string; children: React.ReactNode } } export function DrawDetailConsole({ drawId }: { drawId: string }) { + const { t } = useTranslation(["draws", "common"]); const idNum = Number(drawId); const profile = useAdminProfile(); const canManageDraw = adminHasAnyPermission(profile?.permissions, [PRD_DRAW_RESULT_MANAGE]); @@ -49,7 +51,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) { const load = useCallback(async () => { if (!Number.isFinite(idNum)) { - setError("无效的期号 ID"); + setError(t("invalidDrawId")); setLoading(false); return; } @@ -59,21 +61,21 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) { setData(await getAdminDraw(idNum)); } catch (e) { setData(null); - setError(e instanceof LotteryApiBizError ? e.message : "加载失败"); + setError(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" })); } finally { setLoading(false); } - }, [idNum]); + }, [idNum, t]); async function runAction(name: string, action: () => Promise): Promise { if (!Number.isFinite(idNum)) return; setActing(name); try { await action(); - toast.success(`${name}成功`); + toast.success(t("actionSuccess", { name })); await load(); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : `${name}失败`); + toast.error(e instanceof LotteryApiBizError ? e.message : t("actionFailed", { name })); } finally { setActing(null); } @@ -87,11 +89,11 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) { }, [load]); if (loading && !data) { - return

    加载中…

    ; + return

    {t("states.loading", { ns: "common" })}

    ; } if (error || !data) { - return

    {error ?? "无数据"}

    ; + return

    {error ?? t("states.noData", { ns: "common" })}

    ; } return ( @@ -101,46 +103,49 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) {
    {data.draw_no} -

    开奖详情

    +

    {t("drawDetail")}

    - +
    - {data.business_date} - {data.sequence_no} - {formatDt(data.start_time)} - {formatDt(data.close_time)} - {formatDt(data.draw_time)} - {formatDt(data.cooling_end_time)} + {data.business_date} + {data.sequence_no} + {formatDt(data.start_time)} + {formatDt(data.close_time)} + {formatDt(data.draw_time)} + {formatDt(data.cooling_end_time)}
    - {data.result_source ?? "—"} - {data.current_result_version} - {data.settle_version} - {data.is_reopened ? "是" : "否"} + {data.result_source ?? "—"} + {data.current_result_version} + {data.settle_version} + {data.is_reopened ? t("yes") : t("no")}
    -

    批次统计

    +

    {t("batchStats")}

    - 总批次 + {t("batchTotal")} {data.result_batch_counts.total}
    - 待审核 + {t("pendingReview")} {data.result_batch_counts.pending_review}
    - 已发布 + {t("published")} {data.result_batch_counts.published}
    @@ -148,7 +153,7 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) { href={`/admin/draws/${drawId}/finance`} className={cn(buttonVariants({ variant: "outline", size: "sm" }), "mt-4 w-full")} > - 查看期号收支 + {t("viewFinance")}
    @@ -156,9 +161,9 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) { - 期号操作 + {t("drawActions")}

    - 手动封盘 / 取消 / RNG / 重开 / 触发结算均直接调用后台接口。 + {t("drawActionsDesc")}

    @@ -166,42 +171,42 @@ export function DrawDetailConsole({ drawId }: { drawId: string }) { type="button" variant="secondary" disabled={!canManageDraw || acting !== null || !["pending", "open"].includes(data.status)} - onClick={() => void runAction("手动封盘", () => postAdminManualCloseDraw(idNum))} + onClick={() => void runAction(t("manualClose"), () => postAdminManualCloseDraw(idNum))} > - {acting === "手动封盘" ? "处理中…" : "手动封盘"} + {acting === t("manualClose") ? t("processing") : t("manualClose")} {isSuperAdmin ? ( ) : null}
    diff --git a/src/modules/draws/draw-finance-console.tsx b/src/modules/draws/draw-finance-console.tsx index 94f59d6..311bc16 100644 --- a/src/modules/draws/draw-finance-console.tsx +++ b/src/modules/draws/draw-finance-console.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { getAdminDrawFinanceSummary } from "@/api/admin-draws"; import { postAdminRunDrawSettlement } from "@/api/admin-settlement"; @@ -21,6 +22,7 @@ import type { AdminDrawFinanceSummaryData } from "@/types/api/admin-draw-finance import { toast } from "sonner"; export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactElement { + const { t } = useTranslation(["draws", "common"]); const idNum = Number(drawId); const [data, setData] = useState(null); const [err, setErr] = useState(null); @@ -29,7 +31,7 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE const load = useCallback(async () => { if (!Number.isFinite(idNum) || idNum < 1) { - setErr("无效的期号 ID"); + setErr(t("invalidDrawId")); setLoading(false); return; } @@ -38,22 +40,22 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE try { setData(await getAdminDrawFinanceSummary(idNum)); } catch (e) { - setErr(e instanceof LotteryApiBizError ? e.message : "加载失败"); + setErr(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" })); setData(null); } finally { setLoading(false); } - }, [idNum]); + }, [idNum, t]); async function runSettlement(): Promise { if (!Number.isFinite(idNum) || idNum < 1) return; setSettling(true); try { const res = await postAdminRunDrawSettlement(idNum); - toast.success(res.ran ? "已触发结算" : "当前状态不可结算或已处理"); + toast.success(res.ran ? t("runSettlement") : t("status")); await load(); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "触发结算失败"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("actionFailed", { name: t("runSettlement") })); } finally { setSettling(false); } @@ -66,44 +68,44 @@ export function DrawFinanceConsole({ drawId }: { drawId: string }): React.ReactE }, [load]); if (loading && !data) { - return

    加载中…

    ; + return

    {t("states.loading", { ns: "common" })}

    ; } if (err || !data) { - return

    {err ?? "无数据"}

    ; + return

    {err ?? t("states.noData", { ns: "common" })}

    ; } return (
    - 期号收支概览 + {t("financeOverview")}
    - 期号 + {t("drawNo")}

    {data.draw_no}

    - 状态 + {t("status")}

    {data.draw_status}

    - 订单数 / 注项数 + {t("orderAndItemCount")}

    {data.order_count} / {data.ticket_item_count}

    - 当期实扣投注 + {t("actualBet")}

    {data.total_bet_minor}

    - 当期派彩合计 + {t("currentPayout")}

    {data.total_payout_minor}

    - 近似毛损益 + {t("grossProfit")}

    - 结算批次列表(按期号筛选) + {t("settlementBatchList")}

    - 本关联期结算批次 + {t("relatedSettlementBatches")} {data.settlement_batches.length === 0 ? ( -

    暂无结算批次记录。

    +

    {t("noSettlementBatches")}

    ) : (
ID - 状态 - 票数 - 中奖数 - 派彩 - Jackpot - 完成时间 + {t("status")} + {t("ticketCount")} + {t("winCount")} + {t("payoutTotal")} + {t("jackpot")} + {t("finishedAt")} diff --git a/src/modules/draws/draw-publish-console.tsx b/src/modules/draws/draw-publish-console.tsx index 3a29a17..3f392d2 100644 --- a/src/modules/draws/draw-publish-console.tsx +++ b/src/modules/draws/draw-publish-console.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { getAdminDrawResultBatches, postAdminPublishResultBatch } from "@/api/admin-draws"; @@ -25,6 +26,7 @@ import type { AdminDrawBatchRow, AdminDrawBatchesData } from "@/types/api/admin- import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd"; export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchId: string }) { + const { t } = useTranslation(["draws", "common"]); const profile = useAdminProfile(); const canManageDraw = adminHasAnyPermission(profile?.permissions, [ PRD_DRAW_RESULT_MANAGE, @@ -38,7 +40,7 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI const load = useCallback(async () => { if (!Number.isFinite(idNum)) { - setError("无效的期号 ID"); + setError(t("invalidDrawId")); setLoading(false); return; } @@ -48,11 +50,11 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI setData(await getAdminDrawResultBatches(idNum)); } catch (e) { setData(null); - setError(e instanceof LotteryApiBizError ? e.message : "加载失败"); + setError(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" })); } finally { setLoading(false); } - }, [idNum]); + }, [idNum, t]); useEffect(() => { const timer = window.setTimeout(() => { @@ -71,10 +73,10 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI setPublishing(true); try { const res = await postAdminPublishResultBatch(idNum, batchNum); - toast.success(`已发布 · ${res.draw_no} · 状态 ${res.status}`); + toast.success(t("publishSuccess", { drawNo: res.draw_no, status: res.status })); await load(); } catch (e) { - const msg = e instanceof LotteryApiBizError ? e.message : "发布失败"; + const msg = e instanceof LotteryApiBizError ? e.message : t("publishFailed"); toast.error(msg); } finally { setPublishing(false); @@ -82,18 +84,18 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI } if (loading && !data) { - return

加载中…

; + return

{t("states.loading", { ns: "common" })}

; } if (error || !data) { - return

{error ?? "无数据"}

; + return

{error ?? t("states.noData", { ns: "common" })}

; } if (!batch) { return ( - 未找到批次 - 请返回审核列表确认 batch id。 + {t("batchNotFound")} + {t("batchNotFoundDesc")} ); } @@ -105,31 +107,31 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
- ← 审核队列 + ← {t("backToReviewQueue")}
- 发布 + {t("publishTitle")} {!canManageDraw ? ( - 无发布权限 - 当前账号不可执行发布。 + {t("noPublishPermission")} + {t("noPublishPermission")} ) : null} {!canPublish && canManageDraw ? ( - 不可发布 - 当前批次状态为「{batch.status}」。 + {t("cannotPublish")} + {t("cannotPublishDesc", { status: batch.status })} ) : null} {canPublish ? ( - 请核对以下号码后再发布 - 确认无误后点击发布。 + {t("checkBeforePublish")} + {t("checkBeforePublishDesc")} ) : null} @@ -137,7 +139,7 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI
- 奖项 + {t("prize")} # 4D @@ -155,8 +157,11 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI

- 生成方式:{batch.source_type === "manual" ? "人工录入" : "RNG 自动生成"} · 号码条数: - {batch.items.length}/23 · RNG 摘要:{batch.rng_seed_hash ?? "—"} + {t("sourceTypeFull", { + source: batch.source_type === "manual" ? t("manualEntry") : t("rngAutoGenerate"), + count: batch.items.length, + hash: batch.rng_seed_hash ?? "—", + })}

@@ -164,14 +169,14 @@ export function DrawPublishConsole({ drawId, batchId }: { drawId: string; batchI href={`/admin/draws/${drawId}/results`} className={cn(buttonVariants({ variant: "outline", size: "default" }))} > - 查看已发布展示 + {t("publishedView")} diff --git a/src/modules/draws/draw-results-console.tsx b/src/modules/draws/draw-results-console.tsx index 8706f09..eee06b9 100644 --- a/src/modules/draws/draw-results-console.tsx +++ b/src/modules/draws/draw-results-console.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { getAdminDrawResultBatches } from "@/api/admin-draws"; import { buttonVariants } from "@/components/ui/button"; @@ -24,6 +25,7 @@ import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd"; import { DrawStatusBadge } from "./draw-status-badge"; export function DrawResultsConsole({ drawId }: { drawId: string }) { + const { t } = useTranslation(["draws", "common"]); const profile = useAdminProfile(); const canManageDraw = adminHasAnyPermission(profile?.permissions, [ PRD_DRAW_RESULT_MANAGE, @@ -35,7 +37,7 @@ export function DrawResultsConsole({ drawId }: { drawId: string }) { const load = useCallback(async () => { if (!Number.isFinite(idNum)) { - setError("无效的期号 ID"); + setError(t("invalidDrawId")); setLoading(false); return; } @@ -45,11 +47,11 @@ export function DrawResultsConsole({ drawId }: { drawId: string }) { setData(await getAdminDrawResultBatches(idNum)); } catch (e) { setData(null); - setError(e instanceof LotteryApiBizError ? e.message : "加载失败"); + setError(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" })); } finally { setLoading(false); } - }, [idNum]); + }, [idNum, t]); useEffect(() => { const timer = window.setTimeout(() => { @@ -59,11 +61,11 @@ export function DrawResultsConsole({ drawId }: { drawId: string }) { }, [load]); if (loading && !data) { - return

加载中…

; + return

{t("states.loading", { ns: "common" })}

; } if (error || !data) { - return

{error ?? "无数据"}

; + return

{error ?? t("states.noData", { ns: "common" })}

; } const published = data.batches.filter((b) => b.status === "published"); @@ -72,23 +74,23 @@ export function DrawResultsConsole({ drawId }: { drawId: string }) {
-

开奖结果

+

{t("resultsTitle")}

- 期号 {data.draw_no} · + {t("drawNo")} {data.draw_no} ·

- {canManageDraw ? "去审核 / 发布" : "查看审核队列"} + {canManageDraw ? t("reviewAndPublish") : t("viewReviewQueue")}
{published.length === 0 ? ( - 暂无已发布批次。 + {t("noPublishedBatch")} ) : ( @@ -99,24 +101,29 @@ export function DrawResultsConsole({ drawId }: { drawId: string }) { } function BatchTable({ batch }: { batch: AdminDrawBatchRow }) { + const { t } = useTranslation("draws"); return ( - 版本 v{batch.result_version} + {t("version", { version: batch.result_version })}

- 生成方式 {batch.source_type === "manual" ? "人工录入" : "RNG"} · RNG 摘要 {batch.rng_seed_hash ?? "—"} · 确认时间 {batch.confirmed_at ?? "—"} + {t("sourceType", { + source: batch.source_type === "manual" ? t("manualEntry") : t("rng"), + })}{" "} + · {t("rngSummary", { hash: batch.rng_seed_hash ?? "—" })} ·{" "} + {t("confirmedAt", { time: batch.confirmed_at ?? "—" })}

- 奖项 + {t("prize")} # 4D - 尾3 - 尾2 - 头/尾 + {t("tail3")} + {t("tail2")} + {t("headTail")} diff --git a/src/modules/draws/draw-review-console.tsx b/src/modules/draws/draw-review-console.tsx index b765d24..66eef77 100644 --- a/src/modules/draws/draw-review-console.tsx +++ b/src/modules/draws/draw-review-console.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { getAdminDrawResultBatches, postAdminCreateManualResultBatch } from "@/api/admin-draws"; @@ -26,22 +27,25 @@ import { PRD_DRAW_RESULT_MANAGE } from "./draw-prd"; import { DrawStatusBadge } from "./draw-status-badge"; const RESULT_SLOTS = [ - { prize_type: "first", prize_index: 0, label: "头奖" }, - { prize_type: "second", prize_index: 0, label: "二奖" }, - { prize_type: "third", prize_index: 0, label: "三奖" }, + { prize_type: "first", prize_index: 0, label: "resultSlots.first" }, + { prize_type: "second", prize_index: 0, label: "resultSlots.second" }, + { prize_type: "third", prize_index: 0, label: "resultSlots.third" }, ...Array.from({ length: 10 }, (_, i) => ({ prize_type: "starter", prize_index: i, - label: `特别奖 ${i + 1}`, + label: `resultSlots.starter`, + labelIndex: i + 1, })), ...Array.from({ length: 10 }, (_, i) => ({ prize_type: "consolation", prize_index: i, - label: `安慰奖 ${i + 1}`, + label: `resultSlots.consolation`, + labelIndex: i + 1, })), ] as const; export function DrawReviewConsole({ drawId }: { drawId: string }) { + const { t } = useTranslation(["draws", "common"]); const profile = useAdminProfile(); const canManageDraw = adminHasAnyPermission(profile?.permissions, [ PRD_DRAW_RESULT_MANAGE, @@ -57,7 +61,7 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) { const load = useCallback(async () => { if (!Number.isFinite(idNum)) { - setError("无效的期号 ID"); + setError(t("invalidDrawId")); setLoading(false); return; } @@ -67,11 +71,11 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) { setData(await getAdminDrawResultBatches(idNum)); } catch (e) { setData(null); - setError(e instanceof LotteryApiBizError ? e.message : "加载失败"); + setError(e instanceof LotteryApiBizError ? e.message : t("errors.loadFailed", { ns: "common" })); } finally { setLoading(false); } - }, [idNum]); + }, [idNum, t]); useEffect(() => { const timer = window.setTimeout(() => { @@ -88,7 +92,7 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) { if (!Number.isFinite(idNum)) return; const invalid = manualNumbers.some((n) => !/^[0-9]{4}$/.test(n)); if (invalid) { - toast.error("请完整输入 23 组 4 位数字"); + toast.error(t("enter23Numbers")); return; } @@ -101,38 +105,46 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) { number_4d: manualNumbers[i], })), }); - toast.success(`已保存草稿 v${res.batch.result_version},等待确认发布`); + toast.success(t("draftSaved", { version: res.batch.result_version })); setManualNumbers(RESULT_SLOTS.map(() => "")); await load(); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "保存失败"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("saveFailed")); } finally { setSavingManual(false); } } if (loading && !data) { - return

加载中…

; + return

{t("states.loading", { ns: "common" })}

; } if (error || !data) { - return

{error ?? "无数据"}

; + return

{error ?? t("states.noData", { ns: "common" })}

; } return (
- 人工录入开奖结果 + {t("manualResultEntry")}

- 当前状态 · 保存后生成待确认批次,不会直接发布 + {t("currentStatusAndDraft", { + status: data.draw_status, + }).split(data.draw_status)[0]} + + {t("currentStatusAndDraft", { + status: data.draw_status, + }).split(data.draw_status)[1] ?? ""}

{RESULT_SLOTS.map((slot, i) => (
@@ -170,21 +182,21 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) { - 待确认批次 + {t("pendingBatches")} {pending.length === 0 ? (

- 当前没有待审核(pending_review)批次。 + {t("noPendingBatches")}

) : (
- 批次 ID - 版本 - 号码条数 - 操作 + {t("batchId")} + {t("version", { version: "" }).replace(" v", "").trim()} + {t("numberCount")} + {t("actions")} @@ -199,10 +211,10 @@ export function DrawReviewConsole({ drawId }: { drawId: string }) { href={`/admin/draws/${drawId}/publish/${b.id}`} className={cn(buttonVariants({ size: "sm" }))} > - 核对并发布 + {t("reviewAndPublishAction")} ) : ( - 无发布权限 + {t("noPublishPermission")} )} diff --git a/src/modules/draws/draw-subnav.tsx b/src/modules/draws/draw-subnav.tsx index fd1ad3b..705f433 100644 --- a/src/modules/draws/draw-subnav.tsx +++ b/src/modules/draws/draw-subnav.tsx @@ -2,15 +2,16 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useTranslation } from "react-i18next"; import { buttonVariants } from "@/components/ui/button"; import { cn } from "@/lib/utils"; const segments = [ - { suffix: "", key: "status", label: "期号状态" }, - { suffix: "/results", key: "results", label: "开奖结果" }, - { suffix: "/finance", key: "finance", label: "期号收支" }, - { suffix: "/review", key: "review", label: "审核与发布" }, + { suffix: "", key: "status", label: "subnav.status" }, + { suffix: "/results", key: "results", label: "subnav.results" }, + { suffix: "/finance", key: "finance", label: "subnav.finance" }, + { suffix: "/review", key: "review", label: "subnav.review" }, ] as const; function isReviewTabActive(pathname: string, base: string): boolean { @@ -24,6 +25,7 @@ function isReviewTabActive(pathname: string, base: string): boolean { } export function DrawSubnav({ drawId }: { drawId: string }) { + const { t } = useTranslation("draws"); const pathname = usePathname(); const base = `/admin/draws/${drawId}`; @@ -46,7 +48,7 @@ export function DrawSubnav({ drawId }: { drawId: string }) { buttonVariants({ variant: active ? "default" : "outline", size: "sm" }), )} > - {label} + {t(label)} ); })} diff --git a/src/modules/draws/draws-index-console.tsx b/src/modules/draws/draws-index-console.tsx index 557f953..9ddf485 100644 --- a/src/modules/draws/draws-index-console.tsx +++ b/src/modules/draws/draws-index-console.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { getAdminDraws, postAdminGenerateDrawPlan } from "@/api/admin-draws"; @@ -37,27 +38,29 @@ const DRAW_FILTER_ALL = "__all__"; /** 与 {@see App\Lottery\DrawStatus} 一致 */ const DRAW_STATUS_OPTIONS: { value: string; label: string }[] = [ - { value: "pending", label: "未开始" }, - { value: "open", label: "可下注" }, - { value: "closing", label: "封盘中" }, - { value: "closed", label: "已封盘待开奖" }, - { value: "drawing", label: "开奖处理中" }, - { value: "review", label: "待人工审核" }, - { value: "cooldown", label: "冷静期" }, - { value: "settling", label: "结算处理中" }, - { value: "settled", label: "已结算" }, - { value: "cancelled", label: "已取消" }, + { value: "pending", label: "statusOptions.pending" }, + { value: "open", label: "statusOptions.open" }, + { value: "closing", label: "statusOptions.closing" }, + { value: "closed", label: "statusOptions.closed" }, + { value: "drawing", label: "statusOptions.drawing" }, + { value: "review", label: "statusOptions.review" }, + { value: "cooldown", label: "statusOptions.cooldown" }, + { value: "settling", label: "statusOptions.settling" }, + { value: "settled", label: "statusOptions.settled" }, + { value: "cancelled", label: "statusOptions.cancelled" }, ]; -function drawAdminStatusSelectLabel(raw: unknown): string { +function drawAdminStatusSelectLabel(raw: unknown, t: (key: string) => string): string { const v = raw == null ? "" : String(raw); if (v === "" || v === DRAW_FILTER_ALL) { - return "不限"; + return t("statusOptions.all"); } - return DRAW_STATUS_OPTIONS.find((o) => o.value === v)?.label ?? v; + const key = DRAW_STATUS_OPTIONS.find((o) => o.value === v)?.label; + return key ? t(key) : v; } export function DrawsIndexConsole() { + const { t } = useTranslation(["draws", "common"]); const formatDt = useAdminDateTimeFormatter(); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); @@ -76,8 +79,9 @@ export function DrawsIndexConsole() { draftStatus === "" || !DRAW_STATUS_OPTIONS.some((o) => o.value === draftStatus) ? DRAW_FILTER_ALL : draftStatus, + t, ), - [draftStatus], + [draftStatus, t], ); const load = useCallback(async () => { @@ -96,22 +100,28 @@ export function DrawsIndexConsole() { setData(d); } catch (e) { const msg = - e instanceof LotteryApiBizError ? e.message : "加载失败,请检查登录与 API 配置"; + e instanceof LotteryApiBizError ? e.message : t("loadFailed"); setError(msg); setData(null); } finally { setLoading(false); } - }, [page, perPage, appliedDrawNo, appliedStatus]); + }, [page, perPage, appliedDrawNo, appliedStatus, t]); async function generatePlan(): Promise { setGenerating(true); try { const res = await postAdminGenerateDrawPlan(); - toast.success(`已生成 ${res.created} 期,当前缓冲 ${res.upcoming}/${res.buffer_target}`); + toast.success( + t("generateSuccess", { + created: res.created, + upcoming: res.upcoming, + target: res.buffer_target, + }), + ); await load(); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "生成失败"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("generateFailed")); } finally { setGenerating(false); } @@ -127,9 +137,9 @@ export function DrawsIndexConsole() { return ( - 期号列表 + {t("statusListTitle")} @@ -138,17 +148,17 @@ export function DrawsIndexConsole() { className="grid max-w-full gap-x-6 gap-y-3 sm:grid-cols-[minmax(0,12rem)_minmax(0,11rem)_auto] sm:gap-y-1.5" > setDraftDrawNo(e.target.value)} />
- 期号 - 开始时间 - 封盘时间 - 开奖时间 - 状态 - 下注总额 - 派彩总额 - 盈亏 - 操作 + {t("drawNo")} + {t("startTime")} + {t("closeTime")} + {t("drawTime")} + {t("status")} + {t("betTotal")} + {t("payoutTotal")} + {t("profitLoss")} + {t("actions")} {loading ? ( - 加载中… + {t("states.loading", { ns: "common" })} ) : data === null || data.items.length === 0 ? ( - 暂无数据 + {t("states.noData", { ns: "common" })} ) : ( @@ -264,7 +274,7 @@ export function DrawsIndexConsole() { href={`/admin/draws/${row.id}`} className={cn(buttonVariants({ variant: "outline", size: "sm" }))} > - 查看详情 + {t("viewDetails")} diff --git a/src/modules/draws/meta.ts b/src/modules/draws/meta.ts index 95db9b0..0d46f6b 100644 --- a/src/modules/draws/meta.ts +++ b/src/modules/draws/meta.ts @@ -1,5 +1,5 @@ export const drawsModuleMeta = { segment: "draws", - title: "期号列表", + title: "Draws", description: "", } as const; diff --git a/src/modules/jackpot/jackpot-pools-console.tsx b/src/modules/jackpot/jackpot-pools-console.tsx index 39acf03..6d6fb1d 100644 --- a/src/modules/jackpot/jackpot-pools-console.tsx +++ b/src/modules/jackpot/jackpot-pools-console.tsx @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { getAdminJackpotPools, @@ -53,6 +54,7 @@ function toDraft(p: AdminJackpotPoolRow): Draft { } export function JackpotPoolsConsole() { + const { t } = useTranslation(["jackpot", "common"]); const [items, setItems] = useState([]); const [drafts, setDrafts] = useState>({}); const [loading, setLoading] = useState(true); @@ -70,11 +72,11 @@ export function JackpotPoolsConsole() { } setDrafts(d); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "加载失败"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("loadFailed")); } finally { setLoading(false); } - }, []); + }, [t]); useEffect(() => { queueMicrotask(() => { @@ -107,10 +109,10 @@ export function JackpotPoolsConsole() { .filter(Boolean), status: Number.parseInt(d.status, 10), }); - toast.success("已保存"); + toast.success(t("saveSuccess")); await load(); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "保存失败"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("saveFailed")); } finally { setSavingId(null); } @@ -121,7 +123,7 @@ export function JackpotPoolsConsole() { if (!d) return; const drawId = Number.parseInt(d.manual_burst_draw_id, 10); if (!Number.isFinite(drawId) || drawId <= 0) { - toast.error("请填写有效的期号 ID"); + toast.error(t("invalidDrawId")); return; } @@ -135,10 +137,10 @@ export function JackpotPoolsConsole() { draw_id: drawId, amount: amount !== undefined && Number.isFinite(amount) ? amount : undefined, }); - toast.success("已手动触发爆池"); + toast.success(t("manualBurstSuccess")); await load(); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "手动爆池失败"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("manualBurstFailed")); } finally { setBurstingId(null); } @@ -148,12 +150,12 @@ export function JackpotPoolsConsole() { - Jackpot 奖池配置 + {t("configTitle")} - {loading ?

加载中…

: null} + {loading ?

{t("states.loading", { ns: "common" })}

: null} {!loading && items.length === 0 ? ( -

暂无奖池数据

+

{t("noPoolData")}

) : null} {items.map((p) => { const d = drafts[p.id] ?? toDraft(p); @@ -162,12 +164,14 @@ export function JackpotPoolsConsole() {

{p.currency_code}

- 展示余额 {formatAdminMinorUnits(p.current_amount, p.currency_code)} + {t("displayBalance", { + amount: formatAdminMinorUnits(p.current_amount, p.currency_code), + })}
- +
- +
- +
- +
- +
- +
- +
- +
- +
- + void manualBurst(p)} > - {burstingId === p.id ? "处理中…" : "手动爆池"} + {burstingId === p.id ? t("processing") : t("manualBurst")}
diff --git a/src/modules/jackpot/jackpot-records-console.tsx b/src/modules/jackpot/jackpot-records-console.tsx index 393079c..acc99df 100644 --- a/src/modules/jackpot/jackpot-records-console.tsx +++ b/src/modules/jackpot/jackpot-records-console.tsx @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { getAdminJackpotContributions, getAdminJackpotPayoutLogs } from "@/api/admin-jackpot"; import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer"; @@ -26,6 +27,7 @@ import type { } from "@/types/api/admin-jackpot"; export function JackpotRecordsConsole() { + const { t } = useTranslation(["jackpot", "common"]); const formatDt = useAdminDateTimeFormatter(); const [drawNo, setDrawNo] = useState(""); const [appliedDrawNo, setAppliedDrawNo] = useState(""); @@ -52,11 +54,11 @@ export function JackpotRecordsConsole() { }); setPayouts(d); } catch (e) { - setErr(e instanceof LotteryApiBizError ? e.message : "派彩记录加载失败"); + setErr(e instanceof LotteryApiBizError ? e.message : t("payoutLoadFailed")); } finally { setLoadingP(false); } - }, [pPage, pPer, appliedDrawNo]); + }, [pPage, pPer, appliedDrawNo, t]); const loadContribs = useCallback(async () => { setLoadingC(true); @@ -68,11 +70,11 @@ export function JackpotRecordsConsole() { }); setContribs(d); } catch (e) { - setErr(e instanceof LotteryApiBizError ? e.message : "蓄水记录加载失败"); + setErr(e instanceof LotteryApiBizError ? e.message : t("contributionLoadFailed")); } finally { setLoadingC(false); } - }, [cPage, cPer, appliedDrawNo]); + }, [cPage, cPer, appliedDrawNo, t]); useEffect(() => { queueMicrotask(() => { @@ -96,21 +98,21 @@ export function JackpotRecordsConsole() { - 筛选 + {t("filter")}
- + setDrawNo(e.target.value)} - placeholder="可选" + placeholder={t("optional")} />
@@ -119,21 +121,21 @@ export function JackpotRecordsConsole() { - Jackpot 派彩记录 + {t("payoutRecords")} {loadingP && !payouts ? ( -

加载中…

+

{t("states.loading", { ns: "common" })}

) : (
ID - 期号 - 触发 - 派彩额 - 中奖人数 - 时间 + {t("drawNo")} + {t("trigger")} + {t("payoutAmount")} + {t("winnerCount")} + {t("time")} @@ -174,21 +176,21 @@ export function JackpotRecordsConsole() { - Jackpot 蓄水记录 + {t("contributionRecords")} {loadingC && !contribs ? ( -

加载中…

+

{t("states.loading", { ns: "common" })}

) : (
ID - 期号 - 注单 - 玩家 - 蓄水额 - 时间 + {t("drawNo")} + {t("ticketNo")} + {t("player")} + {t("contributionAmount")} + {t("time")} diff --git a/src/modules/jackpot/jackpot-subnav.tsx b/src/modules/jackpot/jackpot-subnav.tsx index 3a41267..3ac7b66 100644 --- a/src/modules/jackpot/jackpot-subnav.tsx +++ b/src/modules/jackpot/jackpot-subnav.tsx @@ -2,19 +2,21 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useTranslation } from "react-i18next"; import { cn } from "@/lib/utils"; const LINKS: { href: string; label: string }[] = [ - { href: "/admin/jackpot/pools", label: "奖池配置" }, - { href: "/admin/jackpot/records", label: "记录" }, + { href: "/admin/jackpot/pools", label: "subnavPools" }, + { href: "/admin/jackpot/records", label: "subnavRecords" }, ]; export function JackpotSubNav() { + const { t } = useTranslation("jackpot"); const pathname = usePathname(); return ( -
ID - 主站 - 主站玩家ID - 用户名 - 昵称 - 币种 - 余额 - 可用 - 状态 - 最后登录 - 操作 + {t("site")} + {t("sitePlayerId")} + {t("username")} + {t("nickname")} + {t("currency")} + {t("balance")} + {t("available")} + {t("status")} + {t("lastLogin")} + {t("actions")} {items.length === 0 && !loading ? ( - 暂无数据 + {t("states.noData", { ns: "common" })} ) : ( @@ -331,7 +333,7 @@ export function PlayersConsole(): React.ReactElement { - {playerStatusLabel(row.status)} + {playerStatusLabelT(row.status, t)} @@ -355,7 +357,7 @@ export function PlayersConsole(): React.ReactElement { } onClick={() => openEditAccount(row)} > - 编辑 + {t("edit")} @@ -392,59 +394,57 @@ export function PlayersConsole(): React.ReactElement { - {accountMode === "create" ? "新建玩家" : "编辑玩家"} + {accountMode === "create" ? t("createDialogTitle") : t("editDialogTitle")} - {accountMode === "create" - ? "手动注册一个主站玩家到彩票平台,通常由 SSO 登录自动创建。" - : "编辑玩家信息。"} + {accountMode === "create" ? t("createDialogDesc") : t("editDialogDesc")}
{accountMode === "create" && ( <>
- + setFormSiteCode(e.target.value)} />
- + setFormSitePlayerId(e.target.value)} />
)}
- + setFormUsername(e.target.value)} />
- + setFormNickname(e.target.value)} />
{accountMode === "create" && ( <>
- +
- + setFormStatus(Number(v))} @@ -485,7 +485,7 @@ export function PlayersConsole(): React.ReactElement { {PLAYER_STATUS_OPTIONS.map((o) => ( - {o.label} + {t(o.label)} ))} @@ -499,14 +499,14 @@ export function PlayersConsole(): React.ReactElement { variant="outline" onClick={() => handleAccountDialogOpenChange(false)} > - 取消 + {t("cancel")}
@@ -515,23 +515,21 @@ export function PlayersConsole(): React.ReactElement { !open && setDeleteTarget(null)}> - 确认删除 + {t("confirmDelete")} - 确定要删除玩家{" "} - {deleteTarget ? ( - - {deleteTarget.username ?? deleteTarget.site_player_id} - - ) : null}{" "} - 吗?此操作不可恢复。 + {deleteTarget + ? t("confirmDeleteDesc", { + name: deleteTarget.username ?? deleteTarget.site_player_id, + }) + : null}
diff --git a/src/modules/reconcile/meta.ts b/src/modules/reconcile/meta.ts index c75f078..aa234e0 100644 --- a/src/modules/reconcile/meta.ts +++ b/src/modules/reconcile/meta.ts @@ -1,5 +1,5 @@ export const reconcileModuleMeta = { segment: "reconcile", - title: "对账", + title: "Reconcile", description: "", } as const; diff --git a/src/modules/reconcile/reconcile-console.tsx b/src/modules/reconcile/reconcile-console.tsx index 6bc164b..a7a7d2d 100644 --- a/src/modules/reconcile/reconcile-console.tsx +++ b/src/modules/reconcile/reconcile-console.tsx @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { @@ -42,34 +43,34 @@ import type { const MANAGE = ["prd.wallet_reconcile.manage"] as const; /** 与后端 reconcile_type 对齐;扩展时在 API 与下拉同步增加 */ -const RECONCILE_TYPE_OPTIONS = [{ value: "wallet_transfer", label: "钱包划转(主站 ⇄ 彩票)" }] as const; +const RECONCILE_TYPE_OPTIONS = [{ value: "wallet_transfer", label: "walletTransfer" }] as const; -function reconcileTypeLabel(slug: string): string { +function reconcileTypeLabel(slug: string, t: (key: string) => string): string { const hit = RECONCILE_TYPE_OPTIONS.find((o) => o.value === slug); - return hit?.label ?? slug; + return hit ? t(hit.label) : slug; } -function jobStatusLabel(status: string): string { +function jobStatusLabel(status: string, t: (key: string) => string): string { switch (status) { case "completed": - return "已完成"; + return t("statusCompleted"); case "running": - return "执行中"; + return t("statusRunning"); case "failed": - return "失败"; + return t("statusFailed"); default: return status; } } -function itemStatusLabel(status: string): string { +function itemStatusLabel(status: string, t: (key: string) => string): string { switch (status) { case "mismatch": - return "不一致"; + return t("itemMismatch"); case "matched": - return "一致"; + return t("itemMatched"); case "pending_check": - return "待核对"; + return t("itemPendingCheck"); default: return status; } @@ -106,6 +107,7 @@ function scopeLinesToItems( } export function ReconcileConsole(): React.ReactElement { + const { t } = useTranslation(["reconcile", "common"]); const profile = useAdminProfile(); const canCreate = adminHasAnyPermission(profile?.permissions, [...MANAGE]); const formatTs = useAdminDateTimeFormatter(); @@ -137,12 +139,12 @@ export function ReconcileConsole(): React.ReactElement { const d = await getAdminReconcileJobs({ page, per_page: perPage }); setJobs(d); } catch (e) { - setJobsErr(e instanceof LotteryApiBizError ? e.message : "加载失败"); + setJobsErr(e instanceof LotteryApiBizError ? e.message : t("loadFailed")); setJobs(null); } finally { setJobsLoading(false); } - }, [page, perPage]); + }, [page, perPage, t]); useEffect(() => { queueMicrotask(() => { @@ -163,12 +165,12 @@ export function ReconcileConsole(): React.ReactElement { }); setItems(d); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "加载明细失败"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("loadItemsFailed")); setItems(null); } finally { setItemsLoading(false); } - }, [selectedId, itemsPage, itemsPerPage]); + }, [selectedId, itemsPage, itemsPerPage, t]); useEffect(() => { queueMicrotask(() => { @@ -178,17 +180,17 @@ export function ReconcileConsole(): React.ReactElement { async function onCreate(): Promise { if (!periodStartLocal.trim() || !periodEndLocal.trim()) { - toast.error("请填写对账时间范围(开始与结束)"); + toast.error(t("periodRequired")); return; } const periodStartIso = toIsoFromDatetimeLocal(periodStartLocal); const periodEndIso = toIsoFromDatetimeLocal(periodEndLocal); if (periodStartIso == null || periodEndIso == null) { - toast.error("时间无效,请检查所选日期与时间"); + toast.error(t("periodInvalid")); return; } if (new Date(periodStartIso).getTime() > new Date(periodEndIso).getTime()) { - toast.error("结束时间需晚于或等于开始时间"); + toast.error(t("periodOrderInvalid")); return; } @@ -202,7 +204,7 @@ export function ReconcileConsole(): React.ReactElement { Parameters[0]["items"] >; } catch { - toast.error("高级选项中的 JSON 无法解析"); + toast.error(t("advancedJsonInvalid")); return; } } @@ -220,7 +222,7 @@ export function ReconcileConsole(): React.ReactElement { period_end: periodEndIso, items: itemsPayload, }); - toast.success("已创建对账任务"); + toast.success(t("createSuccess")); setPage(1); setScopeLines(""); if (showAdvanced) { @@ -228,7 +230,7 @@ export function ReconcileConsole(): React.ReactElement { } await loadJobs(); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "创建失败"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("createFailed")); } finally { setSubmitting(false); } @@ -242,15 +244,14 @@ export function ReconcileConsole(): React.ReactElement { {canCreate ? ( - 人工发起对账 + {t("createTitle")} - 异常流水由定时任务自动核对。此处供财务按产品文档手动触发 - :选择对账类型与时间范围;可选填写待核对对象(玩家标识、划转单号或幂等键,每行一条)。任务与明细落库留痕,后续可接自动差异引擎。 + {t("createDesc")}
- +
- +
- +