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,9 +1,9 @@
"use client";
import { isAxiosError } from "axios";
import { ChevronLeft, Loader2 } from "lucide-react";
import Link from "next/link";
import { Loader2 } from "lucide-react";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { postWalletTransferIn, postWalletTransferOut } from "@/api/wallet";
@@ -17,6 +17,7 @@ import {
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { PlayerPanel } from "@/components/layout/player-panel";
import { formatMinorAsCurrency, parseDecimalInputToMinor } from "@/lib/money";
import { formatWalletClientError } from "@/lib/wallet-api-error";
import { LotteryApiBizError } from "@/types/api/errors";
@@ -25,14 +26,15 @@ import { LotteryApiBizError } from "@/types/api/errors";
async function handleTransferMaybePending(
e: unknown,
onRefresh: () => Promise<void>,
t: (key: string) => string,
): Promise<boolean> {
if (e instanceof LotteryApiBizError && e.code === 1002) {
toast.message(e.message || "处理中…");
toast.message(e.message || t("wallet.pendingToast"));
await onRefresh();
return true;
}
if (isAxiosError(e) && e.response?.status === 409) {
toast.message("转账处理中,请稍后刷新。");
toast.message(t("wallet.pendingShort"));
await onRefresh();
return true;
}
@@ -61,6 +63,7 @@ export function TransferInPanel({
onCancel: () => void;
variant?: PanelVariant;
}) {
const { t } = useTranslation("player");
const [amountText, setAmountText] = useState("");
const [submitting, setSubmitting] = useState(false);
const [localError, setLocalError] = useState<string | null>(null);
@@ -76,7 +79,7 @@ export function TransferInPanel({
const submit = async () => {
setLocalError(null);
if (parsedMinor == null || parsedMinor < 1) {
setLocalError("请输入有效金额。");
setLocalError(t("wallet.invalidAmount"));
return;
}
setSubmitting(true);
@@ -86,15 +89,15 @@ export function TransferInPanel({
currency,
idempotent_key: crypto.randomUUID(),
});
toast.success("转入成功,彩票钱包余额已更新。");
toast.success(t("wallet.successIn"));
setAmountText("");
await onSuccess();
} catch (e) {
if (await handleTransferMaybePending(e, onSuccess)) {
setLocalError(formatWalletClientError(e));
if (await handleTransferMaybePending(e, onSuccess, t)) {
setLocalError(formatWalletClientError(e, t));
return;
}
setLocalError(formatWalletClientError(e));
setLocalError(formatWalletClientError(e, t));
} finally {
setSubmitting(false);
}
@@ -104,17 +107,17 @@ export function TransferInPanel({
variant === "page" ? (
<Button
type="button"
className="w-full"
className="h-11 w-full rounded-lg bg-[#07459f] text-base font-bold text-white hover:bg-[#063b88]"
disabled={submitting}
onClick={() => void submit()}
>
{submitting ? (
<>
<Loader2 className="mr-2 size-4 animate-spin" />
{t("actions.processing")}
</>
) : (
"确认转入"
t("wallet.confirmIn")
)}
</Button>
) : (
@@ -125,7 +128,7 @@ export function TransferInPanel({
disabled={submitting}
onClick={onCancel}
>
{t("actions.cancel")}
</Button>
<Button
type="button"
@@ -135,10 +138,10 @@ export function TransferInPanel({
{submitting ? (
<>
<Loader2 className="mr-2 size-4 animate-spin" />
{t("actions.processing")}
</>
) : (
"确认转入"
t("wallet.confirmIn")
)}
</Button>
</div>
@@ -147,32 +150,34 @@ export function TransferInPanel({
return (
<>
<div className="grid gap-3 py-1">
<div className="rounded-lg bg-muted/50 px-3 py-2 text-xs">
<div className="rounded-xl border border-[#e5edf8] bg-[#f8fbff] px-3 py-2 text-xs">
<p>
:{" "}
<span className="text-muted-foreground"></span>
{t("wallet.mainBalance")}{" "}
<span className="text-muted-foreground">{t("wallet.mainPending")}</span>
</p>
<p className="mt-1">
:{" "}
{t("wallet.lotteryBalance")}{" "}
<span className="font-medium text-foreground">
{formatMinorAsCurrency(lotteryMinor, currency)}
</span>
</p>
</div>
<div className="grid gap-2">
<Label htmlFor={tid}></Label>
<Label htmlFor={tid}>{t("wallet.inAmount")}</Label>
<Input
id={tid}
inputMode="decimal"
placeholder="例如 1000.00"
placeholder={t("wallet.exampleIn")}
value={amountText}
onChange={(ev) => setAmountText(ev.target.value)}
disabled={submitting}
autoComplete="off"
className="h-11 rounded-lg border-[#dce7f7] bg-white text-base"
/>
<p className="text-xs text-muted-foreground">
:{" "}
{formatMinorAsCurrency(previewAfter, currency)}
{t("wallet.afterInPreview", {
amount: formatMinorAsCurrency(previewAfter, currency),
})}
</p>
</div>
{localError ? (
@@ -196,6 +201,7 @@ export function TransferOutPanel({
onCancel: () => void;
variant?: PanelVariant;
}) {
const { t } = useTranslation("player");
const [amountText, setAmountText] = useState("");
const [submitting, setSubmitting] = useState(false);
const [localError, setLocalError] = useState<string | null>(null);
@@ -218,11 +224,11 @@ export function TransferOutPanel({
const submit = async () => {
setLocalError(null);
if (parsedMinor == null || parsedMinor < 1) {
setLocalError("请输入有效金额。");
setLocalError(t("wallet.invalidAmount"));
return;
}
if (parsedMinor > availableMinor) {
setLocalError("转出金额不能超过可用余额。");
setLocalError(t("wallet.outExceeds"));
return;
}
setSubmitting(true);
@@ -232,15 +238,15 @@ export function TransferOutPanel({
currency,
idempotent_key: crypto.randomUUID(),
});
toast.success("转出成功,资金将返回主站钱包。");
toast.success(t("wallet.successOut"));
setAmountText("");
await onSuccess();
} catch (e) {
if (await handleTransferMaybePending(e, onSuccess)) {
setLocalError(formatWalletClientError(e));
if (await handleTransferMaybePending(e, onSuccess, t)) {
setLocalError(formatWalletClientError(e, t));
return;
}
setLocalError(formatWalletClientError(e));
setLocalError(formatWalletClientError(e, t));
} finally {
setSubmitting(false);
}
@@ -250,17 +256,17 @@ export function TransferOutPanel({
variant === "page" ? (
<Button
type="button"
className="w-full"
className="h-11 w-full rounded-lg bg-[#e5002c] text-base font-bold text-white hover:bg-[#d10028]"
disabled={submitting}
onClick={() => void submit()}
>
{submitting ? (
<>
<Loader2 className="mr-2 size-4 animate-spin" />
{t("actions.processing")}
</>
) : (
"确认转出"
t("wallet.confirmOut")
)}
</Button>
) : (
@@ -271,7 +277,7 @@ export function TransferOutPanel({
disabled={submitting}
onClick={onCancel}
>
{t("actions.cancel")}
</Button>
<Button
type="button"
@@ -281,10 +287,10 @@ export function TransferOutPanel({
{submitting ? (
<>
<Loader2 className="mr-2 size-4 animate-spin" />
{t("actions.processing")}
</>
) : (
"确认转出"
t("wallet.confirmOut")
)}
</Button>
</div>
@@ -293,9 +299,9 @@ export function TransferOutPanel({
return (
<>
<div className="grid gap-3 py-1">
<div className="rounded-lg bg-muted/50 px-3 py-2 text-xs">
<div className="rounded-xl border border-[#e5edf8] bg-[#f8fbff] px-3 py-2 text-xs">
<p>
:{" "}
{t("wallet.lotteryAvailable")}{" "}
<span className="font-medium text-foreground">
{formatMinorAsCurrency(availableMinor, currency)}
</span>
@@ -303,7 +309,7 @@ export function TransferOutPanel({
</div>
<div className="grid gap-2">
<div className="flex items-end justify-between gap-2">
<Label htmlFor={tid}></Label>
<Label htmlFor={tid}>{t("wallet.outAmount")}</Label>
<Button
type="button"
variant="link"
@@ -311,22 +317,25 @@ export function TransferOutPanel({
onClick={fillAll}
disabled={submitting || availableMinor < 1}
>
{" "}
{formatMinorAsCurrency(availableMinor, currency)}
{t("wallet.allOut", {
amount: formatMinorAsCurrency(availableMinor, currency),
})}
</Button>
</div>
<Input
id={tid}
inputMode="decimal"
placeholder="例如 500.00"
placeholder={t("wallet.exampleOut")}
value={amountText}
onChange={(ev) => setAmountText(ev.target.value)}
disabled={submitting}
autoComplete="off"
className="h-11 rounded-lg border-[#dce7f7] bg-white text-base"
/>
<p className="text-xs text-muted-foreground">
:{" "}
{formatMinorAsCurrency(previewAfter, currency)}
{t("wallet.afterOutPreview", {
amount: formatMinorAsCurrency(previewAfter, currency),
})}
</p>
</div>
{localError ? (
@@ -344,21 +353,21 @@ export function TransferInPage({
lotteryMinor,
onSuccess,
}: PanelBase & { lotteryMinor: number }) {
const { t } = useTranslation("player");
return (
<div className="flex flex-col gap-4">
<Link
href="/wallet"
className="inline-flex w-fit items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
>
<ChevronLeft className="size-4" />
</Link>
<Card>
<PlayerPanel
title={t("wallet.transferInTitle")}
subtitle={t("wallet.transferInSubtitle", { currency })}
eyebrow={t("wallet.title")}
backHref="/wallet"
backLabel={t("wallet.title")}
>
<Card className="rounded-xl border-[#e5edf8] shadow-[0_8px_24px_rgba(15,23,42,0.05)]">
<CardHeader>
<CardTitle></CardTitle>
<CardTitle className="text-[#0b3f96]">{t("wallet.transferInTitle")}</CardTitle>
<CardDescription>
1.00{" "}
{currency}
{t("wallet.transferInDescription")}
</CardDescription>
</CardHeader>
<CardContent>
@@ -372,7 +381,7 @@ export function TransferInPage({
/>
</CardContent>
</Card>
</div>
</PlayerPanel>
);
}
@@ -382,20 +391,21 @@ export function TransferOutPage({
availableMinor,
onSuccess,
}: PanelBase & { availableMinor: number }) {
const { t } = useTranslation("player");
return (
<div className="flex flex-col gap-4">
<Link
href="/wallet"
className="inline-flex w-fit items-center gap-1 text-sm text-muted-foreground hover:text-foreground"
>
<ChevronLeft className="size-4" />
</Link>
<Card>
<PlayerPanel
title={t("wallet.transferOutTitle")}
subtitle={t("wallet.transferOutSubtitle", { currency })}
eyebrow={t("wallet.title")}
backHref="/wallet"
backLabel={t("wallet.title")}
>
<Card className="rounded-xl border-[#e5edf8] shadow-[0_8px_24px_rgba(15,23,42,0.05)]">
<CardHeader>
<CardTitle></CardTitle>
<CardTitle className="text-[#0b3f96]">{t("wallet.transferOutTitle")}</CardTitle>
<CardDescription>
{t("wallet.transferOutDescription")}
</CardDescription>
</CardHeader>
<CardContent>
@@ -409,6 +419,6 @@ export function TransferOutPage({
/>
</CardContent>
</Card>
</div>
</PlayerPanel>
);
}