refactor: 完成全站国际化改造,统一多语言支持
此提交完成了全项目的国际化适配: 1. 新增多语言翻译文件与基础配置 2. 替换所有硬编码文本为i18n调用 3. 优化语言切换与文档语言同步逻辑 4. 重构部分业务逻辑以支持动态翻译 5. 移除过时代码与硬编码配置
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { CirclePlus, Cuboid, PackageOpen, Ticket, Trash2 } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { getPlayEffective } from "@/api/play";
|
||||
@@ -205,6 +206,7 @@ function pickSimplePlay(
|
||||
|
||||
export function HallBettingGrid() {
|
||||
const { display, isBettable, reload: reloadDraw } = useHallDrawLive();
|
||||
const { t } = useTranslation("player");
|
||||
const [activeCategory, setActiveCategory] = useState<HallCategory>("D2");
|
||||
const [boxMode, setBoxMode] = useState<BoxMode>("ibox");
|
||||
const [rows, setRows] = useState<DraftRow[]>(() => [
|
||||
@@ -239,10 +241,10 @@ export function HallBettingGrid() {
|
||||
setCatalogState({ kind: "ok", data });
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e instanceof LotteryApiBizError ? e.message : "加载玩法失败,请稍后重试。";
|
||||
e instanceof LotteryApiBizError ? e.message : t("hall.loadingError");
|
||||
setCatalogState({ kind: "error", message: msg });
|
||||
}
|
||||
}, [currencyParam]);
|
||||
}, [currencyParam, t]);
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => {
|
||||
@@ -373,21 +375,21 @@ export function HallBettingGrid() {
|
||||
|
||||
const handlePreview = async () => {
|
||||
if (!display) {
|
||||
toast.error("暂无当期期号,无法提交。");
|
||||
toast.error(t("hall.noDraw"));
|
||||
return;
|
||||
}
|
||||
if (!isBettable) {
|
||||
toast.error("当前已封盘或不可下注。");
|
||||
toast.error(t("hall.notBettable"));
|
||||
return;
|
||||
}
|
||||
if (catalogState.kind !== "ok") {
|
||||
toast.error("玩法配置尚未加载完成。");
|
||||
toast.error(t("hall.catalogNotReady"));
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = buildLines();
|
||||
if (lines.length === 0) {
|
||||
toast.error("请至少填写一组有效号码和下注金额。");
|
||||
toast.error(t("hall.emptyLines"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -403,8 +405,8 @@ export function HallBettingGrid() {
|
||||
setPreviewOpen(true);
|
||||
} catch (e) {
|
||||
const code = e instanceof LotteryApiBizError ? e.code : 0;
|
||||
const msg = e instanceof LotteryApiBizError ? e.message : "预览失败";
|
||||
toast.error(mapTicketBetError(code, msg));
|
||||
const msg = e instanceof LotteryApiBizError ? e.message : t("hall.previewFailed");
|
||||
toast.error(mapTicketBetError(code, msg, t));
|
||||
} finally {
|
||||
setPreviewLoading(false);
|
||||
}
|
||||
@@ -413,13 +415,13 @@ export function HallBettingGrid() {
|
||||
const handlePlace = async () => {
|
||||
if (!display || !previewData) return;
|
||||
if (!isBettable) {
|
||||
toast.error("已封盘,无法提交。");
|
||||
toast.error(t("hall.closedSubmit"));
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = buildLines();
|
||||
if (lines.length === 0) {
|
||||
toast.error("提交前数据已变化,请关闭预览后重试。");
|
||||
toast.error(t("hall.changedBeforeSubmit"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -436,7 +438,10 @@ export function HallBettingGrid() {
|
||||
expected_config_versions: previewData.config_versions,
|
||||
});
|
||||
toast.success(
|
||||
`下注成功,订单号 ${data.order_no},实扣 ${formatMinorAsCurrency(data.summary.total_actual_deduct, currencyCode)}。`,
|
||||
t("hall.placeSuccess", {
|
||||
orderNo: data.order_no,
|
||||
amount: formatMinorAsCurrency(data.summary.total_actual_deduct, currencyCode),
|
||||
}),
|
||||
);
|
||||
setPreviewOpen(false);
|
||||
setPreviewData(null);
|
||||
@@ -445,8 +450,8 @@ export function HallBettingGrid() {
|
||||
void reloadDraw();
|
||||
} catch (e) {
|
||||
const code = e instanceof LotteryApiBizError ? e.code : 0;
|
||||
const msg = e instanceof LotteryApiBizError ? e.message : "提交失败";
|
||||
toast.error(mapTicketBetError(code, msg));
|
||||
const msg = e instanceof LotteryApiBizError ? e.message : t("hall.placeFailed");
|
||||
toast.error(mapTicketBetError(code, msg, t));
|
||||
} finally {
|
||||
setPlaceLoading(false);
|
||||
}
|
||||
@@ -454,7 +459,7 @@ export function HallBettingGrid() {
|
||||
|
||||
if (catalogState.kind === "loading") {
|
||||
return (
|
||||
<section className="space-y-3" aria-label="Betting table">
|
||||
<section className="space-y-3" aria-label={t("hall.aria")}>
|
||||
<Skeleton className="h-12 rounded-xl" />
|
||||
<Skeleton className="h-72 rounded-xl" />
|
||||
<Skeleton className="h-14 rounded-xl" />
|
||||
@@ -473,7 +478,7 @@ export function HallBettingGrid() {
|
||||
className="mt-3 border-red-200 bg-white text-red-700 hover:bg-red-50"
|
||||
onClick={() => void loadCatalog()}
|
||||
>
|
||||
重试
|
||||
{t("actions.retry")}
|
||||
</Button>
|
||||
</section>
|
||||
);
|
||||
@@ -485,7 +490,7 @@ export function HallBettingGrid() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="space-y-4" aria-label="Betting table">
|
||||
<section className="space-y-4" aria-label={t("hall.aria")}>
|
||||
<div className="grid grid-cols-4 rounded-xl border border-[#e8eef7] bg-white p-1 shadow-[0_6px_18px_rgba(30,64,175,0.06)]">
|
||||
{categoryTabs.map((tab) => {
|
||||
const active = activeCategory === tab.value;
|
||||
@@ -539,9 +544,11 @@ export function HallBettingGrid() {
|
||||
<Cuboid className="size-4" aria-hidden />
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-sm font-bold">iBox</span>
|
||||
<span className="block truncate text-sm font-bold">
|
||||
{t("hall.boxMode.iboxTitle")}
|
||||
</span>
|
||||
<span className="block truncate text-[10px] text-slate-500">
|
||||
Divide all by amount
|
||||
{t("hall.boxMode.iboxDesc")}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
@@ -561,9 +568,11 @@ export function HallBettingGrid() {
|
||||
<PackageOpen className="size-4" aria-hidden />
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-sm font-bold">Box</span>
|
||||
<span className="block truncate text-sm font-bold">
|
||||
{t("hall.boxMode.boxTitle")}
|
||||
</span>
|
||||
<span className="block truncate text-[10px] text-slate-500">
|
||||
Multiply all by amount
|
||||
{t("hall.boxMode.boxDesc")}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
@@ -575,10 +584,12 @@ export function HallBettingGrid() {
|
||||
<div className="mx-auto flex size-14 items-center justify-center rounded-full bg-slate-200 text-slate-600">
|
||||
<Ticket className="size-7" aria-hidden />
|
||||
</div>
|
||||
<p className="mt-4 text-lg font-bold text-slate-900">Closed</p>
|
||||
<p className="mt-1 text-xs">This issue is now closed.</p>
|
||||
<p className="mt-4 text-lg font-bold text-slate-900">
|
||||
{t("hall.closed.title")}
|
||||
</p>
|
||||
<p className="mt-1 text-xs">{t("hall.closed.subtitle")}</p>
|
||||
<div className="mt-5 rounded-lg border border-[#cbdcf7] bg-white px-3 py-3 text-left text-xs text-[#315a9f]">
|
||||
The betting window has closed. Please wait for the next issue to place your bets.
|
||||
{t("hall.closed.description")}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -593,10 +604,10 @@ export function HallBettingGrid() {
|
||||
<thead>
|
||||
<tr className="border-b border-[#e8eef7] bg-[#f5f8fd] text-[11px] font-semibold text-[#32518d]">
|
||||
<th className="sticky left-0 z-20 w-12 bg-[#f5f8fd] px-2 py-3 text-center">
|
||||
No.
|
||||
{t("hall.table.no")}
|
||||
</th>
|
||||
<th className="sticky left-12 z-20 w-24 bg-[#f5f8fd] px-2 py-3 text-center">
|
||||
Number
|
||||
{t("hall.table.number")}
|
||||
<span className="block text-[10px] font-normal text-[#6b7896]">
|
||||
({numberPlaceholder})
|
||||
</span>
|
||||
@@ -609,12 +620,18 @@ export function HallBettingGrid() {
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<th className="min-w-28 px-2 py-3 text-center">Stake Amount</th>
|
||||
<th className="min-w-28 px-2 py-3 text-center">Commission / Rebate</th>
|
||||
<th className="min-w-28 px-2 py-3 text-center">Actual Deduction</th>
|
||||
<th className="min-w-28 px-2 py-3 text-center">
|
||||
{t("hall.table.stake")}
|
||||
</th>
|
||||
<th className="min-w-28 px-2 py-3 text-center">
|
||||
{t("hall.table.rebate")}
|
||||
</th>
|
||||
<th className="min-w-28 px-2 py-3 text-center">
|
||||
{t("hall.table.actual")}
|
||||
</th>
|
||||
</>
|
||||
)}
|
||||
<th className="w-10 px-2 py-3" aria-label="Delete" />
|
||||
<th className="w-10 px-2 py-3" aria-label={t("hall.table.delete")} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -695,7 +712,7 @@ export function HallBettingGrid() {
|
||||
disabled={tableDisabled || rows.length <= 1}
|
||||
onClick={() => removeRow(row.id)}
|
||||
className="inline-flex size-8 items-center justify-center rounded-full text-[#ff4d4f] hover:bg-red-50 disabled:text-slate-300 disabled:hover:bg-transparent"
|
||||
aria-label={`删除第 ${index + 1} 行`}
|
||||
aria-label={t("actions.deleteRow", { row: index + 1 })}
|
||||
>
|
||||
<Trash2 className="size-4" aria-hidden />
|
||||
</button>
|
||||
@@ -713,13 +730,13 @@ export function HallBettingGrid() {
|
||||
className="flex h-11 w-full items-center justify-center gap-1.5 border-t border-[#edf2f9] text-sm font-semibold text-[#1d57b7] hover:bg-[#f7faff] disabled:text-slate-300"
|
||||
>
|
||||
<CirclePlus className="size-4" aria-hidden />
|
||||
Add Row
|
||||
{t("hall.table.addRow")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between rounded-xl border border-[#e9eef7] bg-[#f8fbff] px-4 py-3 text-sm shadow-[0_6px_20px_rgba(15,23,42,0.04)]">
|
||||
<span className="font-medium text-slate-800">Draft Total</span>
|
||||
<span className="font-medium text-slate-800">{t("hall.table.draftTotal")}</span>
|
||||
<span className="text-lg font-bold tabular-nums text-[#0b3f96]">
|
||||
{formatMinorAsCurrency(draftSummary.actual, currencyCode)}
|
||||
</span>
|
||||
@@ -727,7 +744,7 @@ export function HallBettingGrid() {
|
||||
|
||||
{sealedBetUi ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-600">
|
||||
已封盘:当前表格不可编辑,请等待下一期。
|
||||
{t("hall.table.sealedHint")}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -738,7 +755,11 @@ export function HallBettingGrid() {
|
||||
className="h-12 w-full rounded-xl border-0 bg-[#e5002c] text-base font-bold text-white shadow-[0_8px_20px_rgba(229,0,44,0.26)] hover:bg-[#d10028]"
|
||||
>
|
||||
<Ticket className="size-5" aria-hidden />
|
||||
{previewLoading ? "Previewing..." : activeCategory === "JACKPOT" ? "Closed" : "Submit Bet"}
|
||||
{previewLoading
|
||||
? t("hall.table.previewing")
|
||||
: activeCategory === "JACKPOT"
|
||||
? t("hall.closed.title")
|
||||
: t("hall.table.submitBet")}
|
||||
</Button>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user