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,17 +3,12 @@
import { Wallet } from "lucide-react";
import Link from "next/link";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { getWalletBalance, getWalletLogs } from "@/api/wallet";
import { Button, buttonVariants } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { PlayerPanel } from "@/components/layout/player-panel";
import {
TransferInDialog,
TransferOutDialog,
@@ -21,14 +16,13 @@ import {
import { WalletLogsBlock } from "@/features/wallet/wallet-logs-block";
import { formatMinorAsCurrency } from "@/lib/money";
import { formatWalletClientError } from "@/lib/wallet-api-error";
import { cn } from "@/lib/utils";
import { usePlayerSessionStore } from "@/stores/player-session-store";
import type { WalletLogsData } from "@/types/api/wallet-logs";
import type { WalletBalanceData } from "@/types/api/wallet-balance";
import type { WalletLogsData } from "@/types/api/wallet-logs";
export function WalletScreen() {
const profile = usePlayerSessionStore((s) => s.profile);
const { t } = useTranslation("player");
const [balance, setBalance] = useState<WalletBalanceData | null>(null);
const [logs, setLogs] = useState<WalletLogsData | null>(null);
const [filter, setFilter] = useState("");
@@ -70,7 +64,7 @@ export function WalletScreen() {
setLogs(L);
} catch (e) {
if (!cancelled) {
setError(formatWalletClientError(e));
setError(formatWalletClientError(e, t));
}
} finally {
if (!cancelled) {
@@ -83,7 +77,7 @@ export function WalletScreen() {
return () => {
cancelled = true;
};
}, [filter]);
}, [filter, t]);
const refreshAll = useCallback(async () => {
setError(null);
@@ -98,138 +92,102 @@ export function WalletScreen() {
});
setLogs(L);
} catch (e) {
setError(formatWalletClientError(e));
setError(formatWalletClientError(e, t));
} finally {
setLogsLoading(false);
setLoading(false);
}
}, [filter]);
}, [filter, t]);
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<h1 className="text-lg font-semibold tracking-tight"></h1>
<div className="mt-2 flex flex-wrap gap-2">
<Link
href="/wallet/transfer-in"
className={cn(
buttonVariants({ variant: "secondary", size: "sm" }),
"text-xs",
)}
<PlayerPanel title={t("wallet.title")} subtitle={t("wallet.subtitle")} eyebrow={t("brand.name")}>
<div className="space-y-4">
{error ? (
<div className="rounded-xl border border-red-200 bg-red-50 px-4 py-4 text-sm text-red-700">
<p>{error}</p>
<Button
type="button"
className="mt-3 bg-[#e5002c] text-white hover:bg-[#d10028]"
onClick={() => void refreshAll()}
>
</Link>
<Link
href="/wallet/transfer-out"
className={cn(
buttonVariants({ variant: "secondary", size: "sm" }),
"text-xs",
)}
>
</Link>
<Link
href="/wallet/logs"
className={cn(
buttonVariants({ variant: "secondary", size: "sm" }),
"text-xs",
)}
>
</Link>
</div>
</div>
<Link
href="/hall"
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"shrink-0 self-start",
)}
>
</Link>
</div>
{error ? (
<Card className="border-destructive/40">
<CardHeader>
<CardTitle className="text-destructive"></CardTitle>
<CardDescription>{error}</CardDescription>
</CardHeader>
<CardContent>
<Button type="button" onClick={() => void refreshAll()}>
{t("actions.retry")}
</Button>
</CardContent>
</Card>
) : null}
<Card>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base">
<Wallet className="size-5 opacity-80" aria-hidden />
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{loading ? (
<Skeleton className="h-12 w-full max-w-xs rounded-lg" />
) : (
<>
<div>
<p className="text-xs text-muted-foreground"></p>
<p className="font-heading text-2xl font-semibold tabular-nums text-[#52c41a]">
{formatMinorAsCurrency(
balance?.balance ?? 0,
currency,
)}
</p>
<p className="mt-1 text-xs text-muted-foreground">
{" "}
{formatMinorAsCurrency(
balance?.available_balance ?? 0,
currency,
)}
</p>
</div>
<div className="rounded-lg border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
{" "}
<span className="font-medium text-foreground">
{balance?.main_balance == null
? "—(待接入主站)"
: formatMinorAsCurrency(balance.main_balance, currency)}
</span>
</div>
</>
)}
<div className="flex gap-2">
<TransferInDialog
idPrefix="wallet-"
currency={currency}
lotteryMinor={Number(balance?.balance ?? 0)}
onSuccess={refreshAll}
triggerVariant="wallet"
/>
<TransferOutDialog
idPrefix="wallet-"
currency={currency}
availableMinor={Number(balance?.available_balance ?? 0)}
onSuccess={refreshAll}
triggerVariant="wallet"
/>
</div>
</CardContent>
</Card>
) : null}
<WalletLogsBlock
logs={logs}
logsLoading={loading || logsLoading}
filter={filter}
onFilterChange={setFilter}
currency={currency}
/>
</div>
<section className="relative overflow-hidden rounded-xl bg-[#e5002c] px-4 py-5 text-white shadow-[0_10px_28px_rgba(229,0,44,0.25)]">
<div className="relative flex items-center gap-3">
<div className="flex size-13 shrink-0 items-center justify-center rounded-full bg-white text-[#d81435] shadow-sm">
<Wallet className="size-7" aria-hidden />
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold text-white/90">{t("wallet.balance")}</p>
{loading ? (
<Skeleton className="mt-2 h-8 w-44 rounded-md bg-white/25" />
) : (
<p className="mt-1 text-2xl font-black leading-none tabular-nums tracking-normal">
{formatMinorAsCurrency(balance?.balance ?? 0, currency)}
</p>
)}
<p className="mt-2 text-xs text-white/75">
{t("wallet.available", {
amount: formatMinorAsCurrency(balance?.available_balance ?? 0, currency),
})}
</p>
</div>
</div>
</section>
<div className="grid grid-cols-2 gap-3">
<TransferInDialog
idPrefix="wallet-"
currency={currency}
lotteryMinor={Number(balance?.balance ?? 0)}
onSuccess={refreshAll}
triggerVariant="hall"
triggerLabel={t("wallet.transferIn")}
triggerClassName="h-12 rounded-lg text-base font-bold"
/>
<TransferOutDialog
idPrefix="wallet-"
currency={currency}
availableMinor={Number(balance?.available_balance ?? 0)}
onSuccess={refreshAll}
triggerVariant="hall"
triggerLabel={t("wallet.transferOut")}
triggerClassName="h-12 rounded-lg text-base font-bold"
/>
</div>
<div className="grid grid-cols-3 gap-2 text-center text-xs font-bold">
<Link
className="rounded-lg border border-[#e5edf8] bg-[#f8fbff] py-2 text-[#0b56b7]"
href="/wallet/transfer-in"
>
{t("wallet.inPage")}
</Link>
<Link
className="rounded-lg border border-[#e5edf8] bg-[#f8fbff] py-2 text-[#0b56b7]"
href="/wallet/transfer-out"
>
{t("wallet.outPage")}
</Link>
<Link
className="rounded-lg border border-[#e5edf8] bg-[#f8fbff] py-2 text-[#0b56b7]"
href="/wallet/logs"
>
{t("wallet.logs")}
</Link>
</div>
<WalletLogsBlock
logs={logs}
logsLoading={loading || logsLoading}
filter={filter}
onFilterChange={setFilter}
currency={currency}
/>
</div>
</PlayerPanel>
);
}