-
Stake
+
+ {t("orders.stake")}
+
{formatMinorAsCurrency(row.total_bet_amount, cur)}
-
Deduction
+
+ {t("orders.deduction")}
+
{formatMinorAsCurrency(row.actual_deduct_amount, cur)}
@@ -169,7 +176,7 @@ export function TicketOrdersListScreen() {
{totalWin > 0 && row.status === "settled_win" ? (
- Win {formatMinorAsCurrency(totalWin, cur)}
+ {t("orders.win", { amount: formatMinorAsCurrency(totalWin, cur) })}
) : null}
@@ -187,7 +194,7 @@ export function TicketOrdersListScreen() {
disabled={loadingMore}
onClick={() => loadMore()}
>
- {loadingMore ? "Loading..." : "Load More"}
+ {loadingMore ? t("actions.loading") : t("actions.loadMore")}
) : null}
>
diff --git a/src/features/player/entry-gate.tsx b/src/features/player/entry-gate.tsx
index d23a806..28fb154 100644
--- a/src/features/player/entry-gate.tsx
+++ b/src/features/player/entry-gate.tsx
@@ -12,7 +12,7 @@ import {
} from "lucide-react";
import Image from "next/image";
import { useRouter, useSearchParams } from "next/navigation";
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { getPlayerMe, getPlayerPing } from "@/api/player";
@@ -97,7 +97,6 @@ export function EntryGate() {
usePlayerSessionStore();
const [phase, setPhase] = useState("loading");
- const [progress, setProgress] = useState(0);
const [failureDetails, setFailureDetails] = useState([]);
const [steps, setSteps] = useState(initialSteps());
@@ -107,15 +106,11 @@ export function EntryGate() {
setSteps((prev) => prev.map((s) => (s.id === stepId ? { ...s, status } : s)));
}, []);
- const calculateProgress = useCallback((currentSteps: EntryStep[]) => {
- const doneCount = currentSteps.filter((s) => s.status === "done").length;
- const inProgressCount = currentSteps.filter((s) => s.status === "in-progress").length;
- return Math.round(((doneCount + inProgressCount * 0.5) / currentSteps.length) * 100);
- }, []);
-
- useEffect(() => {
- setProgress(calculateProgress(steps));
- }, [steps, calculateProgress]);
+ const progress = useMemo(() => {
+ const doneCount = steps.filter((s) => s.status === "done").length;
+ const inProgressCount = steps.filter((s) => s.status === "in-progress").length;
+ return Math.round(((doneCount + inProgressCount * 0.5) / steps.length) * 100);
+ }, [steps]);
const handleRetry = useCallback(() => {
setPhase("loading");
diff --git a/src/features/player/player-session-bar.tsx b/src/features/player/player-session-bar.tsx
index 14b2e0d..38e52df 100644
--- a/src/features/player/player-session-bar.tsx
+++ b/src/features/player/player-session-bar.tsx
@@ -1,6 +1,7 @@
"use client";
import { UserRound } from "lucide-react";
+import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
import { usePlayerSessionStore } from "@/stores/player-session-store";
@@ -10,11 +11,12 @@ import { usePlayerSessionStore } from "@/stores/player-session-store";
*/
export function PlayerSessionBar({ className }: { className?: string }) {
const profile = usePlayerSessionStore((s) => s.profile);
+ const { t } = useTranslation("player");
const label =
profile?.nickname?.trim() ||
profile?.username?.trim() ||
- (profile?.id != null ? `玩家 #${profile.id}` : null);
+ (profile?.id != null ? t("player.fallback", { id: profile.id }) : null);
return (
] 切换 + 本人命中高亮 + Jackpot */
export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps) {
+ const { t } = useTranslation("player");
const [data, setData] = useState
(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
@@ -47,11 +50,11 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
setData(row);
} catch {
setData(null);
- setError("该期开奖结果不可用或不存在");
+ setError(t("results.unavailable"));
} finally {
setLoading(false);
}
- }, [drawNo]);
+ }, [drawNo, t]);
useEffect(() => {
queueMicrotask(() => {
@@ -98,34 +101,37 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
if (loading) {
return (
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
);
}
if (error || !data) {
return (
-
-
- 开奖结果
- {error ?? "无数据"}
-
-
+
+
+
{error ?? t("results.noData")}
-
- 返回列表
-
-
-
+
+
);
}
@@ -143,60 +149,81 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
myTotals.jackpot === 0;
return (
-
-
+
+
+
-
-
-
- {data.previous_draw_no ? (
-
- ‹ 上一期
-
- ) : (
-
- )}
-
- {data.draw_no}
-
- {data.next_draw_no ? (
-
- 下一期 ›
-
- ) : (
-
- )}
-
-
- 开奖时间:{" "}
- {formatLotteryInstant(data.draw_time_iso ?? data.draw_time ?? null)}
-
-
-
-
+
+
+
+ {data.previous_draw_no ? (
+
+ {t("results.previous")}
+
+ ) : (
+
+ )}
+
+ {data.draw_no}
+
+ {data.next_draw_no ? (
+
+ {t("results.next")}
+
+ ) : (
+
+ )}
+
+
+ {t("results.drawTime", {
+ time: formatLotteryInstant(data.draw_time_iso ?? data.draw_time ?? null),
+ })}
+
+
+
+
{showMyPayout && myTotals ? (
-
本期我的派彩
+
+ {t("results.myPayout")}
+
- 常规:{formatMinorAsCurrency(myTotals.win, currency)}
+ {t("results.regular", {
+ amount: formatMinorAsCurrency(myTotals.win, currency),
+ })}
{myTotals.jackpot > 0 ? (
<>
{" "}
- · Jackpot:{formatMinorAsCurrency(myTotals.jackpot, currency)}
+ ·{" "}
+ {t("results.jackpot", {
+ amount: formatMinorAsCurrency(myTotals.jackpot, currency),
+ })}
>
) : null}
@@ -204,13 +231,13 @@ export function DrawResultDetailScreen({ drawNo }: DrawResultDetailScreenProps)
) : null}
{showHitOnly ? (
- 您的注单已命中本期开奖号码中的格子;派彩完成后将显示金额汇总。
+ {t("results.hitPending")}
) : null}
- 如果您中奖,与注单匹配的号码将以金色高亮显示(需登录)。
+ {t("results.hitHint")}
- 查看我的中奖情况
+ {t("results.viewMyWinning")}
-
-
-
+
+
+
+
);
}
diff --git a/src/features/results/draw-results-list-screen.tsx b/src/features/results/draw-results-list-screen.tsx
index 2222e4c..cc39e86 100644
--- a/src/features/results/draw-results-list-screen.tsx
+++ b/src/features/results/draw-results-list-screen.tsx
@@ -2,6 +2,7 @@
import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
import { getDrawResults } from "@/api/draw";
import { Button } from "@/components/ui/button";
@@ -13,6 +14,7 @@ import { formatLotteryInstant } from "@/lib/player-datetime";
import type { DrawResultListItem } from "@/types/api/draw-results";
export function DrawResultsListScreen() {
+ const { t } = useTranslation("player");
const [items, setItems] = useState
(null);
const [error, setError] = useState(null);
const [date, setDate] = useState("");
@@ -29,12 +31,12 @@ export function DrawResultsListScreen() {
});
setItems(res.items);
} catch {
- setError("加载失败");
+ setError(t("results.loadFailed"));
setItems(null);
} finally {
setLoading(false);
}
- }, [date]);
+ }, [date, t]);
useEffect(() => {
queueMicrotask(() => {
@@ -43,12 +45,14 @@ export function DrawResultsListScreen() {
}, [fetchList]);
return (
-
+
-
Business Date
+
+ {t("results.businessDate")}
+
void fetchList()}
>
- Apply
+ {t("actions.apply")}
@@ -82,12 +86,12 @@ export function DrawResultsListScreen() {
className="mt-3 bg-[#e5002c] text-white hover:bg-[#d10028]"
onClick={() => void fetchList()}
>
- Retry
+ {t("actions.retry")}
) : items && items.length === 0 ? (
- No results yet.
+ {t("results.empty")}
) : (
@@ -107,7 +111,7 @@ export function DrawResultsListScreen() {
- Detail
+ {t("results.detail")}
diff --git a/src/features/results/jackpot-results-strip.tsx b/src/features/results/jackpot-results-strip.tsx
index 650ea1a..dc16316 100644
--- a/src/features/results/jackpot-results-strip.tsx
+++ b/src/features/results/jackpot-results-strip.tsx
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
import { getJackpotSummary } from "@/api/jackpot";
import { formatMinorAsCurrency } from "@/lib/money";
@@ -13,6 +14,7 @@ type JackpotResultsStripProps = {
export function JackpotResultsStrip({
currencyCode = "NPR",
}: JackpotResultsStripProps) {
+ const { t } = useTranslation("player");
const [minor, setMinor] = useState(null);
const [enabled, setEnabled] = useState(false);
@@ -44,7 +46,7 @@ export function JackpotResultsStrip({
return (
- Jackpot
+ {t("results.jackpotLabel")}
{formatMinorAsCurrency(minor, currencyCode.toUpperCase())}
diff --git a/src/features/results/twenty-three-results-grid.tsx b/src/features/results/twenty-three-results-grid.tsx
index 5bc327a..2bda543 100644
--- a/src/features/results/twenty-three-results-grid.tsx
+++ b/src/features/results/twenty-three-results-grid.tsx
@@ -1,4 +1,6 @@
import type { DrawResultsNumbers } from "@/types/api/draw-results";
+import { useTranslation } from "react-i18next";
+
import { norm4d } from "@/lib/norm-4d";
import { cn } from "@/lib/utils";
@@ -15,6 +17,7 @@ export function TwentyThreeResultsGrid({
numbers,
highlighted4d,
}: TwentyThreeResultsGridProps) {
+ const { t } = useTranslation("player");
const starters = numbers.starter ?? [];
const consos = numbers.consolation ?? [];
const hits = highlighted4d ?? null;
@@ -44,7 +47,11 @@ export function TwentyThreeResultsGrid({
{(["1st", "2nd", "3rd"] as const).map((key) => (
- {key === "1st" ? "头奖" : key === "2nd" ? "二奖" : "三奖"}
+ {key === "1st"
+ ? t("results.grid.first")
+ : key === "2nd"
+ ? t("results.grid.second")
+ : t("results.grid.third")}
{numbers[key] || "—"}
@@ -54,7 +61,9 @@ export function TwentyThreeResultsGrid({
-
特别奖 (Starter)
+
+ {t("results.grid.starter")} (Starter)
+
{Array.from({ length: 10 }).map((_, i) => (
@@ -65,7 +74,9 @@ export function TwentyThreeResultsGrid({
-
安慰奖 (Consolation)
+
+ {t("results.grid.consolation")} (Consolation)
+
{Array.from({ length: 10 }).map((_, i) => (
diff --git a/src/features/wallet/wallet-logs-block.tsx b/src/features/wallet/wallet-logs-block.tsx
index 417b892..468ab83 100644
--- a/src/features/wallet/wallet-logs-block.tsx
+++ b/src/features/wallet/wallet-logs-block.tsx
@@ -1,47 +1,37 @@
"use client";
+import { useMemo } from "react";
+import { useTranslation } from "react-i18next";
+
import { Button } from "@/components/ui/button";
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { formatLocalDateTime } from "@/lib/format-local-datetime";
import { formatMinorAsCurrency } from "@/lib/money";
import type { WalletLogItem, WalletLogsData } from "@/types/api/wallet-logs";
/** 与 §4.9 筛选一致;接口 `type` 查询参数 */
-export const WALLET_FLOW_FILTERS: { value: string; label: string }[] = [
- { value: "", label: "全部" },
- { value: "transfer_in", label: "转入" },
- { value: "transfer_out", label: "转出" },
- { value: "bet", label: "下注扣款" },
- { value: "prize", label: "派彩" },
- { value: "refund", label: "退本" },
- { value: "reversal", label: "冲正" },
+export const WALLET_FLOW_FILTERS: { value: string; labelKey: string }[] = [
+ { value: "", labelKey: "wallet.flow.all" },
+ { value: "transfer_in", labelKey: "wallet.flow.transfer_in" },
+ { value: "transfer_out", labelKey: "wallet.flow.transfer_out" },
+ { value: "bet", labelKey: "wallet.flow.bet" },
+ { value: "prize", labelKey: "wallet.flow.prize" },
+ { value: "refund", labelKey: "wallet.flow.refund" },
+ { value: "reversal", labelKey: "wallet.flow.reversal" },
];
-export function logTypeLabel(t: string): string {
- const map: Record
= {
- transfer_in: "转入",
- transfer_out: "转出",
- refund: "退本",
- reversal: "冲正",
- bet: "下注扣款",
- prize: "派彩",
- };
- return map[t] ?? t;
+export function logTypeLabel(
+ type: string,
+ t?: (key: string, options?: { defaultValue?: string }) => string,
+): string {
+ return t?.(`wallet.flow.${type}`, { defaultValue: type }) ?? type;
}
-function txnStatusLabel(status: string): string {
- if (status === "posted") return "成功";
- if (status === "pending_reconcile") return "待对账";
- if (status === "reversed") return "已冲正";
- if (status === "manually_processed") return "已人工处理";
- return status;
+function txnStatusLabel(
+ status: string,
+ t: (key: string, options?: { defaultValue?: string }) => string,
+): string {
+ return t(`wallet.txnStatus.${status}`, { defaultValue: status });
}
type WalletLogsBlockProps = {
@@ -61,47 +51,59 @@ export function WalletLogsBlock({
filter,
onFilterChange,
currency,
- title = "资金流水",
+ title,
}: WalletLogsBlockProps) {
+ const { t } = useTranslation("player");
+ const resolvedTitle = title ?? t("wallet.flowsTitle");
+ const filters = useMemo(
+ () =>
+ WALLET_FLOW_FILTERS.map((f) => ({
+ ...f,
+ label: t(f.labelKey),
+ })),
+ [t],
+ );
+
return (
<>
{logs && logs.pending_reconcile.length > 0 ? (
-
-
- 待对账
-
- 以下划转主站结果未最终确认;若长时间未到账请联系客服(界面文档 §4.10
- 超时说明)。
-
-
-
+
+ {t("wallet.pendingTitle")}
+
+ {t("wallet.pendingDescription")}
+
+
{logs.pending_reconcile.map((p) => (
- {p.type === "transfer_in" ? "转入" : "转出"}{" "}
+ {logTypeLabel(p.type, t)}{" "}
{formatMinorAsCurrency(p.amount, p.currency_code)}
- 处理中
+ {t("wallet.pendingStatus")}
))}
-
-
+
+
) : null}
-
{title}
+
{resolvedTitle}
- {WALLET_FLOW_FILTERS.map((f) => (
+ {filters.map((f) => (