feat(i18n): add batch group switch text to English, Nepali, and Chinese locales

- Updated the English, Nepali, and Chinese locale files to include a new translation for "Toggle batch switch for {{group}}".
- Enhanced internationalization support for the admin interface by adding relevant strings for improved user experience.
This commit is contained in:
2026-05-26 10:33:03 +08:00
parent fbe385666a
commit 05fa0cbeec
15 changed files with 328 additions and 357 deletions

View File

@@ -19,7 +19,7 @@ import { ConfigChipGroup } from "@/modules/config/config-chip-group";
import { ConfigContextBanner, ConfigContextEmphasis } from "@/modules/config/config-context-banner";
import { ConfigDocPage, ConfigDocToolbar } from "@/modules/config/config-doc-page";
import { ConfigSection } from "@/modules/config/config-section";
import { Checkbox } from "@/components/ui/checkbox";
import { Switch } from "@/components/ui/switch";
import {
Dialog,
DialogContent,
@@ -29,7 +29,6 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Table,
TableBody,
@@ -46,6 +45,7 @@ import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher"
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
import { useConfirmAction } from "@/hooks/use-confirm-action";
import { adminHasAnyPermission } from "@/lib/admin-permissions";
import { formatAdminMinorDecimal, parseAdminMajorToMinor } from "@/lib/money";
import { PRD_PLAY_SWITCH_MANAGE } from "@/lib/admin-prd";
import { useAdminProfile } from "@/stores/admin-session";
import { LotteryApiBizError } from "@/types/api/errors";
@@ -151,9 +151,6 @@ export function PlayConfigDocScreen() {
const [error, setError] = useState<string | null>(null);
const detailRequestSeq = useRef(0);
const [nameDialogOpen, setNameDialogOpen] = useState(false);
const [namePlayCode, setNamePlayCode] = useState<string | null>(null);
const [nameDraft, setNameDraft] = useState("");
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
const [rulePlayCode, setRulePlayCode] = useState<string | null>(null);
const [ruleDraftZh, setRuleDraftZh] = useState("");
@@ -258,6 +255,7 @@ export function PlayConfigDocScreen() {
const isSelectedDetail = detail !== null && String(detail.id) === selectedId;
const selectedStatus = isSelectedDetail ? detail.status : selectedVersionSummary?.status;
const isDraft = selectedStatus === "draft";
const amountCurrencyCode = "NPR";
const orderedRows = useMemo(
() =>
@@ -373,30 +371,6 @@ export function PlayConfigDocScreen() {
}
}
function openNameEditor(play_code: string) {
const item = draftRows.find((row) => row.play_code === play_code);
setNamePlayCode(play_code);
setNameDraft(item?.display_name ?? item?.play_code ?? "");
setNameDialogOpen(true);
}
function saveNameDraft() {
if (!namePlayCode) {
return;
}
const name = nameDraft.trim();
if (!name) {
toast.error(t("play.validation.displayNameRequired", { ns: "config" }));
return;
}
updateConfigRow(namePlayCode, {
display_name: name,
});
setNameDialogOpen(false);
setNamePlayCode(null);
toast.message(t("play.nameDialog.savedLocal", { ns: "config" }));
}
function openRuleEditor(play_code: string) {
const item = draftRows.find((row) => row.play_code === play_code);
setRulePlayCode(play_code);
@@ -501,52 +475,53 @@ export function PlayConfigDocScreen() {
description={!isDraft ? t("play.readOnlyDraftHint", { ns: "config" }) : undefined}
>
<ConfigChipGroup>
{batchSwitchStates.map((group) => (
<div
key={group.key}
className="flex items-center gap-3 rounded-xl border border-border/60 bg-card px-4 py-3"
>
<div className="min-w-[100px]">
<p className="text-sm font-medium text-foreground">{group.label}</p>
<p className="text-sm text-muted-foreground">
{group.total > 0
? t("play.batchEnabledCount", {
ns: "config",
enabledCount: group.enabledCount,
total: group.total,
})
: t("play.noPlayTypes", { ns: "config" })}
</p>
</div>
<Button
type="button"
size="sm"
variant={group.allEnabled ? "secondary" : "outline"}
disabled={!isDraft || saving || group.total === 0}
onClick={() => {
const enable = !group.allEnabled;
const action = enable
? t("play.batchSwitchEnable", { ns: "config" })
: t("play.batchSwitchDisable", { ns: "config" });
requestConfirm({
title: t("play.batchSwitchConfirmTitle", { ns: "config", action }),
description: t("play.batchSwitchConfirmDescription", {
ns: "config",
action,
group: group.label,
count: group.total,
}),
confirmVariant: enable ? "default" : "destructive",
onConfirm: () => applyBatchSwitch(group, enable),
});
}}
{batchSwitchStates.map((group) => {
const groupOn = group.enabledCount > 0;
return (
<div
key={group.key}
className="flex items-center justify-between gap-4 rounded-xl border border-border/60 bg-card px-4 py-3"
>
{group.allEnabled
? t("play.actions.disable", { ns: "config" })
: t("play.actions.enable", { ns: "config" })}
</Button>
</div>
))}
<div className="min-w-0">
<p className="text-sm font-medium text-foreground">{group.label}</p>
<p className="text-sm text-muted-foreground">
{group.total > 0
? t("play.batchEnabledCount", {
ns: "config",
enabledCount: group.enabledCount,
total: group.total,
})
: t("play.noPlayTypes", { ns: "config" })}
</p>
</div>
<Switch
checked={groupOn}
disabled={!isDraft || saving || group.total === 0}
aria-label={t("play.aria.batchGroupSwitch", {
ns: "config",
group: group.label,
})}
onCheckedChange={(checked) => {
const enable = checked;
const action = enable
? t("play.batchSwitchEnable", { ns: "config" })
: t("play.batchSwitchDisable", { ns: "config" });
requestConfirm({
title: t("play.batchSwitchConfirmTitle", { ns: "config", action }),
description: t("play.batchSwitchConfirmDescription", {
ns: "config",
action,
group: group.label,
count: group.total,
}),
confirmVariant: enable ? "default" : "destructive",
onConfirm: () => applyBatchSwitch(group, enable),
});
}}
/>
</div>
);
})}
</ConfigChipGroup>
</ConfigSection>
) : null}
@@ -562,7 +537,7 @@ export function PlayConfigDocScreen() {
<TableHead className="text-center">{t("play.table.playCode", { ns: "config" })}</TableHead>
<TableHead className="w-[100px] text-center">{t("play.table.category", { ns: "config" })}</TableHead>
<TableHead className="w-[88px] text-center">{t("play.table.status", { ns: "config" })}</TableHead>
<TableHead className="min-w-[120px] text-center">{t("play.table.displayName", { ns: "config" })}</TableHead>
<TableHead className="w-32 text-center">{t("play.table.displayName", { ns: "config" })}</TableHead>
<TableHead className="w-[120px] text-center">{t("play.table.order", { ns: "config" })}</TableHead>
<TableHead className="w-[110px] text-center">{t("play.table.minBet", { ns: "config" })}</TableHead>
<TableHead className="w-[110px] text-center">{t("play.table.maxBet", { ns: "config" })}</TableHead>
@@ -575,12 +550,16 @@ export function PlayConfigDocScreen() {
<TableCell className="text-center font-mono text-sm">{row.play_code}</TableCell>
<TableCell className="text-center text-muted-foreground text-sm">{row.category ?? "—"}</TableCell>
<TableCell className="text-center">
{isDraft ? (
<Checkbox
<div className="flex justify-center">
<Switch
checked={row.is_enabled}
disabled={saving}
onCheckedChange={(v) => {
const enabled = v === true;
disabled={!isDraft || saving}
aria-label={t("play.aria.enablePlay", { ns: "config", playCode: row.play_code })}
onCheckedChange={(checked) => {
if (!isDraft) {
return;
}
const enabled = checked;
const action = enabled
? t("play.toggleEnable", { ns: "config" })
: t("play.toggleDisable", { ns: "config" });
@@ -598,35 +577,27 @@ export function PlayConfigDocScreen() {
},
});
}}
aria-label={t("play.aria.enablePlay", { ns: "config", playCode: row.play_code })}
/>
) : (
<div className="flex justify-center">
<AdminStatusBadge status={row.is_enabled ? "enabled" : "disabled"}>
{row.is_enabled
? t("play.states.enabled", { ns: "config" })
: t("play.states.disabled", { ns: "config" })}
</AdminStatusBadge>
</div>
)}
</div>
</TableCell>
<TableCell>
<TableCell className="w-32 text-center">
{isDraft ? (
<div className="flex flex-col items-center gap-1.5">
<p className="max-w-[10rem] truncate text-sm font-medium">
{row.display_name ?? row.play_code}
</p>
<Button
type="button"
variant="outline"
size="sm"
className="h-7 text-xs"
disabled={saving}
onClick={() => openNameEditor(row.play_code)}
>
{t("play.actions.editDisplayName", { ns: "config" })}
</Button>
</div>
<Input
type="text"
className="mx-auto h-8 w-28 text-center text-sm"
disabled={saving}
value={row.display_name ?? ""}
placeholder={row.play_code}
onChange={(e) =>
updateConfigRow(row.play_code, { display_name: e.target.value })
}
onBlur={(e) => {
const trimmed = e.target.value.trim();
updateConfigRow(row.play_code, {
display_name: trimmed || row.play_code,
});
}}
/>
) : (
<ConfigReadonlyValue className="justify-center">
{renderDisplayNameReadonly(row)}
@@ -658,19 +629,20 @@ export function PlayConfigDocScreen() {
{isDraft ? (
<Input
type="text"
inputMode="numeric"
inputMode="decimal"
className="h-8 text-center font-mono tabular-nums"
disabled={saving}
value={row.min_bet_amount}
value={formatAdminMinorDecimal(row.min_bet_amount, amountCurrencyCode)}
onChange={(e) =>
updateConfigRow(row.play_code, {
min_bet_amount: Number.parseInt(e.target.value, 10) || 0,
min_bet_amount:
parseAdminMajorToMinor(e.target.value, amountCurrencyCode) ?? 0,
})
}
/>
) : (
<ConfigReadonlyValue mono className="justify-center">
{row.min_bet_amount}
{formatAdminMinorDecimal(row.min_bet_amount, amountCurrencyCode)}
</ConfigReadonlyValue>
)}
</TableCell>
@@ -678,19 +650,20 @@ export function PlayConfigDocScreen() {
{isDraft ? (
<Input
type="text"
inputMode="numeric"
inputMode="decimal"
className="h-8 text-center font-mono tabular-nums"
disabled={saving}
value={row.max_bet_amount}
value={formatAdminMinorDecimal(row.max_bet_amount, amountCurrencyCode)}
onChange={(e) =>
updateConfigRow(row.play_code, {
max_bet_amount: Number.parseInt(e.target.value, 10) || 0,
max_bet_amount:
parseAdminMajorToMinor(e.target.value, amountCurrencyCode) ?? 0,
})
}
/>
) : (
<ConfigReadonlyValue mono className="justify-center">
{row.max_bet_amount}
{formatAdminMinorDecimal(row.max_bet_amount, amountCurrencyCode)}
</ConfigReadonlyValue>
)}
</TableCell>
@@ -716,33 +689,6 @@ export function PlayConfigDocScreen() {
</Table>
)}
<Dialog open={nameDialogOpen} onOpenChange={setNameDialogOpen}>
<DialogContent showCloseButton className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{t("play.nameDialog.title", { ns: "config" })}</DialogTitle>
<DialogDescription>
{t("play.nameDialog.description", { ns: "config", playCode: namePlayCode ?? "—" })}
</DialogDescription>
</DialogHeader>
<div className="grid gap-1.5">
<Label htmlFor="play-display-name">{t("play.table.displayName", { ns: "config" })}</Label>
<Input
id="play-display-name"
value={nameDraft}
onChange={(e) => setNameDraft(e.target.value)}
/>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setNameDialogOpen(false)}>
{t("actions.cancel", { ns: "adminUsers" })}
</Button>
<Button type="button" onClick={saveNameDraft}>
{t("play.nameDialog.apply", { ns: "config" })}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={ruleDialogOpen} onOpenChange={setRuleDialogOpen}>
<DialogContent showCloseButton className="sm:max-w-lg">
<DialogHeader>