diff --git a/src/api/admin-player-tickets.ts b/src/api/admin-player-tickets.ts index a74cafb..c01a07c 100644 --- a/src/api/admin-player-tickets.ts +++ b/src/api/admin-player-tickets.ts @@ -8,7 +8,15 @@ const A = `${API_V1_PREFIX}/admin`; export async function getAdminPlayerTicketItems( playerId: number, - params?: { page?: number; per_page?: number; draw_no?: string }, + params?: { + page?: number; + per_page?: number; + draw_no?: string; + status?: string[]; + number?: string; + start_date?: string; + end_date?: string; + }, ): Promise { return adminRequest.get( `${A}/players/${playerId}/ticket-items`, diff --git a/src/api/admin-tickets.ts b/src/api/admin-tickets.ts new file mode 100644 index 0000000..855dc35 --- /dev/null +++ b/src/api/admin-tickets.ts @@ -0,0 +1,25 @@ +import { adminRequest } from "@/lib/admin-http"; + +import { API_V1_PREFIX } from "./paths"; + +import type { AdminTicketItemsData } from "@/types/api/admin-tickets"; + +const A = `${API_V1_PREFIX}/admin`; + +export type TicketItemsListQuery = { + page?: number; + per_page?: number; + player_id?: number; + player_account?: string; + draw_no?: string; + status?: string[]; + number?: string; + start_date?: string; + end_date?: string; +}; + +export async function getAdminTicketItems( + q: TicketItemsListQuery = {}, +): Promise { + return adminRequest.get(`${A}/tickets`, { params: q }); +} diff --git a/src/api/index.ts b/src/api/index.ts index 10693a2..17e85bd 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -22,6 +22,7 @@ export { getAdminDraws, postAdminPublishResultBatch, } from "@/api/admin-draws"; +export { getAdminTicketItems } from "@/api/admin-tickets"; export { getAdminPlayerTicketItems } from "@/api/admin-player-tickets"; export type { AdminAuthCaptchaResponse, diff --git a/src/app/admin/(shell)/config/jackpot/records/page.tsx b/src/app/admin/(shell)/config/jackpot/records/page.tsx index 70891a4..bf72361 100644 --- a/src/app/admin/(shell)/config/jackpot/records/page.tsx +++ b/src/app/admin/(shell)/config/jackpot/records/page.tsx @@ -4,17 +4,13 @@ import { jackpotModuleMeta } from "@/modules/jackpot/meta"; import type { Metadata } from "next"; export const metadata: Metadata = { - title: `Jackpot 记录 · ${jackpotModuleMeta.title}`, + title: jackpotModuleMeta.title, }; export default function AdminConfigJackpotRecordsPage() { return (
-
-

Jackpot 记录

-

派彩与蓄水流水

-
); diff --git a/src/app/admin/(shell)/config/wallet/page.tsx b/src/app/admin/(shell)/config/wallet/page.tsx index 50b516a..f96a1f6 100644 --- a/src/app/admin/(shell)/config/wallet/page.tsx +++ b/src/app/admin/(shell)/config/wallet/page.tsx @@ -1,11 +1,10 @@ -import { configWalletMeta } from "@/modules/config/meta"; -import { WalletConfigDocScreen } from "@/modules/config/doc/wallet-config-doc-screen"; +import { redirect } from "next/navigation"; import type { Metadata } from "next"; export const metadata: Metadata = { - title: configWalletMeta.title, + title: "系统设置", }; export default function AdminConfigWalletPage() { - return ; + redirect("/admin/settings"); } diff --git a/src/app/admin/(shell)/settings/page.tsx b/src/app/admin/(shell)/settings/page.tsx index 5639aee..88eec74 100644 --- a/src/app/admin/(shell)/settings/page.tsx +++ b/src/app/admin/(shell)/settings/page.tsx @@ -1,5 +1,6 @@ import { ModuleScaffold } from "@/components/admin/module-scaffold"; import { settingsModuleMeta } from "@/modules/settings/meta"; +import { SystemSettingsScreen } from "@/modules/settings/system-settings-screen"; import type { Metadata } from "next"; export const metadata: Metadata = { @@ -9,13 +10,7 @@ export const metadata: Metadata = { export default function AdminSettingsPage() { return ( -

- 业务组件请放在{" "} - - src/modules/settings - {" "} - 下。 -

+
); } diff --git a/src/app/admin/(shell)/settlement-batches/[batchId]/details/page.tsx b/src/app/admin/(shell)/settlement-batches/[batchId]/details/page.tsx index 1f6ebf5..8d2b1bc 100644 --- a/src/app/admin/(shell)/settlement-batches/[batchId]/details/page.tsx +++ b/src/app/admin/(shell)/settlement-batches/[batchId]/details/page.tsx @@ -1,3 +1,4 @@ +import { InvalidSettlementBatchId } from "@/modules/settlement/invalid-settlement-batch-id"; import { SettlementBatchDetailsConsole } from "@/modules/settlement/settlement-batch-details-console"; import { settlementModuleMeta } from "@/modules/settlement/meta"; import type { Metadata } from "next"; @@ -12,7 +13,7 @@ export default async function AdminSettlementBatchDetailsPage(props: { const { batchId } = await props.params; const id = Number.parseInt(batchId, 10); if (!Number.isFinite(id) || id < 1) { - return

无效的批次 ID

; + return ; } return ; diff --git a/src/app/globals.css b/src/app/globals.css index d27ab57..f7cf144 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -151,7 +151,11 @@ } .admin-list-field { - @apply flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center sm:shrink-0; + @apply flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center sm:shrink-0 sm:gap-1.5; + } + + .admin-list-field > label { + @apply w-auto min-w-0 shrink-0 whitespace-nowrap; } .admin-list-actions { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3e9bfe9..1e99a8c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -19,7 +19,7 @@ export const metadata: Metadata = { template: "%s · 彩票后台", default: "彩票后台", }, - description: "Lottery 管理端", + description: "彩票后台管理端", }; export default function RootLayout({ diff --git a/src/components/admin/admin-breadcrumb.tsx b/src/components/admin/admin-breadcrumb.tsx index 4cc24e5..7b0a9a8 100644 --- a/src/components/admin/admin-breadcrumb.tsx +++ b/src/components/admin/admin-breadcrumb.tsx @@ -21,6 +21,23 @@ const DRAW_ROUTE_LABELS: Record = { results: "Results", }; +const NAV_TRANSLATION_KEYS: Record = { + dashboard: "dashboard", + admin_users: "admin_users", + admin_roles: "admin_roles", + players: "players", + wallet: "wallet", + draws: "draws", + config: "config", + risk: "risk", + settlement: "settlement", + reconcile: "reconcile", + tickets: "tickets", + reports: "reports", + audit: "audit", + settings: "settings", +}; + function titleCase(value: string): string { return value .split("-") @@ -60,22 +77,24 @@ 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" }), - }; + const translatedNavLabel = + NAV_TRANSLATION_KEYS[navItem.segment] != null + ? t(`nav.${NAV_TRANSLATION_KEYS[navItem.segment]}`, { + ns: "common", + defaultValue: navItem.label, + }) + : navItem.label; breadcrumbs.push({ - label: - navItem.segment === "draws" - ? "Draws" - : navLabelMap[navItem.segment] ?? navItem.label, + label: translatedNavLabel, href: navItem.href, isCurrent: pathname === navItem.href || segments.length === 2, }); } else { breadcrumbs.push({ - label: titleCase(businessSegment), + label: t(`nav.${businessSegment}`, { + ns: "common", + defaultValue: titleCase(businessSegment), + }), href: `${ADMIN_BASE}/${businessSegment}`, isCurrent: segments.length === 2, }); @@ -111,7 +130,7 @@ export function AdminBreadcrumb() { {breadcrumbs.map((crumb, index) => { const isLast = index === breadcrumbs.length - 1; const itemKey = `${crumb.href}-${index}`; - + return ( diff --git a/src/components/admin/admin-language-switcher.tsx b/src/components/admin/admin-language-switcher.tsx index 54c5ac7..d0ee227 100644 --- a/src/components/admin/admin-language-switcher.tsx +++ b/src/components/admin/admin-language-switcher.tsx @@ -20,6 +20,13 @@ import { getAdminRequestLocale, type AdminApiLocale, } from "@/lib/admin-locale"; +import { cn } from "@/lib/utils"; + +const LOCALE_FLAGS: Record = { + zh: "🇨🇳", + en: "🇺🇸", + ne: "🇳🇵", +}; export function AdminLanguageSwitcher() { const { i18n, t } = useTranslation("common"); @@ -44,32 +51,60 @@ export function AdminLanguageSwitcher() { ); } + const currentFlag = LOCALE_FLAGS[locale]; + return ( - - - {locale} + + + {currentFlag} + + - - - + + + {t("language.title")} - {ADMIN_API_LOCALES.map((code) => ( - void onSelectLocale(code)} - > - {locale === code ? ( - - ) : ( - - )} - {ADMIN_LOCALE_LABELS[code]} - {code} - - ))} + {ADMIN_API_LOCALES.map((code) => { + const active = locale === code; + + return ( + void onSelectLocale(code)} + > + + {LOCALE_FLAGS[code]} + + + + {ADMIN_LOCALE_LABELS[code]} + + + + {active ? : null} + + + ); + })} diff --git a/src/components/admin/admin-shell.tsx b/src/components/admin/admin-shell.tsx index bbf4466..814e0c8 100644 --- a/src/components/admin/admin-shell.tsx +++ b/src/components/admin/admin-shell.tsx @@ -16,7 +16,7 @@ export function AdminShell({ children }: { children: ReactNode }) { return ( - +
@@ -25,7 +25,7 @@ export function AdminShell({ children }: { children: ReactNode }) {
-
+
{children}
diff --git a/src/components/admin/admin-table-export-button.tsx b/src/components/admin/admin-table-export-button.tsx index f148d0d..6ed02a3 100644 --- a/src/components/admin/admin-table-export-button.tsx +++ b/src/components/admin/admin-table-export-button.tsx @@ -91,7 +91,7 @@ export function AdminTableExportButton({ }; return ( - diff --git a/src/components/admin/module-scaffold.tsx b/src/components/admin/module-scaffold.tsx index a1c7043..640138d 100644 --- a/src/components/admin/module-scaffold.tsx +++ b/src/components/admin/module-scaffold.tsx @@ -9,5 +9,5 @@ type ModuleScaffoldProps = { /** 内容区容器;模块标题由侧栏导航体现,此处不再重复大标题与说明。 */ export function ModuleScaffold({ children, className }: ModuleScaffoldProps) { - return
{children}
; + return
{children}
; } diff --git a/src/components/admin/toolbar.tsx b/src/components/admin/toolbar.tsx index 5b10775..5120db7 100644 --- a/src/components/admin/toolbar.tsx +++ b/src/components/admin/toolbar.tsx @@ -1,7 +1,6 @@ "use client"; import { - BellIcon, ChevronDownIcon, LogOutIcon, UserRoundIcon, @@ -11,8 +10,6 @@ import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { AdminLanguageSwitcher } from "@/components/admin/admin-language-switcher"; -import { Avatar, AvatarFallback } from "@/components/ui/avatar"; -import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, @@ -22,47 +19,10 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Separator } from "@/components/ui/separator"; import { useAdminProfile, useAdminSessionStore, } from "@/stores/admin-session"; -import type { AdminProfile } from "@/types/api/admin-auth"; - -/** 暂未接入通知中心时的占位未读数(与设计稿一致可改为接口数据) */ -const NOTIFICATION_PLACEHOLDER_COUNT = 6; - -function initialsFromProfile(profile: AdminProfile | null): string { - if (!profile) { - return "—"; - } - const s = (profile.nickname?.trim() || profile.username?.trim() || "").trim(); - if (!s) { - return "?"; - } - const runes = Array.from(s); - if (runes.length === 1) { - return runes[0].toUpperCase(); - } - const cp = runes[0].codePointAt(0); - const isCjk = - cp !== undefined && - ((cp >= 0x4e00 && cp <= 0x9fff) || - (cp >= 0x3400 && cp <= 0x4dbf) || - (cp >= 0xf900 && cp <= 0xfaff)); - if (isCjk) { - return runes.slice(0, 2).join(""); - } - const parts = s.split(/\s+/).filter(Boolean); - if (parts.length >= 2) { - const a = parts[0][0]; - const b = parts[1][0]; - - return `${a}${b}`.toUpperCase(); - } - - return s.slice(0, 2).toUpperCase(); -} export function ShellToolbar() { const { t } = useTranslation("common"); @@ -84,40 +44,14 @@ export function ShellToolbar() { return (
- - - - - - - - - - {initialsFromProfile(adminProfile)} - - - + + {displayName} - + diff --git a/src/components/providers.tsx b/src/components/providers.tsx index c327b1a..a88fcd5 100644 --- a/src/components/providers.tsx +++ b/src/components/providers.tsx @@ -2,8 +2,7 @@ import type { ReactNode } from "react"; import { useEffect } from "react"; -import { ThemeProvider } from "next-themes"; -import "@/i18n"; +import i18n from "@/i18n"; import { Toaster } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; @@ -16,7 +15,10 @@ type ProvidersProps = { function AdminSessionHydrator() { useEffect(() => { - hydrateAdminUiLocale(); + const locale = hydrateAdminUiLocale(); + if (locale && i18n.resolvedLanguage !== locale) { + void i18n.changeLanguage(locale); + } useAdminSessionStore.getState().rehydrate(); }, []); @@ -25,12 +27,10 @@ function AdminSessionHydrator() { export function Providers({ children }: ProvidersProps) { return ( - - - - {children} - - - + + + {children} + + ); } diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index 4a161ed..cb72039 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -307,7 +307,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
{ - const { theme = "system" } = useTheme() + const [theme, setTheme] = useState("light"); + + useEffect(() => { + if (typeof window === "undefined") { + return undefined; + } + + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const syncTheme = () => { + setTheme(mediaQuery.matches ? "dark" : "light"); + }; + + syncTheme(); + mediaQuery.addEventListener("change", syncTheme); + + return () => { + mediaQuery.removeEventListener("change", syncTheme); + }; + }, []); return ( { }} {...props} /> - ) -} + ); +}; -export { Toaster } +export { Toaster }; diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 6a4de04..cffb99f 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -1,7 +1,6 @@ "use client"; import i18n from "i18next"; -import LanguageDetector from "i18next-browser-languagedetector"; import { initReactI18next } from "react-i18next"; import { adminHtmlLang, applyAdminUiLocale, type AdminApiLocale } from "@/lib/admin-locale"; @@ -53,7 +52,7 @@ 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"; +export const ADMIN_DEFAULT_LANGUAGE: AdminLanguage = "zh"; const namespaces = ["common", "auth", "dashboard", "reports", "audit", "draws", "settlement", "risk", "jackpot", "players", "tickets", "reconcile", "wallet", "adminUsers", "config"] as const; @@ -118,6 +117,14 @@ function normalizeAdminLanguage(lang: string | undefined): AdminLanguage { return "en"; } +function getInitialAdminLanguage(): AdminLanguage { + if (typeof document === "undefined") { + return ADMIN_DEFAULT_LANGUAGE; + } + + return normalizeAdminLanguage(document.documentElement.lang); +} + function syncAdminLanguage(lang: AdminLanguage): void { if (typeof document !== "undefined") { document.documentElement.lang = adminHtmlLang(lang); @@ -127,20 +134,15 @@ function syncAdminLanguage(lang: AdminLanguage): void { if (!i18n.isInitialized) { void i18n - .use(LanguageDetector) .use(initReactI18next) .init({ resources, + lng: getInitialAdminLanguage(), fallbackLng: ADMIN_DEFAULT_LANGUAGE, supportedLngs: [...ADMIN_SUPPORTED_LANGUAGES], defaultNS: "common", ns: [...namespaces], load: "languageOnly", - detection: { - order: ["localStorage", "navigator"], - caches: ["localStorage"], - lookupLocalStorage: "lottery_admin_ui_locale", - }, interpolation: { escapeValue: false, }, diff --git a/src/i18n/locales/en/adminUsers.json b/src/i18n/locales/en/adminUsers.json index 39fa115..17f3df9 100644 --- a/src/i18n/locales/en/adminUsers.json +++ b/src/i18n/locales/en/adminUsers.json @@ -14,6 +14,17 @@ "saveAccountFailed": "Failed to save account", "deleteSuccess": "Deleted {{name}}", "deleteFailed": "Delete failed", + "roleLoadFailed": "Failed to load role list", + "roleListTitle": "Role Management", + "createRole": "Create role", + "roleCreateSuccess": "Created role {{name}}", + "roleUpdateSuccess": "Updated role {{name}}", + "roleSaveFailed": "Failed to save role", + "roleDeleteSuccess": "Deleted role {{name}}", + "roleDeleteFailed": "Failed to delete role", + "rolePermissionSaveSuccess": "Role permissions updated", + "rolePermissionSaveFailed": "Failed to save role permissions", + "roleFormRequired": "Role name and slug are required", "allPermissions": "All permissions", "saveRoleSuccess": "Updated roles for {{name}}", "saveRoleFailed": "Failed to save roles", @@ -36,6 +47,10 @@ "enabled": "Enabled", "disabled": "Disabled" }, + "roleType": { + "system": "System", + "custom": "Custom" + }, "actions": { "permissions": "Assign roles", "edit": "Edit", @@ -43,6 +58,18 @@ "cancel": "Cancel", "save": "Save" }, + "roleTable": { + "name": "Role", + "slug": "Role Code", + "type": "Type", + "status": "Status", + "users": "Users", + "permissions": "Permission count", + "actions": "Actions" + }, + "roleActions": { + "permissions": "Permissions" + }, "permissionDialog": { "title": "Assign roles", "rolesTitle": "Roles", @@ -51,6 +78,18 @@ "selectedRoles": "Selected roles:", "saveRoles": "Save roles" }, + "rolePermissionDialog": { + "title": "Role Permissions" + }, + "roleDialog": { + "createTitle": "Create Role", + "editTitle": "Edit Role", + "description": "Roles group backend function permissions and are then assigned to admin accounts.", + "slug": "Role code", + "name": "Role name", + "descriptionLabel": "Role description", + "status": "Status" + }, "accountDialog": { "createTitle": "Create admin", "editTitle": "Edit account", @@ -75,5 +114,60 @@ "rowActionTitle": "Delete this admin", "confirmTitle": "Confirm deletion", "confirmDescription": "Delete admin {{name}}? This action cannot be undone." + }, + "roleDelete": { + "confirmTitle": "Delete Role", + "confirmDescription": "Delete role {{name}}?" + }, + "permissionGroups": { + "all": "All Permissions", + "dashboard": "Dashboard", + "admin_users": "Admin Users", + "admin_roles": "Role Management", + "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" + }, + "permissionNames": { + "prd.admin_user.manage": "Admin Users · Manage", + "prd.admin_role.manage": "Role Management · Manage", + "prd.users.manage": "Players · Manage", + "prd.users.view_finance": "Players · View Finance", + "prd.users.view_cs": "Players · View Customer Service Cases", + "prd.player_freeze.manage": "Freeze/Unfreeze Player · Manage", + "prd.wallet_reconcile.manage": "Wallet Reconcile · Manage", + "prd.wallet_reconcile.view": "Wallet Reconcile · View", + "prd.wallet_reconcile.view_cs": "Wallet Reconcile · Customer Service View", + "prd.wallet_adjust.manage": "Adjustment/Reversal · Manage", + "prd.draw_result.manage": "Draw Results Entry · Manage", + "prd.draw_result.view": "Draw Results · View", + "prd.draw_reopen.manage": "Draw Reopen · Manage", + "prd.play_switch.manage": "Play Switches · Manage", + "prd.odds.manage": "Odds Configuration · Manage", + "prd.risk_cap.manage": "Risk Caps · Manage", + "prd.risk_cap.view": "Risk Caps · View", + "prd.rebate.manage": "Commission/Rebate · Manage", + "prd.rebate.view": "Commission/Rebate · View", + "prd.jackpot.manage": "Jackpot Configuration · Manage", + "prd.jackpot.view": "Jackpot Configuration · View", + "prd.payout.manage": "Payout Confirmation · Manage", + "prd.payout.review": "Payout Confirmation · Review", + "prd.payout.view": "Payout Confirmation · View", + "prd.report.all": "Reports · All", + "prd.report.risk": "Reports · Risk", + "prd.report.finance": "Reports · Finance", + "prd.report.player": "Reports · Single Player", + "prd.audit.all": "Audit Logs · All", + "prd.audit.self": "Audit Logs · Related to Self", + "prd.audit.finance": "Audit Logs · Finance Related" } } diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 73f1cc0..a39f1b5 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -46,6 +46,9 @@ "errors": { "loadFailed": "Failed to load" }, + "table": { + "id": "ID" + }, "toolbar": { "defaultAdmin": "Administrator", "notifications": "Notifications", @@ -57,6 +60,7 @@ "home": "Home", "dashboard": "Dashboard", "admin_users": "Admin Users", + "admin_roles": "Role Management", "players": "Players", "wallet": "Wallet", "draws": "Draws", @@ -65,7 +69,7 @@ "settlement": "Settlement", "jackpot": "Jackpot", "reconcile": "Reconcile", - "tickets": "Tickets", + "tickets": "Ticket list", "reports": "Reports", "audit": "Audit Logs", "settings": "Settings" diff --git a/src/i18n/locales/en/config.json b/src/i18n/locales/en/config.json index 6d44f02..771c9e3 100644 --- a/src/i18n/locales/en/config.json +++ b/src/i18n/locales/en/config.json @@ -5,15 +5,14 @@ "sidebarTitle": "Operations configuration", "groups": { "betting": "Betting and display", - "risk_wallet": "Risk and funds" + "risk": "Risk control" }, "items": { "plays": "Play types and limits", "odds": "Odds", "rebate": "Commission / rebate", "jackpot": "Jackpot pool", - "risk-cap": "Payout caps", - "wallet": "Wallet thresholds" + "risk-cap": "Payout caps" } }, "versionStatus": { @@ -70,6 +69,31 @@ }, "discard": "Discard changes" }, + "system": { + "title": "Draw and settlement runtime settings", + "runtimeTitle": "Global runtime parameters", + "runtimeIntro1": "This area stores global system parameters that do not belong to play, odds, or risk-control versions. They directly affect wallet transfers, job switches, and runtime policy.", + "runtimeIntro2": "Play, odds, rebate, and cap management stay under operations configuration. System settings only carry cross-module runtime parameters to avoid overlapping responsibilities in admin.", + "description": "Controls review flow after RNG draw generation, cooldown duration, and automatic settlement behavior. These are global runtime policies and do not belong to versioned operations config.", + "loadFailed": "Failed to load system settings", + "saveSuccess": "System settings saved", + "saveFailed": "Failed to save system settings", + "fields": { + "manualReview": "Require manual review for draw results", + "cooldownMinutes": "Cooldown duration (minutes)", + "autoSettlement": "Run settlement automatically" + }, + "hints": { + "manualReview": "When enabled, RNG draw results enter pending review and must be published manually in admin.", + "cooldownMinutes": "How long to wait after publishing before entering settling. Use 0 to settle immediately.", + "autoSettlement": "When disabled, tick will not run settlement automatically and admins must trigger it manually." + }, + "states": { + "enabled": "Enabled", + "disabled": "Disabled" + }, + "discard": "Discard changes" + }, "play": { "batchGroups": { "d2": "2D Global", @@ -79,6 +103,164 @@ "position": "Position Plays", "box": "Box Plays", "jackpot": "Jackpot" + }, + "validation": { + "minMaxInvalid": "{{playCode}}: min bet cannot exceed max bet" + }, + "publishFailed": "Publish failed", + "createDraftSuccess": "Created draft v{{version}}", + "createDraftFailed": "Failed to create draft", + "ruleSavedLocal": "Rule text was saved into the local draft. Save the draft to persist it.", + "deleteFailed": "Delete failed", + "activeVersion": "Active version v{{version}}", + "readOnlyHint": "Limits and rules are read-only. Create a draft first.", + "batchSwitchesTitle": "Batch switches", + "batchSwitchesDesc": "Only updates the current draft. The player betting table refreshes after save and publish.", + "readOnlyDraftHint": "Current version is read-only. Create a draft first.", + "batchEnabledCount": "{{enabledCount}}/{{total}} enabled", + "noPlayTypes": "No play types", + "actions": { + "enable": "Enable", + "disable": "Disable", + "ruleText": "Rule text" + }, + "table": { + "playCode": "Play code", + "category": "Category", + "status": "Status", + "displayName": "Display name", + "order": "Order", + "minBet": "Min bet", + "maxBet": "Max bet", + "actions": "Actions" + }, + "states": { + "enabled": "Enabled", + "disabled": "Disabled", + "readOnly": "Read only" + }, + "aria": { + "enablePlay": "Enable {{playCode}}" + }, + "ruleDialog": { + "title": "Rule text (Chinese)", + "description": "Play {{playCode}}. Changes stay in the draft until you save and publish it.", + "fieldLabel": "rule_text_zh", + "apply": "Apply to draft" + } + }, + "odds": { + "tabs": { + "all": "All" + }, + "category": "Category", + "playType": "Play type", + "noPlayTypes": "No play types in this category.", + "sheetDescription": "Choose a version to view here. Non-draft versions can be rolled back into a new draft.", + "activeVersionPrefix": "Active version:", + "readOnlyHint": "This version is read-only. Create a draft before editing odds.", + "loadingDetails": "Loading details…", + "multiplier": "Multiplier x{{value}} · {{currency}}", + "missingScopeRow": "Missing {{scope}} row. Check seed or version data.", + "rebateRate": "Rebate rate (%)", + "rebateRateHint": "Writes rebate_rate to all prize scopes under this play type.", + "publishFailed": "Publish failed", + "createDraftSuccess": "Created draft v{{version}}", + "createDraftFailed": "Failed to create draft", + "rollbackSuccess": "Cloned v{{fromVersion}} into new draft v{{version}}", + "rollbackFailed": "Rollback failed", + "deleteFailed": "Delete failed", + "rollbackDialog": { + "title": "Confirm rollback", + "description": "A new draft will be cloned from version v{{version}}. The active version will not be overwritten directly.", + "confirm": "Confirm rollback" + }, + "publishDialog": { + "title": "Publish odds version?", + "description": "New odds affect new tickets immediately. Existing successful tickets still settle by their saved odds snapshot.", + "confirm": "Confirm publish", + "columns": { + "prizeScope": "Prize scope", + "currentActive": "Current active", + "afterPublish": "After publish" + } + } + }, + "rebate": { + "sheetDescription": "Rebate is stored in the odds draft version and shares the same version set as odds.", + "publishLabel": "Publish", + "publishSuccess": "Published odds version with rebate", + "publishFailed": "Publish failed", + "createDraftSuccess": "Created draft v{{version}}", + "createDraftFailed": "Failed to create draft", + "deleteFailed": "Delete failed", + "editingVersion": "Editing version v{{version}} · {{status}}", + "readOnlyHint": "Create a draft before editing rebate.", + "fields": { + "d2": "2D rebate rate (%)", + "d3": "3D rebate rate (%)", + "d4": "4D rebate rate (%)" + }, + "winEnjoy": { + "label": "Apply rebate on winning tickets", + "description": "Placeholder field. It can later be aligned with risk and settlement rules and persisted." + }, + "effectiveTime": "Effective time (current active odds version)" + }, + "riskCap": { + "validation": { + "requireAtLeastOne": "At least one cap row is required", + "defaultGreaterThanZero": "Default cap amount must be greater than 0", + "numberMustBe4Digits": "Number must be 4 digits: {{number}}", + "enterValidCapAmount": "Enter a valid cap amount" + }, + "publishFailed": "Publish failed", + "createDraftSuccess": "Created draft v{{version}}", + "createDraftFailed": "Failed to create draft", + "savedLocalDraft": "Saved into local draft. Save the draft to persist it.", + "deleteFailed": "Delete failed", + "effectiveAt": "Effective at: {{value}}", + "note": "Note: {{value}}", + "readOnlyHint": "Read only. Create a draft first.", + "readOnly": "Read only", + "defaultCap": { + "title": "Default cap", + "description": "Numbers without a special cap use this default cap template.", + "fieldLabel": "Cap amount (minor unit)" + }, + "specialCaps": { + "title": "Special caps" + }, + "loadingDetails": "Loading details…", + "noDetailRows": "No detail rows.", + "table": { + "number": "Number", + "capAmount": "Cap amount", + "used": "Used", + "remaining": "Remaining", + "soldOut": "Sold out", + "ratio": "Ratio", + "actions": "Actions" + }, + "occupancy": { + "title": "All number occupancy", + "description": "Placeholder view: filters and exports still need ticket-summary integration. Data below still comes from the current draft list.", + "searchLabel": "Search number", + "searchPlaceholder": "e.g. 8888", + "filterPending": "Sold-out / high-risk preset filter is pending integration", + "exportPending": "CSV export is pending integration" + }, + "actions": { + "update": "Update", + "addSpecialCap": "+ Add special cap", + "filterPresets": "Filter presets…", + "exportCsv": "Export CSV", + "close": "Close" + }, + "syncDialog": { + "title": "Sync default cap", + "description": "The default cap template will be set to {{value}}. This only changes the draft. Save and publish after confirming.", + "confirm": "Confirm" } } } diff --git a/src/i18n/locales/en/jackpot.json b/src/i18n/locales/en/jackpot.json index a74ceb7..a18761a 100644 --- a/src/i18n/locales/en/jackpot.json +++ b/src/i18n/locales/en/jackpot.json @@ -4,7 +4,7 @@ "loadFailed": "Failed to load", "saveSuccess": "Saved", "saveFailed": "Save failed", - "invalidDrawId": "Enter a valid draw ID", + "invalidDrawId": "Enter a valid draw number", "manualBurstSuccess": "Jackpot burst triggered manually", "manualBurstFailed": "Manual burst failed", "noPoolData": "No pool data", @@ -21,7 +21,7 @@ "enabled": "Enabled", "saving": "Saving…", "save": "Save", - "manualBurstDrawId": "Manual burst draw ID", + "manualBurstDrawId": "Manual burst draw number", "manualBurstAmount": "Burst amount (empty for all)", "processing": "Processing…", "manualBurst": "Manual burst", @@ -31,12 +31,22 @@ "apply": "Apply", "payoutRecords": "Jackpot payout records", "contributionRecords": "Jackpot contribution records", + "recordsPage": { + "title": "Jackpot records", + "description": "Payout records and pool contribution flows" + }, "subnavLabel": "Jackpot sub navigation", "subnavPools": "Pool configuration", "subnavRecords": "Records", "payoutLoadFailed": "Failed to load payout records", "contributionLoadFailed": "Failed to load contribution records", "trigger": "Trigger", + "triggerTypes": { + "threshold": "Threshold reached", + "forced_gap": "Forced by draw gap", + "play_combo": "Triggered by play combo", + "manual": "Manual trigger" + }, "payoutAmount": "Payout amount", "winnerCount": "Winner count", "time": "Time", diff --git a/src/i18n/locales/en/reports.json b/src/i18n/locales/en/reports.json index eca2f0d..321c9a0 100644 --- a/src/i18n/locales/en/reports.json +++ b/src/i18n/locales/en/reports.json @@ -18,6 +18,17 @@ "createdAt": "Created at", "id": "ID", "empty": "No data", + "formatOptions": { + "csv": "CSV", + "xlsx": "Excel" + }, + "statusOptions": { + "pending": "Pending", + "queued": "Queued", + "running": "Running", + "completed": "Completed", + "failed": "Failed" + }, "reportTypes": { "draw_profit_summary": "Draw profit summary", "daily_profit_summary": "Daily profit summary", diff --git a/src/i18n/locales/en/settlement.json b/src/i18n/locales/en/settlement.json index bd74b2f..4e7510b 100644 --- a/src/i18n/locales/en/settlement.json +++ b/src/i18n/locales/en/settlement.json @@ -58,6 +58,7 @@ "matchedTier": "Matched tier", "regularPayout": "Regular payout", "loadingDetails": "Loading details…", + "invalidBatchId": "Invalid settlement batch number", "statusOptions": { "all": "All", "running": "Running", @@ -67,5 +68,10 @@ "paid": "Paid", "completed": "Completed", "failed": "Failed" + }, + "reviewStatusOptions": { + "pending": "Pending review", + "approved": "Approved", + "rejected": "Rejected" } } diff --git a/src/i18n/locales/en/tickets.json b/src/i18n/locales/en/tickets.json index 1f231bc..4e00dcb 100644 --- a/src/i18n/locales/en/tickets.json +++ b/src/i18n/locales/en/tickets.json @@ -1,19 +1,43 @@ { - "title": "Tickets", - "playerTicketQuery": "Player ticket query", - "playerId": "Player ID", - "invalidPlayerId": "Enter a valid player ID", - "drawNoOptional": "Draw no. (optional)", + "title": "Ticket list", + "playerTicketQuery": "Ticket query", + "playerId": "Player ID / account", + "invalidPlayerId": "Enter a valid player ID or account", + "playerIdPlaceholder": "Leave blank for all tickets; enter player ID or account", + "drawNoOptional": "Draw number (optional)", "drawNoPlaceholder": "For example 20260520-001", + "numberKeyword": "Number / ticket / order", + "numberKeywordPlaceholder": "Search by number, ticket no. or order no.", + "placedDateRange": "Placed date range", "query": "Query", + "resetFilters": "Reset filters", + "refreshCurrentPage": "Refresh current page", "loadFailed": "Failed to load", "ticketNo": "Ticket no.", + "player": "Player", "orderNo": "Order no.", "drawNo": "Draw no.", "playCode": "Play", "number": "Number", + "betAmount": "Bet amount", "actualDeduct": "Actual deduct", "status": "Status", "failReason": "Fail reason", - "winAmount": "Win amount" + "winAmount": "Win amount", + "placedAt": "Placed at", + "updatedAt": "Updated at", + "statusFilterLabel": "Status filter", + "statusHint": "Multiple selection supported. Leave empty for all statuses.", + "statusSelectedCount": "{{count}} selected", + "statusOptions": { + "all": "All", + "pending_confirm": "Pending confirmation", + "partial_pending_confirm": "Partially pending confirmation", + "success": "Bet placed", + "failed": "Bet failed", + "pending_payout": "Pending payout", + "settled_win": "Settled win", + "settled_lose": "Settled loss" + }, + "allTickets": "All tickets" } diff --git a/src/i18n/locales/ne/adminUsers.json b/src/i18n/locales/ne/adminUsers.json index 1cb9dce..a6ed515 100644 --- a/src/i18n/locales/ne/adminUsers.json +++ b/src/i18n/locales/ne/adminUsers.json @@ -14,6 +14,17 @@ "saveAccountFailed": "खाता सुरक्षित गर्न असफल", "deleteSuccess": "{{name}} मेटाइयो", "deleteFailed": "मेटाउन असफल", + "roleLoadFailed": "भूमिका सूची लोड असफल भयो", + "roleListTitle": "भूमिका व्यवस्थापन", + "createRole": "भूमिका थप्नुहोस्", + "roleCreateSuccess": "भूमिका {{name}} सिर्जना भयो", + "roleUpdateSuccess": "भूमिका {{name}} अपडेट भयो", + "roleSaveFailed": "भूमिका सुरक्षित गर्न असफल", + "roleDeleteSuccess": "भूमिका {{name}} मेटाइयो", + "roleDeleteFailed": "भूमिका मेटाउन असफल", + "rolePermissionSaveSuccess": "भूमिका अनुमति अद्यावधिक भयो", + "rolePermissionSaveFailed": "भूमिका अनुमति सुरक्षित गर्न असफल", + "roleFormRequired": "भूमिका नाम र slug अनिवार्य छन्", "allPermissions": "सबै अनुमति", "saveRoleSuccess": "{{name}} को भूमिका अपडेट भयो", "saveRoleFailed": "भूमिका सुरक्षित गर्न असफल", @@ -36,6 +47,10 @@ "enabled": "सक्रिय", "disabled": "निष्क्रिय" }, + "roleType": { + "system": "सिस्टम", + "custom": "अनुकूलित" + }, "actions": { "permissions": "भूमिका तोक्नुहोस्", "edit": "सम्पादन", @@ -43,6 +58,18 @@ "cancel": "रद्द गर्नुहोस्", "save": "सेभ गर्नुहोस्" }, + "roleTable": { + "name": "भूमिका", + "slug": "角色 कोड", + "type": "प्रकार", + "status": "स्थिति", + "users": "सम्बन्धित प्रयोगकर्ता", + "permissions": "अनुमति संख्या", + "actions": "कार्य" + }, + "roleActions": { + "permissions": "अनुमति" + }, "permissionDialog": { "title": "भूमिका तोक्नुहोस्", "rolesTitle": "भूमिका", @@ -51,6 +78,18 @@ "selectedRoles": "हाल छनोट गरिएका भूमिका:", "saveRoles": "भूमिका सेभ गर्नुहोस्" }, + "rolePermissionDialog": { + "title": "भूमिका अनुमति" + }, + "roleDialog": { + "createTitle": "भूमिका थप्नुहोस्", + "editTitle": "भूमिका सम्पादन", + "description": "भूमिकाले ब्याकएन्ड कार्य अनुमति समेट्छ र पछि प्रशासक खातालाई बाँडिन्छ।", + "slug": "भूमिका कोड", + "name": "भूमिका नाम", + "descriptionLabel": "भूमिका विवरण", + "status": "स्थिति" + }, "accountDialog": { "createTitle": "प्रशासक सिर्जना", "editTitle": "खाता सम्पादन", @@ -75,5 +114,60 @@ "rowActionTitle": "यो प्रशासक मेटाउनुहोस्", "confirmTitle": "मेटाउने पुष्टि", "confirmDescription": "प्रशासक {{name}} मेटाउने? यो कार्य फिर्ता लिन सकिँदैन।" + }, + "roleDelete": { + "confirmTitle": "भूमिका मेटाउने पुष्टि", + "confirmDescription": "भूमिका {{name}} मेटाउने?" + }, + "permissionGroups": { + "all": "सबै अनुमति", + "dashboard": "ड्यासबोर्ड", + "admin_users": "प्रशासक सूची", + "admin_roles": "भूमिका व्यवस्थापन", + "players": "खेलाडी सूची", + "wallet": "वालेट", + "draws": "ड्रअ सूची", + "config": "कन्फिगरेसन", + "risk": "जोखिम", + "settlement": "सेटलमेन्ट", + "jackpot": "ज्याकपोट", + "reconcile": "मिलान", + "tickets": "टिकटहरू", + "reports": "रिपोर्टहरू", + "audit": "अडिट लग", + "settings": "सेटिङ" + }, + "permissionNames": { + "prd.admin_user.manage": "प्रशासक सूची · व्यवस्थापन", + "prd.admin_role.manage": "भूमिका व्यवस्थापन · व्यवस्थापन", + "prd.users.manage": "खेलाडी व्यवस्थापन · व्यवस्थापन", + "prd.users.view_finance": "खेलाडी व्यवस्थापन · वित्त हेर्नुहोस्", + "prd.users.view_cs": "खेलाडी व्यवस्थापन · ग्राहक सेवा एकल प्रयोगकर्ता", + "prd.player_freeze.manage": "खेलाडी रोक्ने/फुकाउने · व्यवस्थापन", + "prd.wallet_reconcile.manage": "वालेट मिलान · व्यवस्थापन", + "prd.wallet_reconcile.view": "वालेट मिलान · हेर्नुहोस्", + "prd.wallet_reconcile.view_cs": "वालेट मिलान · ग्राहक सेवा दृश्य", + "prd.wallet_adjust.manage": "समायोजन/रिभर्सल · व्यवस्थापन", + "prd.draw_result.manage": "ड्रअ परिणाम प्रविष्टि · व्यवस्थापन", + "prd.draw_result.view": "ड्रअ परिणाम · हेर्नुहोस्", + "prd.draw_reopen.manage": "ड्रअ पुनःखोल्ने · व्यवस्थापन", + "prd.play_switch.manage": "प्ले स्विच · व्यवस्थापन", + "prd.odds.manage": "ओड्स कन्फिगरेसन · व्यवस्थापन", + "prd.risk_cap.manage": "जोखिम सीमा · व्यवस्थापन", + "prd.risk_cap.view": "जोखिम सीमा · हेर्नुहोस्", + "prd.rebate.manage": "कमिसन/रिबेट · व्यवस्थापन", + "prd.rebate.view": "कमिसन/रिबेट · हेर्नुहोस्", + "prd.jackpot.manage": "ज्याकपोट कन्फिगरेसन · व्यवस्थापन", + "prd.jackpot.view": "ज्याकपोट कन्फिगरेसन · हेर्नुहोस्", + "prd.payout.manage": "भुक्तानी पुष्टि · व्यवस्थापन", + "prd.payout.review": "भुक्तानी पुष्टि · समीक्षा", + "prd.payout.view": "भुक्तानी पुष्टि · हेर्नुहोस्", + "prd.report.all": "रिपोर्ट · सबै", + "prd.report.risk": "रिपोर्ट · जोखिम", + "prd.report.finance": "रिपोर्ट · वित्त", + "prd.report.player": "रिपोर्ट · एकल खेलाडी", + "prd.audit.all": "अडिट लग · सबै", + "prd.audit.self": "अडिट लग · आफूसँग सम्बन्धित", + "prd.audit.finance": "अडिट लग · वित्त सम्बन्धित" } } diff --git a/src/i18n/locales/ne/common.json b/src/i18n/locales/ne/common.json index 7e02efa..e7567ff 100644 --- a/src/i18n/locales/ne/common.json +++ b/src/i18n/locales/ne/common.json @@ -46,6 +46,9 @@ "errors": { "loadFailed": "लोड असफल भयो" }, + "table": { + "id": "ID" + }, "toolbar": { "defaultAdmin": "प्रशासक", "notifications": "सूचना", @@ -57,6 +60,7 @@ "home": "गृह", "dashboard": "ड्यासबोर्ड", "admin_users": "प्रशासक सूची", + "admin_roles": "भूमिका व्यवस्थापन", "players": "खेलाडी सूची", "wallet": "वालेट", "draws": "ड्रअहरू", @@ -65,7 +69,7 @@ "settlement": "सेटलमेन्ट", "jackpot": "Jackpot", "reconcile": "मिलान", - "tickets": "टिकटहरू", + "tickets": "टिकट सूची", "reports": "रिपोर्टहरू", "audit": "अडिट लग", "settings": "सेटिङ" diff --git a/src/i18n/locales/ne/config.json b/src/i18n/locales/ne/config.json index 638d68a..fd4b60d 100644 --- a/src/i18n/locales/ne/config.json +++ b/src/i18n/locales/ne/config.json @@ -5,15 +5,14 @@ "sidebarTitle": "सञ्चालन कन्फिगरेसन", "groups": { "betting": "बेटिङ र प्रदर्शन", - "risk_wallet": "जोखिम र कोष" + "risk": "जोखिम नियन्त्रण" }, "items": { "plays": "खेल प्रकार र सीमा", "odds": "अड्स", "rebate": "कमिसन / रिबेट", "jackpot": "Jackpot पूल", - "risk-cap": "पेमेन्ट क्याप", - "wallet": "वालेट थ्रेसहोल्ड" + "risk-cap": "पेमेन्ट क्याप" } }, "versionStatus": { @@ -70,6 +69,31 @@ }, "discard": "परिवर्तन त्याग्नुहोस्" }, + "system": { + "title": "ड्रअ र सेटलमेन्ट रनटाइम सेटिङ", + "runtimeTitle": "ग्लोबल रनटाइम प्यारामिटर", + "runtimeIntro1": "यहाँ खेल प्रकार, अड्स वा जोखिम संस्करणमा नपर्ने ग्लोबल प्रणाली प्यारामिटर राखिन्छ। यसले वालेट ट्रान्सफर, कार्य स्विच र प्रणाली सञ्चालन नीतिमा सीधा असर गर्छ।", + "runtimeIntro2": "खेल प्रकार, अड्स, रिबेट र क्याप अझै पनि सञ्चालन कन्फिगरेसनमै रहन्छन्। प्रणाली सेटिङले मात्र क्रस-मोड्युल रनटाइम प्यारामिटर सम्हाल्छ ताकि प्रशासनिक जिम्मेवारी नदोहोरोस्।", + "description": "RNG ड्रअपछि समीक्षा प्रवाह, कूलडाउन अवधि र स्वचालित सेटलमेन्ट व्यवहार नियन्त्रण गर्छ। यी ग्लोबल रनटाइम नीति हुन् र संस्करणयुक्त सञ्चालन कन्फिगरेसनमा पर्दैनन्।", + "loadFailed": "प्रणाली सेटिङ लोड असफल भयो", + "saveSuccess": "प्रणाली सेटिङ सुरक्षित भयो", + "saveFailed": "प्रणाली सेटिङ सुरक्षित गर्न असफल", + "fields": { + "manualReview": "ड्रअ परिणामका लागि म्यानुअल समीक्षा चाहिने", + "cooldownMinutes": "कूलडाउन अवधि (मिनेट)", + "autoSettlement": "सेटलमेन्ट स्वतः चलाउने" + }, + "hints": { + "manualReview": "सक्रिय हुँदा RNG ड्रअ परिणाम pending review मा जान्छ र एडमिनबाट म्यानुअल रूपमा प्रकाशित गर्नुपर्छ।", + "cooldownMinutes": "प्रकाशनपछि settling मा जानुअघि कति समय पर्खने। 0 राखे तुरुन्त सेटलमेन्ट सुरु हुन्छ।", + "autoSettlement": "बन्द हुँदा tick ले सेटलमेन्ट स्वतः चलाउँदैन र एडमिनले म्यानुअल रूपमा ट्रिगर गर्नुपर्छ।" + }, + "states": { + "enabled": "सक्रिय", + "disabled": "बन्द" + }, + "discard": "परिवर्तन त्याग्नुहोस्" + }, "play": { "batchGroups": { "d2": "2D ग्लोबल", @@ -79,6 +103,164 @@ "position": "स्थिति खेलहरू", "box": "बक्स खेलहरू", "jackpot": "Jackpot" + }, + "validation": { + "minMaxInvalid": "{{playCode}}: न्यूनतम बेट अधिकतम बेटभन्दा ठूलो हुन सक्दैन" + }, + "publishFailed": "प्रकाशन असफल भयो", + "createDraftSuccess": "ड्राफ्ट v{{version}} सिर्जना भयो", + "createDraftFailed": "ड्राफ्ट सिर्जना असफल भयो", + "ruleSavedLocal": "नियम पाठ स्थानीय ड्राफ्टमा सुरक्षित भयो। स्थायी बनाउन ड्राफ्ट सेभ गर्नुहोस्।", + "deleteFailed": "मेटाउन असफल", + "activeVersion": "हाल सक्रिय संस्करण v{{version}}", + "readOnlyHint": "सीमा र नियमहरू केवल पढ्न मिल्ने छन्। पहिले ड्राफ्ट बनाउनुहोस्।", + "batchSwitchesTitle": "समूह स्विचहरू", + "batchSwitchesDesc": "यसले हालको ड्राफ्ट मात्र अपडेट गर्छ। सेभ र प्रकाशित गरेपछि खेलाडीको बेटिङ तालिका रिफ्रेस हुन्छ।", + "readOnlyDraftHint": "हालको संस्करण केवल पढ्न मिल्ने छ। पहिले ड्राफ्ट बनाउनुहोस्।", + "batchEnabledCount": "{{enabledCount}}/{{total}} सक्रिय", + "noPlayTypes": "खेल प्रकार छैन", + "actions": { + "enable": "सक्रिय", + "disable": "निष्क्रिय", + "ruleText": "नियम पाठ" + }, + "table": { + "playCode": "खेल कोड", + "category": "श्रेणी", + "status": "स्थिति", + "displayName": "प्रदर्शित नाम", + "order": "क्रम", + "minBet": "न्यूनतम बेट", + "maxBet": "अधिकतम बेट", + "actions": "कार्य" + }, + "states": { + "enabled": "सक्रिय", + "disabled": "बन्द", + "readOnly": "केवल पढ्न मिल्ने" + }, + "aria": { + "enablePlay": "{{playCode}} सक्रिय गर्ने" + }, + "ruleDialog": { + "title": "नियम पाठ (Chinese)", + "description": "खेल {{playCode}}। परिवर्तनहरू सेभ र प्रकाशित नगरेसम्म ड्राफ्टमै रहन्छन्।", + "fieldLabel": "rule_text_zh", + "apply": "ड्राफ्टमा लागू गर्नुहोस्" + } + }, + "odds": { + "tabs": { + "all": "सबै" + }, + "category": "श्रेणी", + "playType": "खेल प्रकार", + "noPlayTypes": "यस श्रेणीमा खेल प्रकार छैन।", + "sheetDescription": "यहाँ हेर्नका लागि एउटा संस्करण छान्नुहोस्। गैर-ड्राफ्ट संस्करणलाई नयाँ ड्राफ्टमा रोलब्याक गर्न सकिन्छ।", + "activeVersionPrefix": "हाल सक्रिय संस्करण:", + "readOnlyHint": "यो संस्करण केवल पढ्न मिल्ने छ। अड्स सम्पादन गर्नुअघि ड्राफ्ट बनाउनुहोस्।", + "loadingDetails": "विवरण लोड हुँदैछ…", + "multiplier": "गुणक x{{value}} · {{currency}}", + "missingScopeRow": "{{scope}} को row हराइरहेको छ। seed वा version data जाँच गर्नुहोस्।", + "rebateRate": "रिबेट दर (%)", + "rebateRateHint": "यसले यो खेल प्रकारअन्तर्गत सबै prize scope मा rebate_rate लेख्छ।", + "publishFailed": "प्रकाशन असफल भयो", + "createDraftSuccess": "ड्राफ्ट v{{version}} सिर्जना भयो", + "createDraftFailed": "ड्राफ्ट सिर्जना असफल भयो", + "rollbackSuccess": "v{{fromVersion}} बाट नयाँ ड्राफ्ट v{{version}} क्लोन गरियो", + "rollbackFailed": "रोलब्याक असफल भयो", + "deleteFailed": "मेटाउन असफल", + "rollbackDialog": { + "title": "रोलब्याक पुष्टि गर्नुहोस्", + "description": "संस्करण v{{version}} बाट नयाँ ड्राफ्ट क्लोन हुनेछ। हाल सक्रिय संस्करण प्रत्यक्ष रूपमा ओभरराइट हुँदैन।", + "confirm": "रोलब्याक पुष्टि गर्नुहोस्" + }, + "publishDialog": { + "title": "अड्स संस्करण प्रकाशित गर्ने?", + "description": "नयाँ अड्सले तुरुन्तै नयाँ टिकटहरूमा असर गर्छ। सफल भइसकेका टिकटहरू आफ्नो सुरक्षित odds snapshot अनुसार नै सेटल हुन्छन्।", + "confirm": "प्रकाशन पुष्टि गर्नुहोस्", + "columns": { + "prizeScope": "पुरस्कार दायरा", + "currentActive": "हाल सक्रिय", + "afterPublish": "प्रकाशनपछि" + } + } + }, + "rebate": { + "sheetDescription": "रिबेट अड्स ड्राफ्ट संस्करणमा राखिन्छ र अड्ससँग एउटै संस्करण सेट साझा गर्छ।", + "publishLabel": "प्रकाशन", + "publishSuccess": "रिबेटसहितको अड्स संस्करण प्रकाशित भयो", + "publishFailed": "प्रकाशन असफल भयो", + "createDraftSuccess": "ड्राफ्ट v{{version}} सिर्जना भयो", + "createDraftFailed": "ड्राफ्ट सिर्जना असफल भयो", + "deleteFailed": "मेटाउन असफल", + "editingVersion": "सम्पादन भइरहेको संस्करण v{{version}} · {{status}}", + "readOnlyHint": "रिबेट सम्पादन गर्नुअघि ड्राफ्ट बनाउनुहोस्।", + "fields": { + "d2": "2D रिबेट दर (%)", + "d3": "3D रिबेट दर (%)", + "d4": "4D रिबेट दर (%)" + }, + "winEnjoy": { + "label": "जितेका टिकटहरूमा पनि रिबेट लागू गर्ने", + "description": "यो placeholder field हो। पछि risk र settlement नियमसँग मिलाएर स्थायी रूपमा राख्न सकिन्छ।" + }, + "effectiveTime": "लागू समय (हाल सक्रिय अड्स संस्करण)" + }, + "riskCap": { + "validation": { + "requireAtLeastOne": "कम्तीमा एक क्याप row आवश्यक छ", + "defaultGreaterThanZero": "पूर्वनिर्धारित क्याप रकम 0 भन्दा ठूलो हुनुपर्छ", + "numberMustBe4Digits": "नम्बर 4 अङ्कको हुनुपर्छ: {{number}}", + "enterValidCapAmount": "मान्य क्याप रकम प्रविष्ट गर्नुहोस्" + }, + "publishFailed": "प्रकाशन असफल भयो", + "createDraftSuccess": "ड्राफ्ट v{{version}} सिर्जना भयो", + "createDraftFailed": "ड्राफ्ट सिर्जना असफल भयो", + "savedLocalDraft": "स्थानीय ड्राफ्टमा सुरक्षित भयो। स्थायी बनाउन ड्राफ्ट सेभ गर्नुहोस्।", + "deleteFailed": "मेटाउन असफल", + "effectiveAt": "लागू समय: {{value}}", + "note": "टिप्पणी: {{value}}", + "readOnlyHint": "केवल पढ्न मिल्ने। पहिले ड्राफ्ट बनाउनुहोस्।", + "readOnly": "केवल पढ्न मिल्ने", + "defaultCap": { + "title": "पूर्वनिर्धारित क्याप", + "description": "विशेष क्याप नभएका नम्बरहरूमा यही पूर्वनिर्धारित क्याप टेम्प्लेट लागू हुन्छ।", + "fieldLabel": "क्याप रकम (सानो एकाइ)" + }, + "specialCaps": { + "title": "विशेष क्यापहरू" + }, + "loadingDetails": "विवरण लोड हुँदैछ…", + "noDetailRows": "विवरण row छैन।", + "table": { + "number": "नम्बर", + "capAmount": "क्याप रकम", + "used": "प्रयोग भएको", + "remaining": "बाँकी", + "soldOut": "सोल्ड आउट", + "ratio": "अनुपात", + "actions": "कार्य" + }, + "occupancy": { + "title": "सबै नम्बर occupancy", + "description": "यो placeholder दृश्य हो। filter र export ले ticket-summary एकीकरण अझै चाहिन्छ। तलको data अहिले पनि हालको ड्राफ्ट सूचीबाट आउँछ।", + "searchLabel": "नम्बर खोज्नुहोस्", + "searchPlaceholder": "जस्तै 8888", + "filterPending": "Sold-out / high-risk preset filter अझै एकीकृत भएको छैन", + "exportPending": "CSV export अझै एकीकृत भएको छैन" + }, + "actions": { + "update": "अपडेट", + "addSpecialCap": "+ विशेष क्याप थप्नुहोस्", + "filterPresets": "प्रिसेट फिल्टर…", + "exportCsv": "CSV निर्यात", + "close": "बन्द" + }, + "syncDialog": { + "title": "पूर्वनिर्धारित क्याप मिलाउनुहोस्", + "description": "पूर्वनिर्धारित क्याप टेम्प्लेट {{value}} मा सेट हुनेछ। यसले ड्राफ्ट मात्र बदल्छ। पुष्टि पछि सेभ र प्रकाशित गर्नुहोस्।", + "confirm": "पुष्टि" } } } diff --git a/src/i18n/locales/ne/jackpot.json b/src/i18n/locales/ne/jackpot.json index 156bee4..f51b22e 100644 --- a/src/i18n/locales/ne/jackpot.json +++ b/src/i18n/locales/ne/jackpot.json @@ -4,7 +4,7 @@ "loadFailed": "लोड असफल भयो", "saveSuccess": "सुरक्षित भयो", "saveFailed": "सुरक्षित गर्न असफल", - "invalidDrawId": "मान्य ड्रअ ID लेख्नुहोस्", + "invalidDrawId": "मान्य ड्रअ नम्बर लेख्नुहोस्", "manualBurstSuccess": "Jackpot म्यानुअल रूपमा ट्रिगर भयो", "manualBurstFailed": "म्यानुअल बर्स्ट असफल भयो", "noPoolData": "पूल डाटा छैन", @@ -21,7 +21,7 @@ "enabled": "खुला", "saving": "सुरक्षित हुँदैछ…", "save": "सुरक्षित गर्नुहोस्", - "manualBurstDrawId": "म्यानुअल बर्स्ट ड्रअ ID", + "manualBurstDrawId": "म्यानुअल बर्स्ट ड्रअ नम्बर", "manualBurstAmount": "बर्स्ट रकम (खाली भए सबै)", "processing": "प्रक्रियामा…", "manualBurst": "म्यानुअल बर्स्ट", @@ -31,12 +31,22 @@ "apply": "लागू गर्नुहोस्", "payoutRecords": "Jackpot भुक्तानी रेकर्ड", "contributionRecords": "Jackpot योगदान रेकर्ड", + "recordsPage": { + "title": "Jackpot रेकर्ड", + "description": "भुक्तानी रेकर्ड र पूल योगदान प्रवाह" + }, "subnavLabel": "Jackpot उपनेभिगेसन", "subnavPools": "पूल कन्फिगरेसन", "subnavRecords": "रेकर्ड", "payoutLoadFailed": "भुक्तानी रेकर्ड लोड असफल भयो", "contributionLoadFailed": "योगदान रेकर्ड लोड असफल भयो", "trigger": "ट्रिगर", + "triggerTypes": { + "threshold": "सीमा पुगेपछि", + "forced_gap": "लगातार नफुट्दा जबरजस्ती ट्रिगर", + "play_combo": "निर्दिष्ट प्ले कम्बो ट्रिगर", + "manual": "म्यानुअल ट्रिगर" + }, "payoutAmount": "भुक्तानी रकम", "winnerCount": "विजेता संख्या", "time": "समय", diff --git a/src/i18n/locales/ne/reports.json b/src/i18n/locales/ne/reports.json index f6d73f4..19d1ad9 100644 --- a/src/i18n/locales/ne/reports.json +++ b/src/i18n/locales/ne/reports.json @@ -3,7 +3,7 @@ "createExport": "निर्यात सिर्जना", "reportType": "रिपोर्ट प्रकार", "exportFormat": "निर्यात ढाँचा", - "filterJson": "filter_json (वैकल्पिक)", + "filterJson": "फिल्टर JSON (वैकल्पिक)", "parseFilterFailed": "फिल्टर JSON पार्स गर्न सकिएन", "createSuccess": "निर्यात कार्य सिर्जना भयो", "createFailed": "कार्य सिर्जना असफल भयो", @@ -18,6 +18,17 @@ "createdAt": "सिर्जना समय", "id": "ID", "empty": "डाटा छैन", + "formatOptions": { + "csv": "CSV", + "xlsx": "Excel" + }, + "statusOptions": { + "pending": "पेन्डिङ", + "queued": "पर्खाइमा", + "running": "चल्दैछ", + "completed": "सम्पन्न", + "failed": "असफल" + }, "reportTypes": { "draw_profit_summary": "ड्रअ नाफा सारांश", "daily_profit_summary": "दैनिक नाफा सारांश", diff --git a/src/i18n/locales/ne/settlement.json b/src/i18n/locales/ne/settlement.json index 4fa031b..01c8237 100644 --- a/src/i18n/locales/ne/settlement.json +++ b/src/i18n/locales/ne/settlement.json @@ -58,6 +58,7 @@ "matchedTier": "मिलेको स्तर", "regularPayout": "सामान्य भुक्तानी", "loadingDetails": "विवरण लोड हुँदैछ…", + "invalidBatchId": "अमान्य सेटलमेन्ट ब्याच नम्बर", "statusOptions": { "all": "सबै", "running": "चलिरहेको", @@ -67,5 +68,10 @@ "paid": "भुक्तानी भयो", "completed": "सम्पन्न", "failed": "असफल" + }, + "reviewStatusOptions": { + "pending": "समीक्षा बाँकी", + "approved": "स्वीकृत", + "rejected": "अस्वीकृत" } } diff --git a/src/i18n/locales/ne/tickets.json b/src/i18n/locales/ne/tickets.json index 10a0e86..3ea410a 100644 --- a/src/i18n/locales/ne/tickets.json +++ b/src/i18n/locales/ne/tickets.json @@ -1,19 +1,42 @@ { - "title": "टिकट", - "playerTicketQuery": "खेलाडी टिकट खोज", - "playerId": "खेलाडी ID", - "invalidPlayerId": "मान्य खेलाडी ID लेख्नुहोस्", - "drawNoOptional": "ड्रअ नं. (वैकल्पिक)", + "title": "टिकट सूची", + "playerTicketQuery": "टिकट खोज", + "playerId": "खेलाडी ID / खाता", + "invalidPlayerId": "मान्य खेलाडी ID वा खाता लेख्नुहोस्", + "playerIdPlaceholder": "सबै देखाउन खाली छोड्नुहोस्; ID वा खाता लेख्नुहोस्", + "drawNoOptional": "ड्रअ नम्बर (वैकल्पिक)", "drawNoPlaceholder": "जस्तै 20260520-001", + "numberKeyword": "नम्बर / टिकट / अर्डर", + "numberKeywordPlaceholder": "नम्बर, टिकट नं. वा अर्डर नं. बाट खोज्नुहोस्", + "placedDateRange": "बेट गरिएको मिति दायरा", "query": "खोज", + "resetFilters": "फिल्टर रिसेट", + "refreshCurrentPage": "हालको पृष्ठ रिफ्रेस", "loadFailed": "लोड असफल भयो", "ticketNo": "टिकट नं.", + "player": "खेलाडी", "orderNo": "अर्डर नं.", "drawNo": "ड्रअ नं.", "playCode": "प्ले", "number": "नम्बर", + "betAmount": "बेट रकम", "actualDeduct": "कटौती", "status": "स्थिति", "failReason": "असफल कारण", - "winAmount": "जित रकम" + "winAmount": "जित रकम", + "placedAt": "बेट समय", + "updatedAt": "अपडेट समय", + "statusFilterLabel": "स्थिति फिल्टर", + "statusHint": "धेरै चयन गर्न सकिन्छ। खाली छोडे सबै स्थिति देखिन्छ।", + "statusOptions": { + "all": "सबै", + "pending_confirm": "पुष्टि बाँकी", + "partial_pending_confirm": "आंशिक पुष्टि बाँकी", + "success": "बेट सफल", + "failed": "बेट असफल", + "pending_payout": "भुक्तानी बाँकी", + "settled_win": "जित सेटल भयो", + "settled_lose": "हार सेटल भयो" + }, + "allTickets": "सबै टिकट" } diff --git a/src/i18n/locales/zh/adminUsers.json b/src/i18n/locales/zh/adminUsers.json index 0125bff..25231a8 100644 --- a/src/i18n/locales/zh/adminUsers.json +++ b/src/i18n/locales/zh/adminUsers.json @@ -60,7 +60,7 @@ }, "roleTable": { "name": "角色", - "slug": "标识", + "slug": "角色编码", "type": "类型", "status": "状态", "users": "关联用户", @@ -85,7 +85,7 @@ "createTitle": "新增角色", "editTitle": "编辑角色", "description": "角色用于归拢后台功能权限,再分配给管理员账号。", - "slug": "角色标识", + "slug": "角色编码", "name": "角色名称", "descriptionLabel": "角色说明", "status": "状态" @@ -118,5 +118,56 @@ "roleDelete": { "confirmTitle": "删除角色", "confirmDescription": "确认删除角色 {{name}}?" + }, + "permissionGroups": { + "all": "全部权限", + "dashboard": "仪表盘", + "admin_users": "管理列表", + "admin_roles": "角色管理", + "players": "玩家列表", + "wallet": "钱包流水", + "draws": "期号列表", + "config": "运营配置", + "risk": "风控", + "settlement": "结算", + "jackpot": "奖池", + "reconcile": "对账", + "tickets": "玩家注单", + "reports": "报表导出", + "audit": "审计日志", + "settings": "系统设置" + }, + "permissionNames": { + "prd.admin_user.manage": "管理员列表·可管理", + "prd.admin_role.manage": "角色管理·可管理", + "prd.users.manage": "用户管理·可管理", + "prd.users.view_finance": "用户管理·财务查看", + "prd.users.view_cs": "用户管理·客服单用户", + "prd.player_freeze.manage": "冻结/解冻玩家·可管理", + "prd.wallet_reconcile.manage": "钱包对账·可管理", + "prd.wallet_reconcile.view": "钱包对账·查看", + "prd.wallet_reconcile.view_cs": "钱包对账·客服单用户", + "prd.wallet_adjust.manage": "补单/冲正·可管理", + "prd.draw_result.manage": "开奖结果录入·可管理", + "prd.draw_result.view": "开奖结果·查看", + "prd.draw_reopen.manage": "开奖结果重开·可管理", + "prd.play_switch.manage": "玩法开关·可管理", + "prd.odds.manage": "赔率配置·可管理", + "prd.risk_cap.manage": "封顶配置·可管理", + "prd.risk_cap.view": "封顶配置·查看", + "prd.rebate.manage": "佣金/回水·可管理", + "prd.rebate.view": "佣金/回水·查看", + "prd.jackpot.manage": "奖池配置·可管理", + "prd.jackpot.view": "奖池配置·查看", + "prd.payout.manage": "派彩确认·可管理", + "prd.payout.review": "派彩确认·可审核", + "prd.payout.view": "派彩确认·查看", + "prd.report.all": "报表·全部", + "prd.report.risk": "报表·风控", + "prd.report.finance": "报表·财务", + "prd.report.player": "报表·单用户", + "prd.audit.all": "审计日志·全部", + "prd.audit.self": "审计日志·自身相关", + "prd.audit.finance": "审计日志·资金相关" } } diff --git a/src/i18n/locales/zh/audit.json b/src/i18n/locales/zh/audit.json index 8da6524..95293e8 100644 --- a/src/i18n/locales/zh/audit.json +++ b/src/i18n/locales/zh/audit.json @@ -1,10 +1,10 @@ { "title": "审计日志", - "moduleCode": "模块编码", - "actionCode": "动作编码", + "moduleCode": "模块", + "actionCode": "动作", "operatorType": "操作者类型", - "exactMatch": "精确匹配", - "operatorTypePlaceholder": "如 admin / system", + "exactMatch": "请输入完整名称", + "operatorTypePlaceholder": "如管理员、系统", "operator": "操作者", "module": "模块", "action": "动作", diff --git a/src/i18n/locales/zh/common.json b/src/i18n/locales/zh/common.json index 7c8d2d2..fe7fba5 100644 --- a/src/i18n/locales/zh/common.json +++ b/src/i18n/locales/zh/common.json @@ -46,6 +46,9 @@ "errors": { "loadFailed": "加载失败" }, + "table": { + "id": "ID" + }, "toolbar": { "defaultAdmin": "管理员", "notifications": "通知", @@ -64,9 +67,9 @@ "config": "运营配置", "risk": "风控", "settlement": "结算", - "jackpot": "Jackpot", + "jackpot": "奖池", "reconcile": "对账", - "tickets": "玩家注单", + "tickets": "注单列表", "reports": "报表导出", "audit": "审计日志", "settings": "系统设置" diff --git a/src/i18n/locales/zh/config.json b/src/i18n/locales/zh/config.json index 28a4e78..4db609b 100644 --- a/src/i18n/locales/zh/config.json +++ b/src/i18n/locales/zh/config.json @@ -5,15 +5,14 @@ "sidebarTitle": "运营配置导航", "groups": { "betting": "投注与展示", - "risk_wallet": "风控与资金" + "risk": "风控" }, "items": { "plays": "玩法与限额", "odds": "赔率", "rebate": "佣金 / 回水", - "jackpot": "Jackpot 奖池", - "risk-cap": "赔付封顶", - "wallet": "钱包阈值" + "jackpot": "奖池配置", + "risk-cap": "赔付封顶" } }, "versionStatus": { @@ -70,6 +69,31 @@ }, "discard": "放弃更改" }, + "system": { + "title": "开奖与结算运行参数", + "runtimeTitle": "全局运行参数", + "runtimeIntro1": "这里放不属于玩法版本、赔率版本、风控版本的全局系统参数。它们会直接影响钱包转账、任务开关或系统运行策略。", + "runtimeIntro2": "玩法、赔率、回水、封顶仍然统一放在“运营配置”里管理;系统设置只承接跨模块的运行参数,避免后台入口职责重叠。", + "description": "用于控制 RNG 开奖后的审核流转、冷静期时长和系统自动结算行为。这些参数属于全局运行策略,不跟随玩法/赔率版本发布。", + "loadFailed": "系统设置加载失败", + "saveSuccess": "系统设置已保存", + "saveFailed": "系统设置保存失败", + "fields": { + "manualReview": "开奖结果必须人工审核", + "cooldownMinutes": "冷静期时长(分钟)", + "autoSettlement": "自动执行结算" + }, + "hints": { + "manualReview": "开启后,RNG 开奖结果会先进入待审核,必须由后台人工发布。", + "cooldownMinutes": "结果发布后等待多久再进入 settling。填 0 表示发布后直接进入结算。", + "autoSettlement": "关闭后,tick 不会自动跑结算,只能由后台手工执行。" + }, + "states": { + "enabled": "已开启", + "disabled": "已关闭" + }, + "discard": "放弃更改" + }, "play": { "batchGroups": { "d2": "2D 全局", @@ -78,7 +102,165 @@ "big-small": "Big / Small", "position": "位置类玩法", "box": "包号类玩法", - "jackpot": "Jackpot" + "jackpot": "奖池" + }, + "validation": { + "minMaxInvalid": "{{playCode}}:最小下注额不能大于最大下注额" + }, + "publishFailed": "发布失败", + "createDraftSuccess": "已创建草稿 v{{version}}", + "createDraftFailed": "创建草稿失败", + "ruleSavedLocal": "规则文案已写入本地草稿,记得保存草稿后再发布。", + "deleteFailed": "删除失败", + "activeVersion": "当前生效版本 v{{version}}", + "readOnlyHint": "当前限额与规则为只读,请先创建草稿。", + "batchSwitchesTitle": "批量开关", + "batchSwitchesDesc": "这里只会修改当前草稿;保存并发布后,玩家下注表会按新配置刷新。", + "readOnlyDraftHint": "当前版本为只读,请先创建草稿。", + "batchEnabledCount": "{{enabledCount}}/{{total}} 已开启", + "noPlayTypes": "暂无玩法", + "actions": { + "enable": "开启", + "disable": "关闭", + "ruleText": "规则文案" + }, + "table": { + "playCode": "玩法编码", + "category": "分类", + "status": "状态", + "displayName": "显示名称", + "order": "排序", + "minBet": "最小下注", + "maxBet": "最大下注", + "actions": "操作" + }, + "states": { + "enabled": "开启", + "disabled": "关闭", + "readOnly": "只读" + }, + "aria": { + "enablePlay": "切换 {{playCode}} 启用状态" + }, + "ruleDialog": { + "title": "规则文案(中文)", + "description": "玩法 {{playCode}};修改内容只会暂存到草稿,保存并发布后才会生效。", + "fieldLabel": "中文规则文案", + "apply": "应用到草稿" + } + }, + "odds": { + "tabs": { + "all": "全部" + }, + "category": "分类", + "playType": "玩法类型", + "noPlayTypes": "该分类下暂无玩法。", + "sheetDescription": "选择一个版本在此查看;非草稿版本可以回滚成新的草稿。", + "activeVersionPrefix": "当前生效版本:", + "readOnlyHint": "当前版本为只读,请先创建草稿后再修改赔率。", + "loadingDetails": "正在加载详情…", + "multiplier": "倍数 x{{value}} · {{currency}}", + "missingScopeRow": "缺少 {{scope}} 对应行,请检查种子或版本数据。", + "rebateRate": "回水比例 (%)", + "rebateRateHint": "会把 rebate_rate 写入该玩法下所有奖级范围。", + "publishFailed": "发布失败", + "createDraftSuccess": "已创建草稿 v{{version}}", + "createDraftFailed": "创建草稿失败", + "rollbackSuccess": "已从 v{{fromVersion}} 克隆出新草稿 v{{version}}", + "rollbackFailed": "回滚失败", + "deleteFailed": "删除失败", + "rollbackDialog": { + "title": "确认回滚", + "description": "系统会基于版本 v{{version}} 克隆出新的草稿,不会直接覆盖当前生效版本。", + "confirm": "确认回滚" + }, + "publishDialog": { + "title": "确认发布赔率版本?", + "description": "新赔率会立即影响后续新注单;已成功下注的历史注单仍按各自保存的赔率快照结算。", + "confirm": "确认发布", + "columns": { + "prizeScope": "奖级范围", + "currentActive": "当前生效", + "afterPublish": "发布后" + } + } + }, + "rebate": { + "sheetDescription": "回水配置存放在赔率草稿版本中,与赔率共用同一套版本记录。", + "publishLabel": "发布", + "publishSuccess": "已发布带回水的赔率版本", + "publishFailed": "发布失败", + "createDraftSuccess": "已创建草稿 v{{version}}", + "createDraftFailed": "创建草稿失败", + "deleteFailed": "删除失败", + "editingVersion": "当前编辑版本 v{{version}} · {{status}}", + "readOnlyHint": "修改回水前请先创建草稿。", + "fields": { + "d2": "2D 回水比例 (%)", + "d3": "3D 回水比例 (%)", + "d4": "4D 回水比例 (%)" + }, + "winEnjoy": { + "label": "中奖注单也应用回水", + "description": "这是预留字段,后续可和风控、结算规则对齐后再真正落库存储。" + }, + "effectiveTime": "生效时间(当前赔率生效版本)" + }, + "riskCap": { + "validation": { + "requireAtLeastOne": "至少需要一条封顶配置", + "defaultGreaterThanZero": "默认封顶金额必须大于 0", + "numberMustBe4Digits": "号码必须为 4 位数字:{{number}}", + "enterValidCapAmount": "请输入有效的封顶金额" + }, + "publishFailed": "发布失败", + "createDraftSuccess": "已创建草稿 v{{version}}", + "createDraftFailed": "创建草稿失败", + "savedLocalDraft": "已写入本地草稿,记得保存草稿后再发布。", + "deleteFailed": "删除失败", + "effectiveAt": "生效时间:{{value}}", + "note": "备注:{{value}}", + "readOnlyHint": "当前为只读,请先创建草稿。", + "readOnly": "只读", + "defaultCap": { + "title": "默认封顶", + "description": "没有单独特殊封顶的号码,统一使用这条默认封顶模板。", + "fieldLabel": "封顶金额(最小单位)" + }, + "specialCaps": { + "title": "特殊封顶" + }, + "loadingDetails": "正在加载详情…", + "noDetailRows": "暂无明细行。", + "table": { + "number": "号码", + "capAmount": "封顶金额", + "used": "已占用", + "remaining": "剩余额度", + "soldOut": "售罄", + "ratio": "占比", + "actions": "操作" + }, + "occupancy": { + "title": "全号码占用视图", + "description": "这里还是占位视图,筛选和导出后续还需要接入真实注单汇总;下方数据目前仍来自当前草稿列表。", + "searchLabel": "搜索号码", + "searchPlaceholder": "例如 8888", + "filterPending": "售罄 / 高风险预设筛选尚未接入", + "exportPending": "CSV 导出尚未接入" + }, + "actions": { + "update": "更新", + "addSpecialCap": "+ 新增特殊封顶", + "filterPresets": "筛选预设…", + "exportCsv": "导出 CSV", + "close": "关闭" + }, + "syncDialog": { + "title": "同步默认封顶", + "description": "默认封顶模板将被设为 {{value}}。这次只会修改草稿,确认后仍需保存并发布。", + "confirm": "确认" } } } diff --git a/src/i18n/locales/zh/dashboard.json b/src/i18n/locales/zh/dashboard.json index 82bcc8a..5be5169 100644 --- a/src/i18n/locales/zh/dashboard.json +++ b/src/i18n/locales/zh/dashboard.json @@ -5,7 +5,7 @@ "todayBetTotal": "当期投注总额", "currentDrawFinanceSummary": "当前大厅期财务汇总", "currentPayout": "当期派彩", - "payoutSummary": "中奖派彩 + Jackpot", + "payoutSummary": "中奖派彩 + 奖池", "currentProfit": "当期平台盈亏", "profitFormula": "投注 − 派彩(近似)", "currentDraw": "当前期号", diff --git a/src/i18n/locales/zh/jackpot.json b/src/i18n/locales/zh/jackpot.json index da89e3d..bc03c88 100644 --- a/src/i18n/locales/zh/jackpot.json +++ b/src/i18n/locales/zh/jackpot.json @@ -1,6 +1,6 @@ { "title": "奖池", - "configTitle": "Jackpot 奖池配置", + "configTitle": "奖池配置", "loadFailed": "加载失败", "saveSuccess": "已保存", "saveFailed": "保存失败", @@ -29,14 +29,24 @@ "drawNo": "期号", "optional": "可选", "apply": "应用", - "payoutRecords": "Jackpot 派彩记录", - "contributionRecords": "Jackpot 蓄水记录", - "subnavLabel": "Jackpot 子导航", + "payoutRecords": "奖池派彩记录", + "contributionRecords": "奖池蓄水记录", + "recordsPage": { + "title": "奖池记录", + "description": "派彩记录与奖池蓄水流水" + }, + "subnavLabel": "奖池子导航", "subnavPools": "奖池配置", "subnavRecords": "记录", "payoutLoadFailed": "派彩记录加载失败", "contributionLoadFailed": "蓄水记录加载失败", "trigger": "触发", + "triggerTypes": { + "threshold": "达到爆池阈值", + "forced_gap": "连续未爆强制触发", + "play_combo": "指定玩法组合触发", + "manual": "手动触发" + }, "payoutAmount": "派彩额", "winnerCount": "中奖人数", "time": "时间", diff --git a/src/i18n/locales/zh/reports.json b/src/i18n/locales/zh/reports.json index 63b0fc5..5622605 100644 --- a/src/i18n/locales/zh/reports.json +++ b/src/i18n/locales/zh/reports.json @@ -3,7 +3,7 @@ "createExport": "新建导出", "reportType": "报表类型", "exportFormat": "导出格式", - "filterJson": "filter_json(可选)", + "filterJson": "筛选条件 JSON(可选)", "parseFilterFailed": "筛选 JSON 无法解析", "createSuccess": "已创建导出任务", "createFailed": "创建失败", @@ -18,6 +18,17 @@ "createdAt": "创建时间", "id": "ID", "empty": "无数据", + "formatOptions": { + "csv": "CSV", + "xlsx": "Excel" + }, + "statusOptions": { + "pending": "待处理", + "queued": "排队中", + "running": "执行中", + "completed": "已完成", + "failed": "失败" + }, "reportTypes": { "draw_profit_summary": "期号盈亏", "daily_profit_summary": "每日盈亏汇总", diff --git a/src/i18n/locales/zh/settlement.json b/src/i18n/locales/zh/settlement.json index 2753e23..8d654f4 100644 --- a/src/i18n/locales/zh/settlement.json +++ b/src/i18n/locales/zh/settlement.json @@ -18,7 +18,7 @@ "winCount": "中奖笔数", "payoutTotal": "派彩合计", "platformProfit": "盈亏", - "jackpot": "Jackpot", + "jackpot": "奖池", "finishedAt": "完成时间", "details": "明细", "approve": "审核通过", @@ -36,7 +36,7 @@ "ticketTotal": "注单数", "winTotal": "中奖笔数", "payoutAmount": "派彩合计", - "jackpotPayout": "Jackpot 划出", + "jackpotPayout": "奖池划出", "profitFormula": "盈亏 = 总实扣 - 总派彩", "startedAt": "开始", "endedAt": "结束", @@ -58,6 +58,7 @@ "matchedTier": "匹配档", "regularPayout": "常规派彩", "loadingDetails": "加载明细…", + "invalidBatchId": "无效的结算批次编号", "statusOptions": { "all": "不限", "running": "进行中", @@ -67,5 +68,10 @@ "paid": "已派奖", "completed": "已完成", "failed": "失败" + }, + "reviewStatusOptions": { + "pending": "待审核", + "approved": "已通过", + "rejected": "已驳回" } } diff --git a/src/i18n/locales/zh/tickets.json b/src/i18n/locales/zh/tickets.json index 46799a9..708540a 100644 --- a/src/i18n/locales/zh/tickets.json +++ b/src/i18n/locales/zh/tickets.json @@ -1,19 +1,43 @@ { - "title": "注单", - "playerTicketQuery": "玩家注单查询", - "playerId": "玩家 ID", - "invalidPlayerId": "请输入有效玩家 ID", - "drawNoOptional": "期号 draw_no(可选)", + "title": "注单列表", + "playerTicketQuery": "注单查询", + "playerId": "玩家 ID / 账号", + "invalidPlayerId": "请输入有效玩家 ID 或账号", + "playerIdPlaceholder": "留空显示全部,可输入玩家 ID 或账号", + "drawNoOptional": "期号(可选)", "drawNoPlaceholder": "如 20260520-001", + "numberKeyword": "号码 / 注单号 / 订单号", + "numberKeywordPlaceholder": "支持按号码、注单号、订单号搜索", + "placedDateRange": "下单日期范围", "query": "查询", + "resetFilters": "重置筛选", + "refreshCurrentPage": "刷新当前页", "loadFailed": "加载失败", "ticketNo": "注单号", + "player": "玩家", "orderNo": "订单号", "drawNo": "期号", "playCode": "玩法", "number": "号码", + "betAmount": "下注", "actualDeduct": "实扣", "status": "状态", "failReason": "失败原因", - "winAmount": "中奖" + "winAmount": "中奖", + "placedAt": "下单时间", + "updatedAt": "更新时间", + "statusFilterLabel": "状态筛选", + "statusHint": "可多选,留空表示全部状态", + "statusSelectedCount": "已选 {{count}} 项", + "statusOptions": { + "all": "全部", + "pending_confirm": "待确认", + "partial_pending_confirm": "部分待确认", + "success": "已投注成功", + "failed": "投注失败", + "pending_payout": "待派奖", + "settled_win": "已中奖结算", + "settled_lose": "已未中奖结算" + }, + "allTickets": "全部注单" } diff --git a/src/lib/admin-locale.ts b/src/lib/admin-locale.ts index bf598ef..221374a 100644 --- a/src/lib/admin-locale.ts +++ b/src/lib/admin-locale.ts @@ -100,15 +100,18 @@ export function applyAdminUiLocale(loc: AdminApiLocale): void { } /** 启动时从 localStorage 恢复(应在客户端尽早调用一次) */ -export function hydrateAdminUiLocale(): void { +export function hydrateAdminUiLocale(): AdminApiLocale | null { if (typeof window === "undefined") { - return; + return null; } const stored = readStoredUiLocale(); if (stored) { setAdminRequestLocale(stored); document.documentElement.lang = adminHtmlLang(stored); + return stored; } + + return null; } /** 供 `admin-http`:`X-Locale` + `Accept-Language` */ diff --git a/src/modules/admin-roles/admin-roles-console.tsx b/src/modules/admin-roles/admin-roles-console.tsx index 70852c4..6813559 100644 --- a/src/modules/admin-roles/admin-roles-console.tsx +++ b/src/modules/admin-roles/admin-roles-console.tsx @@ -38,6 +38,16 @@ import { cn } from "@/lib/utils"; import type { AdminPermissionCatalogData, AdminRoleRow } from "@/types/api/index"; import { LotteryApiBizError } from "@/types/api/errors"; +function permissionGroupLabel(key: string, fallback: string, t: (key: string) => string): string { + const translated = t(`permissionGroups.${key}`); + return translated === `permissionGroups.${key}` ? fallback : translated; +} + +function permissionLabel(slug: string, fallback: string, t: (key: string) => string): string { + const translated = t(`permissionNames.${slug}`); + return translated === `permissionNames.${slug}` ? fallback : translated; +} + export function AdminRolesConsole(): React.ReactElement { const { t } = useTranslation(["adminUsers", "common"]); const [catalog, setCatalog] = useState(null); @@ -289,7 +299,7 @@ export function AdminRolesConsole(): React.ReactElement { - ID + {t("table.id", { ns: "common" })} {t("roleTable.name")} {t("roleTable.slug")} {t("roleTable.type")} @@ -376,7 +386,7 @@ export function AdminRolesConsole(): React.ReactElement { {t("rolePermissionDialog.title")} - {selectedRole ? `${selectedRole.name} · ${selectedRole.slug}` : null} + {selectedRole ? selectedRole.name : null}
@@ -401,7 +411,9 @@ export function AdminRolesConsole(): React.ReactElement { isOpen && "rotate-180", )} /> - {group.label} + + {permissionGroupLabel(group.key, group.label, t)} + {selectedCount}/{group.permissions.length} @@ -424,7 +436,7 @@ export function AdminRolesConsole(): React.ReactElement { } /> - {permission.name} + {permissionLabel(permission.slug, permission.name, t)} ))} diff --git a/src/modules/admin-roles/meta.ts b/src/modules/admin-roles/meta.ts index 89a8dbd..ecbcec3 100644 --- a/src/modules/admin-roles/meta.ts +++ b/src/modules/admin-roles/meta.ts @@ -1,5 +1,5 @@ export const adminRolesModuleMeta = { segment: "admin_roles", - title: "Roles", + title: "角色管理", description: "", } as const; diff --git a/src/modules/admin-users/admin-users-console.tsx b/src/modules/admin-users/admin-users-console.tsx index 85393fa..6546c61 100644 --- a/src/modules/admin-users/admin-users-console.tsx +++ b/src/modules/admin-users/admin-users-console.tsx @@ -78,6 +78,10 @@ export function AdminUsersConsole(): React.ReactElement { () => items.find((u) => u.id === selectedId) ?? null, [items, selectedId], ); + const roleNameBySlug = useMemo( + () => new Map((catalog?.roles ?? []).map((role) => [role.slug, role.name])), + [catalog], + ); const selectClassName = cn( "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base outline-none transition-colors", @@ -355,7 +359,7 @@ export function AdminUsersConsole(): React.ReactElement {
- ID + {t("table.id", { ns: "common" })} {t("table.account")} {t("table.nickname")} {t("table.status")} @@ -403,7 +407,7 @@ export function AdminUsersConsole(): React.ReactElement { ) : ( row.roles.map((slug) => ( - {slug} + {roleNameBySlug.get(slug) ?? slug} )) )} @@ -497,7 +501,7 @@ export function AdminUsersConsole(): React.ReactElement { /> {role.name} - {role.slug} + {role.description ?? ""} ); @@ -605,7 +609,7 @@ export function AdminUsersConsole(): React.ReactElement { /> {role.name} - {role.slug} + {role.description ?? ""} ); diff --git a/src/modules/admin-users/meta.ts b/src/modules/admin-users/meta.ts index c312c63..40096ca 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: "Admins", + title: "管理员列表", description: "", } as const; diff --git a/src/modules/audit/audit-logs-console.tsx b/src/modules/audit/audit-logs-console.tsx index 2454ed6..59f3a26 100644 --- a/src/modules/audit/audit-logs-console.tsx +++ b/src/modules/audit/audit-logs-console.tsx @@ -66,66 +66,82 @@ export function AuditLogsConsole(): React.ReactElement { const meta = data?.meta; return ( - - -
- {t("title")} -
-
-
- + + + {t("title")} +
+
+ setModuleCode(e.target.value)} placeholder={t("exactMatch")} - className="w-full sm:w-40" + className="w-full" />
-
- +
+ setActionCode(e.target.value)} placeholder={t("exactMatch")} - className="w-full sm:w-40" + className="w-full" />
-
- +
+ setOperatorType(e.target.value)} placeholder={t("operatorTypePlaceholder")} - className="w-full sm:w-40" + className="w-full" />
-
- - - -
+
+
+ + +
- + {err ?

{err}

: null} {loading && !data ? (

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

@@ -137,7 +153,7 @@ export function AuditLogsConsole(): React.ReactElement {
- ID + {t("table.id", { ns: "common" })} {t("operator")} {t("module")} {t("action")} diff --git a/src/modules/audit/meta.ts b/src/modules/audit/meta.ts index 6177863..97fec79 100644 --- a/src/modules/audit/meta.ts +++ b/src/modules/audit/meta.ts @@ -1,5 +1,5 @@ export const auditLogsModuleMeta = { segment: "audit-logs", - title: "Audit Logs", + title: "审计日志", description: "", } as const; diff --git a/src/modules/auth/meta.ts b/src/modules/auth/meta.ts index 27551a3..23e0315 100644 --- a/src/modules/auth/meta.ts +++ b/src/modules/auth/meta.ts @@ -1,5 +1,5 @@ export const authModuleMeta = { segment: "login", - title: "Login", + title: "登录", description: "", } as const; diff --git a/src/modules/config/config-nav-model.ts b/src/modules/config/config-nav-model.ts index 920f5df..22329e6 100644 --- a/src/modules/config/config-nav-model.ts +++ b/src/modules/config/config-nav-model.ts @@ -34,16 +34,12 @@ export const CONFIG_NAV_GROUPS: readonly ConfigNavGroup[] = [ ], }, { - id: "risk_wallet", + id: "risk", items: [ { href: "/admin/config/risk-cap", key: "risk-cap", }, - { - href: "/admin/config/wallet", - key: "wallet", - }, ], }, ] as const; diff --git a/src/modules/config/config-subnav.tsx b/src/modules/config/config-subnav.tsx index cdf6a9a..1fe3aad 100644 --- a/src/modules/config/config-subnav.tsx +++ b/src/modules/config/config-subnav.tsx @@ -11,7 +11,6 @@ const LINKS: { href: string; key: string; match?: "exact" | "prefix" }[] = [ { 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 { diff --git a/src/modules/config/doc/odds-config-doc-screen.tsx b/src/modules/config/doc/odds-config-doc-screen.tsx index 3f88ca2..3a17b44 100644 --- a/src/modules/config/doc/odds-config-doc-screen.tsx +++ b/src/modules/config/doc/odds-config-doc-screen.tsx @@ -276,7 +276,7 @@ export function OddsConfigDocScreen() { void refreshList(); setSelectedId(String(d.id)); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "Publish failed"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("odds.publishFailed", { ns: "config" })); } finally { setSaving(false); } @@ -308,13 +308,13 @@ export function OddsConfigDocScreen() { reason: `draft ${new Date().toISOString()}`, clone_from_version_id: active?.id ?? null, }); - toast.success(`Created draft v${d.version_no}`); + toast.success(t("odds.createDraftSuccess", { ns: "config", version: d.version_no })); await refreshList(); setSelectedId(String(d.id)); setDetail(d); setDraftRows(d.items.map((it) => ({ ...it }))); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "Create draft failed"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("odds.createDraftFailed", { ns: "config" })); } finally { setSaving(false); } @@ -330,7 +330,13 @@ export function OddsConfigDocScreen() { reason: `rollback from v${rollbackTarget.version_no}`, clone_from_version_id: rollbackTarget.id, }); - toast.success(`Cloned v${rollbackTarget.version_no} into new draft v${d.version_no}`); + toast.success( + t("odds.rollbackSuccess", { + ns: "config", + fromVersion: rollbackTarget.version_no, + version: d.version_no, + }), + ); await refreshList(); setSelectedId(String(d.id)); setDetail(d); @@ -338,7 +344,7 @@ export function OddsConfigDocScreen() { setRollbackOpen(false); setRollbackTarget(null); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "Rollback failed"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("odds.rollbackFailed", { ns: "config" })); } finally { setSaving(false); } @@ -352,7 +358,7 @@ export function OddsConfigDocScreen() { toast.success(t("versionSwitcher.delete", { ns: "config" })); await refreshList(); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "Delete failed"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("odds.deleteFailed", { ns: "config" })); throw e; } } @@ -382,7 +388,7 @@ export function OddsConfigDocScreen() { }, [activeCompareRows, detail, draftRows, resolvedPlayCode]); const catTabs: { id: CatTab; label: string }[] = [ - { id: "all", label: "All" }, + { id: "all", label: t("odds.tabs.all", { ns: "config" }) }, { id: "d4", label: "4D" }, { id: "d3", label: "3D" }, { id: "d2", label: "2D" }, @@ -395,7 +401,7 @@ export function OddsConfigDocScreen() {
- Category + {t("odds.category", { ns: "config" })} {catTabs.map((t) => (
); })}
- + {isDraft ? ( )} -

Writes rebate_rate to all prize scopes under this play type.

+

{t("odds.rebateRateHint", { ns: "config" })}

) : null} @@ -554,9 +564,9 @@ export function OddsConfigDocScreen() { - Confirm rollback + {t("odds.rollbackDialog.title", { ns: "config" })} - A new draft will be cloned from version v{rollbackTarget?.version_no}. The active version will not be overwritten directly. + {t("odds.rollbackDialog.description", { ns: "config", version: rollbackTarget?.version_no ?? "—" })} @@ -564,7 +574,7 @@ export function OddsConfigDocScreen() { {t("actions.cancel", { ns: "adminUsers" })} @@ -573,16 +583,16 @@ export function OddsConfigDocScreen() { - Publish odds version? + {t("odds.publishDialog.title", { ns: "config" })} - New odds affect new tickets immediately. Existing successful tickets still settle by their saved odds snapshot. + {t("odds.publishDialog.description", { ns: "config" })}
- Prize Scope - Current Active - After Publish + {t("odds.publishDialog.columns.prizeScope", { ns: "config" })} + {t("odds.publishDialog.columns.currentActive", { ns: "config" })} + {t("odds.publishDialog.columns.afterPublish", { ns: "config" })}
{publishDiffRows.map((row) => (
@@ -606,7 +616,7 @@ export function OddsConfigDocScreen() { void handlePublish(); }} > - Confirm publish + {t("odds.publishDialog.confirm", { ns: "config" })} diff --git a/src/modules/config/doc/play-config-doc-screen.tsx b/src/modules/config/doc/play-config-doc-screen.tsx index 1967eaf..7064691 100644 --- a/src/modules/config/doc/play-config-doc-screen.tsx +++ b/src/modules/config/doc/play-config-doc-screen.tsx @@ -284,7 +284,7 @@ export function PlayConfigDocScreen() { const payload = buildPlayConfigSavePayload(draftRows); for (const r of payload) { if (r.min_bet_amount > r.max_bet_amount) { - toast.error(`${r.play_code}: min_bet_amount cannot exceed max_bet_amount`); + toast.error(t("play.validation.minMaxInvalid", { ns: "config", playCode: r.play_code })); return; } } @@ -315,7 +315,7 @@ export function PlayConfigDocScreen() { void refreshList(); setSelectedId(String(d.id)); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "Publish failed"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("play.publishFailed", { ns: "config" })); } finally { setSaving(false); } @@ -329,14 +329,14 @@ export function PlayConfigDocScreen() { reason: `draft ${new Date().toISOString()}`, clone_from_version_id: active?.id ?? null, }); - toast.success(`Created draft v${d.version_no}`); + toast.success(t("play.createDraftSuccess", { ns: "config", version: d.version_no })); setCreatingDraftId(String(d.id)); setSelectedId(String(d.id)); setDetail(d); setDraftRows(d.items.map((it) => ({ ...it }))); void refreshList(); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "Create draft failed"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("play.createDraftFailed", { ns: "config" })); } finally { setSaving(false); } @@ -356,7 +356,7 @@ export function PlayConfigDocScreen() { updateConfigRow(rulePlayCode, { rule_text_zh: ruleDraftZh.trim() || null }); setRuleDialogOpen(false); setRulePlayCode(null); - toast.message("Rule text saved into the local draft. Save the draft to persist it."); + toast.message(t("play.ruleSavedLocal", { ns: "config" })); } const activeHead = list.find((x) => x.status === "active"); @@ -367,7 +367,7 @@ export function PlayConfigDocScreen() { toast.success(t("versionSwitcher.delete", { ns: "config" })); await refreshList(); } catch (e) { - toast.error(e instanceof LotteryApiBizError ? e.message : "Delete failed"); + toast.error(e instanceof LotteryApiBizError ? e.message : t("play.deleteFailed", { ns: "config" })); throw e; } } @@ -407,14 +407,14 @@ export function PlayConfigDocScreen() {

{activeHead ? ( <> - Active version v{activeHead.version_no} + {t("play.activeVersion", { ns: "config", version: activeHead.version_no })} {activeHead.effective_at ? ` · ${activeHead.effective_at}` : ""} ) : null} {!isDraft ? ( {activeHead ? " — " : ""} - Limits and rules are read-only. Create a draft first. + {t("play.readOnlyHint", { ns: "config" })} ) : null}

@@ -424,14 +424,14 @@ export function PlayConfigDocScreen() {
-

Batch switches

+

{t("play.batchSwitchesTitle", { ns: "config" })}

- Only updates the current draft. The player betting table refreshes after save and publish. + {t("play.batchSwitchesDesc", { ns: "config" })}

{!isDraft ? ( - Current version is read-only. Create a draft first. + {t("play.readOnlyDraftHint", { ns: "config" })} ) : null}
@@ -444,7 +444,13 @@ export function PlayConfigDocScreen() {

{group.label}

- {group.total > 0 ? `${group.enabledCount}/${group.total} enabled` : "No play types"} + {group.total > 0 + ? t("play.batchEnabledCount", { + ns: "config", + enabledCount: group.enabledCount, + total: group.total, + }) + : t("play.noPlayTypes", { ns: "config" })}

))} @@ -471,14 +479,14 @@ export function PlayConfigDocScreen() {
- Play Code - Category - Status - Display Name - Order - Min Bet - Max Bet - Actions + {t("play.table.playCode", { ns: "config" })} + {t("play.table.category", { ns: "config" })} + {t("play.table.status", { ns: "config" })} + {t("play.table.displayName", { ns: "config" })} + {t("play.table.order", { ns: "config" })} + {t("play.table.minBet", { ns: "config" })} + {t("play.table.maxBet", { ns: "config" })} + {t("play.table.actions", { ns: "config" })} @@ -494,11 +502,13 @@ export function PlayConfigDocScreen() { onCheckedChange={(v) => { updateConfigRow(row.play_code, { is_enabled: v === true }); }} - aria-label={`Enable ${row.play_code}`} + aria-label={t("play.aria.enablePlay", { ns: "config", playCode: row.play_code })} /> ) : ( - {row.is_enabled ? "Enabled" : "Disabled"} + {row.is_enabled + ? t("play.states.enabled", { ns: "config" }) + : t("play.states.disabled", { ns: "config" })} )} @@ -588,10 +598,10 @@ export function PlayConfigDocScreen() { disabled={saving} onClick={() => openRuleEditor(row.play_code)} > - Rule Text + {t("play.actions.ruleText", { ns: "config" })} ) : ( - Read only + {t("play.states.readOnly", { ns: "config" })} )} @@ -605,13 +615,13 @@ export function PlayConfigDocScreen() { - Rule Text (Chinese) + {t("play.ruleDialog.title", { ns: "config" })} - Play {rulePlayCode ?? "—"}; changes are only stored in the draft until you save and publish it. + {t("play.ruleDialog.description", { ns: "config", playCode: rulePlayCode ?? "—" })}
- +