Implement internationalization support by adding translation functionality and updating UI components to use translated strings. Add language configuration in .env.example and integrate translations across various strategy and UI components.

This commit is contained in:
discountry
2025-12-09 00:35:28 +08:00
parent 03df1006cc
commit 85e7f245c0
17 changed files with 1166 additions and 295 deletions
+62 -34
View File
@@ -5,6 +5,7 @@ import { getExchangeDisplayName, resolveExchangeId } from "../exchanges/create-a
import { buildAdapterFromEnv } from "../exchanges/resolve-from-env";
import { BasisArbEngine, type BasisArbSnapshot } from "../strategy/basis-arb-engine";
import { formatNumber } from "../utils/format";
import { t } from "../i18n";
interface BasisAppProps {
onExit: () => void;
@@ -31,7 +32,7 @@ export function BasisApp({ onExit }: BasisAppProps) {
useEffect(() => {
if (exchangeId !== "aster") {
setError(new Error("期现套利策略目前仅支持 Aster 交易所。请设置 EXCHANGE=aster 后重试。"));
setError(new Error(t("basis.onlyAster")));
return;
}
try {
@@ -57,8 +58,8 @@ export function BasisApp({ onExit }: BasisAppProps) {
if (error) {
return (
<Box flexDirection="column" padding={1}>
<Text color="red">: {error.message}</Text>
<Text color="gray"> Esc </Text>
<Text color="red">{t("basis.startFailed", { message: error.message })}</Text>
<Text color="gray">{t("common.backHint")}</Text>
</Box>
);
}
@@ -66,7 +67,7 @@ export function BasisApp({ onExit }: BasisAppProps) {
if (!snapshot) {
return (
<Box padding={1}>
<Text></Text>
<Text>{t("basis.initializing")}</Text>
</Box>
);
}
@@ -85,10 +86,15 @@ export function BasisApp({ onExit }: BasisAppProps) {
const fundingRatePct = snapshot.fundingRate != null ? `${(snapshot.fundingRate * 100).toFixed(4)}%` : "-";
const fundingUpdated = snapshot.fundingLastUpdate ? new Date(snapshot.fundingLastUpdate).toLocaleTimeString() : "-";
const nextFundingTime = snapshot.nextFundingTime ? new Date(snapshot.nextFundingTime).toLocaleTimeString() : "-";
const fundingIncomePerFunding = snapshot.fundingIncomePerFunding != null ? `${formatNumber(snapshot.fundingIncomePerFunding, 4)} USDT` : "-";
const fundingIncomePerDay = snapshot.fundingIncomePerDay != null ? `${formatNumber(snapshot.fundingIncomePerDay, 4)} USDT` : "-";
const takerFeesPerRoundTrip = snapshot.takerFeesPerRoundTrip != null ? `${formatNumber(snapshot.takerFeesPerRoundTrip, 4)} USDT` : "-";
const fundingCountToBreakeven = snapshot.fundingCountToBreakeven != null ? `${formatNumber(snapshot.fundingCountToBreakeven, 2)} ` : "-";
const fundingIncomePerFunding =
snapshot.fundingIncomePerFunding != null ? `${formatNumber(snapshot.fundingIncomePerFunding, 4)} USDT` : "-";
const fundingIncomePerDay =
snapshot.fundingIncomePerDay != null ? `${formatNumber(snapshot.fundingIncomePerDay, 4)} USDT` : "-";
const takerFeesPerRoundTrip =
snapshot.takerFeesPerRoundTrip != null ? `${formatNumber(snapshot.takerFeesPerRoundTrip, 4)} USDT` : "-";
const fundingCountToBreakeven =
snapshot.fundingCountToBreakeven != null ? formatNumber(snapshot.fundingCountToBreakeven, 2) : "-";
const feePct = (basisConfig.takerFeeRate * 100).toFixed(4);
const feedStatus = snapshot.feedStatus;
const lastLogs = snapshot.tradeLog.slice(-5);
const spotBalances = (snapshot.spotBalances ?? []).filter((b) => Math.abs(b.free) > 0 || Math.abs(b.locked) > 0);
@@ -97,72 +103,94 @@ export function BasisApp({ onExit }: BasisAppProps) {
return (
<Box flexDirection="column" paddingX={1}>
<Box flexDirection="column" marginBottom={1}>
<Text color="cyanBright">Basis Arbitrage Dashboard</Text>
<Text color="cyanBright">{t("basis.title")}</Text>
<Text>
: {exchangeName} : {snapshot.futuresSymbol} : {snapshot.spotSymbol}
{t("basis.headerLine", {
exchange: exchangeName,
futures: snapshot.futuresSymbol,
spot: snapshot.spotSymbol,
})}
</Text>
<Text color="gray"> Esc 数据状态: 期货({feedStatus.futures ? "OK" : "--"}) ({feedStatus.spot ? "OK" : "--"}) ({feedStatus.funding ? "OK" : "--"})</Text>
<Text color="gray">: {lastUpdated}</Text>
<Text color="gray">
{t("basis.statusLine", {
futuresStatus: feedStatus.futures ? "OK" : "--",
spotStatus: feedStatus.spot ? "OK" : "--",
fundingStatus: feedStatus.funding ? "OK" : "--",
})}
</Text>
<Text color="gray">{t("basis.lastUpdated", { time: lastUpdated })}</Text>
</Box>
<Box flexDirection="row" marginBottom={1}>
<Box flexDirection="column" marginRight={4}>
<Text color="greenBright"></Text>
<Text>: {futuresBid} : {futuresAsk}</Text>
<Text color="gray">: {futuresUpdated}</Text>
<Text color="greenBright">{t("basis.section.futures")}</Text>
<Text>{t("basis.bookLine", { bid: futuresBid, ask: futuresAsk })}</Text>
<Text color="gray">{t("basis.updatedAt", { time: futuresUpdated })}</Text>
</Box>
<Box flexDirection="column">
<Text color="greenBright"></Text>
<Text>: {spotBid} : {spotAsk}</Text>
<Text color="gray">: {spotUpdated}</Text>
<Text color="greenBright">{t("basis.section.spot")}</Text>
<Text>{t("basis.bookLine", { bid: spotBid, ask: spotAsk })}</Text>
<Text color="gray">{t("basis.updatedAt", { time: spotUpdated })}</Text>
</Box>
</Box>
<Box flexDirection="column" marginBottom={1}>
<Text color="yellow"></Text>
<Text>: {fundingRatePct}</Text>
<Text color="gray">: {fundingUpdated} : {nextFundingTime}</Text>
<Text>(): {fundingIncomePerFunding} (): {fundingIncomePerDay}</Text>
<Text>(): {takerFeesPerRoundTrip} : {fundingCountToBreakeven}</Text>
<Text color="yellow">{t("basis.section.funding")}</Text>
<Text>{t("basis.fundingRate", { rate: fundingRatePct })}</Text>
<Text color="gray">
{t("basis.fundingTimes", { updated: fundingUpdated, next: nextFundingTime })}
</Text>
<Text>{t("basis.fundingIncome", { per: fundingIncomePerFunding, perDay: fundingIncomePerDay })}</Text>
<Text>{t("basis.takerFees", { fees: takerFeesPerRoundTrip, count: fundingCountToBreakeven })}</Text>
</Box>
<Box flexDirection="row" marginBottom={1}>
<Box flexDirection="column" marginRight={4}>
<Text color="cyan">0</Text>
<Text color="cyan">{t("basis.spotBalanceTitle")}</Text>
{spotBalances.length ? (
spotBalances.map((b) => (
<Text key={`spot-${b.asset}`}>
{b.asset}: {formatNumber(b.free, 8)} {formatNumber(b.locked, 8)}
{t("basis.balanceLine", {
asset: b.asset,
free: formatNumber(b.free, 8),
locked: formatNumber(b.locked, 8),
})}
</Text>
))
) : (
<Text color="gray"></Text>
<Text color="gray">{t("basis.none")}</Text>
)}
</Box>
<Box flexDirection="column">
<Text color="cyan">0</Text>
<Text color="cyan">{t("basis.futuresBalanceTitle")}</Text>
{futuresBalances.length ? (
futuresBalances.map((b) => (
<Text key={`fut-${b.asset}`}>
{b.asset}: {formatNumber(b.wallet, 8)} {formatNumber(b.available, 8)}
{t("basis.futuresBalanceLine", {
asset: b.asset,
wallet: formatNumber(b.wallet, 8),
available: formatNumber(b.available, 8),
})}
</Text>
))
) : (
<Text color="gray"></Text>
<Text color="gray">{t("basis.none")}</Text>
)}
</Box>
</Box>
<Box flexDirection="column" marginBottom={1}>
<Text color={snapshot.opportunity ? "greenBright" : "redBright"}> / </Text>
<Text color={snapshot.opportunity ? "green" : undefined}>: {spread} USDT {spreadBps} bp</Text>
<Text color={snapshot.opportunity ? "greenBright" : "redBright"}>{t("basis.spreadTitle")}</Text>
<Text color={snapshot.opportunity ? "green" : undefined}>
{t("basis.spreadLine", { spread, bps: spreadBps })}
</Text>
<Text color={snapshot.opportunity ? "green" : "red"}>
taker ({(basisConfig.takerFeeRate * 100).toFixed(4)}% × ): {netSpread} USDT {netSpreadBps} bp
{t("basis.netSpreadLine", { feePct, net: netSpread, netBps: netSpreadBps })}
</Text>
</Box>
<Box flexDirection="column">
<Text color="yellow"></Text>
<Text color="yellow">{t("common.section.recent")}</Text>
{lastLogs.length ? (
lastLogs.map((entry, index) => {
const color = entry.type === "entry" ? "green" : entry.type === "exit" ? "red" : undefined;
@@ -173,7 +201,7 @@ export function BasisApp({ onExit }: BasisAppProps) {
);
})
) : (
<Text color="gray"></Text>
<Text color="gray">{t("common.noLogs")}</Text>
)}
</Box>
</Box>