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

@@ -3,6 +3,7 @@
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { getTicketItems } from "@/api/ticket-items";
import { Button } from "@/components/ui/button";
@@ -11,11 +12,12 @@ import { PlayerPanel } from "@/components/layout/player-panel";
import { StatusDot, ticketStatusDisplay } from "@/features/orders/ticket-item-status";
import { formatMinorAsCurrency } from "@/lib/money";
import { formatLotteryInstant } from "@/lib/player-datetime";
import { playLabelZh } from "@/lib/play-labels";
import { playLabel } from "@/lib/play-labels";
import type { TicketItemListRow } from "@/types/api/ticket-items";
export function TicketOrdersListScreen() {
const searchParams = useSearchParams();
const { t } = useTranslation("player");
const drawNoFilter = useMemo(
() => (searchParams.get("draw_no") ?? "").trim(),
[searchParams],
@@ -45,14 +47,14 @@ export function TicketOrdersListScreen() {
setLastPage(res.last_page);
setTotal(res.total);
} catch {
setError("加载失败");
setError(t("orders.loadFailed"));
if (!append) setItems([]);
} finally {
setLoading(false);
setLoadingMore(false);
}
},
[drawNoFilter],
[drawNoFilter, t],
);
useEffect(() => {
@@ -67,13 +69,13 @@ export function TicketOrdersListScreen() {
};
return (
<PlayerPanel title="My Bets" subtitle="Recent ticket records" eyebrow="N lotto">
<PlayerPanel title={t("orders.title")} subtitle={t("orders.subtitle")} eyebrow={t("brand.name")}>
<div className="space-y-4">
<div className="rounded-xl border border-[#e6edf8] bg-[#f8fbff] p-3">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<p className="text-xs font-bold text-[#32518d]">
{drawNoFilter ? "Filtered Issue" : "Total Records"}
{drawNoFilter ? t("orders.filteredIssue") : t("orders.totalRecords")}
</p>
<p className="mt-1 truncate font-mono text-lg font-black text-[#0b3f96]">
{drawNoFilter || total}
@@ -84,14 +86,14 @@ export function TicketOrdersListScreen() {
href="/orders"
className="shrink-0 rounded-full border border-[#dce7f7] bg-white px-3 py-1.5 text-xs font-bold text-[#0b56b7]"
>
Clear
{t("actions.clear")}
</Link>
) : (
<Link
href="/hall"
className="shrink-0 rounded-full bg-[#e5002c] px-3 py-1.5 text-xs font-bold text-white"
>
Bet Now
{t("orders.betNow")}
</Link>
)}
</div>
@@ -112,17 +114,17 @@ export function TicketOrdersListScreen() {
className="mt-3 bg-[#e5002c] text-white hover:bg-[#d10028]"
onClick={() => void fetchPage(1, false)}
>
Retry
{t("actions.retry")}
</Button>
</div>
) : items.length === 0 ? (
<div className="rounded-xl border border-dashed border-[#dce7f7] bg-[#f8fbff] px-4 py-10 text-center">
<p className="text-sm font-bold text-slate-700">No bet records yet.</p>
<p className="text-sm font-bold text-slate-700">{t("orders.empty")}</p>
<Link
href="/hall"
className="mt-4 inline-flex h-9 items-center rounded-lg bg-[#e5002c] px-4 text-sm font-bold text-white"
>
Submit Bet
{t("orders.submitBet")}
</Link>
</div>
) : (
@@ -134,6 +136,7 @@ export function TicketOrdersListScreen() {
row.status,
row.win_amount,
row.jackpot_win_amount,
t,
);
const totalWin = row.win_amount + row.jackpot_win_amount;
return (
@@ -148,20 +151,24 @@ export function TicketOrdersListScreen() {
{row.draw_no ?? "—"}
</p>
<p className="mt-1 truncate text-xs text-slate-500">
{playLabelZh(row.play_code)} · {row.original_number ?? row.play_code}
{playLabel(row.play_code, t)} · {row.original_number ?? row.play_code}
</p>
</div>
<StatusDot label={st.label} dotClass={st.dotClass} ring={st.ring} />
</div>
<div className="mt-3 grid grid-cols-2 gap-2">
<div className="rounded-lg bg-[#f8fbff] px-3 py-2">
<p className="text-[10px] font-bold uppercase text-[#7890b8]">Stake</p>
<p className="text-[10px] font-bold uppercase text-[#7890b8]">
{t("orders.stake")}
</p>
<p className="mt-1 text-sm font-black text-slate-900">
{formatMinorAsCurrency(row.total_bet_amount, cur)}
</p>
</div>
<div className="rounded-lg bg-[#f8fbff] px-3 py-2">
<p className="text-[10px] font-bold uppercase text-[#7890b8]">Deduction</p>
<p className="text-[10px] font-bold uppercase text-[#7890b8]">
{t("orders.deduction")}
</p>
<p className="mt-1 text-sm font-black text-[#0b3f96]">
{formatMinorAsCurrency(row.actual_deduct_amount, cur)}
</p>
@@ -169,7 +176,7 @@ export function TicketOrdersListScreen() {
</div>
{totalWin > 0 && row.status === "settled_win" ? (
<p className="mt-2 text-xs font-bold text-emerald-600">
Win {formatMinorAsCurrency(totalWin, cur)}
{t("orders.win", { amount: formatMinorAsCurrency(totalWin, cur) })}
</p>
) : null}
<p className="mt-2 text-[11px] text-slate-500">
@@ -187,7 +194,7 @@ export function TicketOrdersListScreen() {
disabled={loadingMore}
onClick={() => loadMore()}
>
{loadingMore ? "Loading..." : "Load More"}
{loadingMore ? t("actions.loading") : t("actions.loadMore")}
</Button>
) : null}
</>