refactor: 完成全站国际化改造,统一多语言支持

此提交完成了全项目的国际化适配:
1. 新增多语言翻译文件与基础配置
2. 替换所有硬编码文本为i18n调用
3. 优化语言切换与文档语言同步逻辑
4. 重构部分业务逻辑以支持动态翻译
5. 移除过时代码与硬编码配置
This commit is contained in:
2026-05-15 10:41:14 +08:00
parent ac612cb32c
commit f2c7f5e4f1
53 changed files with 2179 additions and 767 deletions

View File

@@ -1,6 +1,7 @@
"use client";
import { AlertTriangleIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
@@ -28,14 +29,16 @@ type HallBetPreviewDialogProps = {
};
function WarningsBlock({ warnings }: { warnings: TicketPreviewWarning[] }) {
const { t } = useTranslation("player");
if (warnings.length === 0) return null;
return (
<Alert className="border-amber-500/40 bg-amber-500/5 text-amber-950 dark:text-amber-100">
<AlertTriangleIcon />
<AlertTitle></AlertTitle>
<AlertTitle>{t("hall.preview.warningsTitle")}</AlertTitle>
<AlertDescription className="space-y-1">
<p className="text-xs leading-relaxed">
§6.4
{t("hall.preview.warningsDescription")}
</p>
<ul className="list-inside list-disc text-xs">
{warnings.map((w, i) => (
@@ -61,6 +64,7 @@ export function HallBetPreviewDialog({
allowSubmit = true,
onConfirmPlace,
}: HallBetPreviewDialogProps) {
const { t } = useTranslation("player");
const summary = data?.summary;
const lines = data?.lines ?? [];
@@ -69,17 +73,17 @@ export function HallBetPreviewDialog({
<DialogContent className="max-h-[min(90vh,560px)] gap-0 overflow-hidden p-0 sm:max-w-md">
<div className="p-4 pb-2">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogTitle>{t("hall.preview.title")}</DialogTitle>
<DialogDescription>
§6.3
{t("hall.preview.description")}
</DialogDescription>
</DialogHeader>
{!allowSubmit ? (
<Alert className="mt-3 border-[#ff4d4f]/35 bg-[#ff4d4f]/8 text-[#ff4d4f] dark:bg-[#ff4d4f]/12">
<AlertTriangleIcon />
<AlertTitle></AlertTitle>
<AlertTitle>{t("hall.preview.sealedTitle")}</AlertTitle>
<AlertDescription className="text-xs leading-relaxed">
§4.2
{t("hall.preview.sealedDescription")}
</AlertDescription>
</Alert>
) : null}
@@ -88,37 +92,38 @@ export function HallBetPreviewDialog({
<ScrollArea className="max-h-[min(52vh,360px)] border-y px-4">
<div className="space-y-4 py-3 pr-3">
{!data ? (
<p className="text-sm text-muted-foreground"></p>
<p className="text-sm text-muted-foreground">{t("hall.preview.empty")}</p>
) : (
<>
<div className="rounded-lg border bg-muted/30 p-3 text-xs">
<p>
{" "}
<span className="font-mono font-semibold">{data.draw.draw_id}</span> · {" "}
{t("hall.preview.draw")}{" "}
<span className="font-mono font-semibold">{data.draw.draw_id}</span> ·{" "}
{t("hall.preview.status")}{" "}
<span className="font-medium">{data.draw.status}</span>
</p>
{summary ? (
<ul className="mt-2 space-y-1 tabular-nums">
<li>
{" "}
{t("hall.preview.totalBet")}{" "}
<span className="font-medium">
{formatMinorAsCurrency(summary.total_bet_amount, currencyCode)}
</span>
</li>
<li>
{" "}
{t("hall.preview.rebateDeduct")}{" "}
<span className="font-medium">
{formatMinorAsCurrency(summary.total_rebate_amount, currencyCode)}
</span>
</li>
<li>
{" "}
{t("hall.preview.actualDeduct")}{" "}
<span className="font-semibold text-primary">
{formatMinorAsCurrency(summary.total_actual_deduct, currencyCode)}
</span>
</li>
<li>
{" "}
{t("hall.preview.estimatedPayout")}{" "}
<span className="font-medium">
{formatMinorAsCurrency(summary.total_estimated_payout, currencyCode)}
</span>
@@ -130,7 +135,9 @@ export function HallBetPreviewDialog({
<WarningsBlock warnings={data.warnings} />
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground"></p>
<p className="text-xs font-medium text-muted-foreground">
{t("hall.preview.lines")}
</p>
<ul className="space-y-2 text-sm">
{lines.map((ln) => (
<li
@@ -146,15 +153,23 @@ export function HallBetPreviewDialog({
<p className="mt-1 font-mono text-base">{ln.number}</p>
<Separator className="my-2" />
<div className="grid grid-cols-2 gap-x-2 gap-y-1 text-xs tabular-nums">
<span className="text-muted-foreground"></span>
<span className="text-muted-foreground">
{t("hall.preview.normalizedNumber")}
</span>
<span className="text-right font-mono">{ln.normalized_number}</span>
<span className="text-muted-foreground"></span>
<span className="text-muted-foreground">
{t("hall.preview.combinationCount")}
</span>
<span className="text-right">{ln.combination_count}</span>
<span className="text-muted-foreground"></span>
<span className="text-muted-foreground">
{t("hall.preview.actual")}
</span>
<span className="text-right">
{formatMinorAsCurrency(ln.actual_deduct_amount, currencyCode)}
</span>
<span className="text-muted-foreground"></span>
<span className="text-muted-foreground">
{t("hall.preview.estimatedMax")}
</span>
<span className="text-right">
{formatMinorAsCurrency(ln.estimated_max_payout, currencyCode)}
</span>
@@ -170,10 +185,14 @@ export function HallBetPreviewDialog({
<div className="flex flex-col-reverse gap-2 border-t bg-muted/30 p-4 sm:flex-row sm:justify-between">
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={placing}>
{t("hall.preview.backEdit")}
</Button>
<Button type="button" onClick={onConfirmPlace} disabled={!data || placing || !allowSubmit}>
{placing ? "提交中…" : allowSubmit ? "确认提交" : "已封盘"}
{placing
? t("hall.preview.submitting")
: allowSubmit
? t("hall.preview.confirmSubmit")
: t("hall.preview.sealedTitle")}
</Button>
</div>
</DialogContent>