From 76d197f568079c6c9ca0d6a1b90585d1f350de4a Mon Sep 17 00:00:00 2001 From: byronwang2005 Date: Wed, 17 Dec 2025 22:25:11 +0800 Subject: [PATCH 1/2] feat(i18n): add i18n files for enhancement --- frontend/src/api/system.ts | 3 +- .../modals/create-strategy-modal.tsx | 11 +- .../modals/share-portfolio-modal.tsx | 29 ++-- frontend/src/app/setting/general.tsx | 14 +- .../components/valuecell/app/app-sidebar.tsx | 14 +- .../valuecell/app/backend-health-check.tsx | 6 +- .../valuecell/modal/copy-strategy-modal.tsx | 43 ++++-- frontend/src/constants/schema.ts | 144 ++++++++++-------- frontend/src/hooks/use-update-toast.tsx | 54 ++++--- frontend/src/i18n/index.ts | 4 +- frontend/src/i18n/locales/en.json | 42 ++++- frontend/src/i18n/locales/ja.json | 42 ++++- frontend/src/i18n/locales/zh_CN.json | 42 ++++- frontend/src/i18n/locales/zh_TW.json | 42 ++++- frontend/src/root.tsx | 12 +- 15 files changed, 350 insertions(+), 152 deletions(-) diff --git a/frontend/src/api/system.ts b/frontend/src/api/system.ts index bcdde1d7f..4890ed598 100644 --- a/frontend/src/api/system.ts +++ b/frontend/src/api/system.ts @@ -1,6 +1,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { API_QUERY_KEYS, VALUECELL_BACKEND_URL } from "@/constants/api"; +import i18n from "@/i18n"; import { type ApiResponse, apiClient } from "@/lib/api-client"; import { useLanguage } from "@/store/settings-store"; import { useSystemStore } from "@/store/system-store"; @@ -111,7 +112,7 @@ export const usePublishStrategy = () => { ); }, onSuccess: () => { - toast.success("Strategy published successfully"); + toast.success(i18n.t("strategy.toast.published")); queryClient.invalidateQueries({ queryKey: API_QUERY_KEYS.SYSTEM.strategyList([]), }); diff --git a/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx b/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx index 3bc4dd527..a5dcd0a32 100644 --- a/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx +++ b/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx @@ -1,7 +1,7 @@ import { useStore } from "@tanstack/react-form"; import { AlertCircleIcon } from "lucide-react"; import type { FC, RefObject } from "react"; -import { memo, useImperativeHandle, useState } from "react"; +import { memo, useImperativeHandle, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useGetModelProviderDetail } from "@/api/setting"; import { @@ -29,9 +29,9 @@ import { TradingStrategyForm } from "@/components/valuecell/form/trading-strateg import { StepIndicator } from "@/components/valuecell/step-indicator"; import { TRADING_SYMBOLS } from "@/constants/agent"; import { - aiModelSchema, - exchangeSchema, - tradingStrategySchema, + createAiModelSchema, + createExchangeSchema, + createTradingStrategySchema, } from "@/constants/schema"; import { useAppForm } from "@/hooks/use-form"; import { tracker } from "@/lib/tracker"; @@ -50,6 +50,9 @@ const CreateStrategyModal: FC = ({ children, }) => { const { t } = useTranslation(); + const aiModelSchema = useMemo(() => createAiModelSchema(t), [t]); + const exchangeSchema = useMemo(() => createExchangeSchema(t), [t]); + const tradingStrategySchema = useMemo(() => createTradingStrategySchema(t), [t]); const [open, setOpen] = useState(false); const [currentStep, setCurrentStep] = useState(1); const [error, setError] = useState(null); diff --git a/frontend/src/app/agent/components/strategy-items/modals/share-portfolio-modal.tsx b/frontend/src/app/agent/components/strategy-items/modals/share-portfolio-modal.tsx index 5896d5341..3b400a161 100644 --- a/frontend/src/app/agent/components/strategy-items/modals/share-portfolio-modal.tsx +++ b/frontend/src/app/agent/components/strategy-items/modals/share-portfolio-modal.tsx @@ -11,6 +11,7 @@ import { useRef, useState, } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; @@ -38,6 +39,7 @@ export interface SharePortfolioCardRef { const SharePortfolioModal: FC<{ ref?: RefObject; }> = ({ ref }) => { + const { t } = useTranslation(); const cardRef = useRef(null); const [isDownloading, setIsDownloading] = useState(false); @@ -84,18 +86,21 @@ const SharePortfolioModal: FC<{ await writeFile(path, new Uint8Array(arrayBuffer)); setOpen(false); - toast.success("Image downloaded successfully", { + toast.success(t("sharePortfolio.toast.downloaded"), { action: { - label: "open file", + label: t("sharePortfolio.toast.openFile"), onClick: async () => { return await openPath(path); }, }, }); } catch (err) { - toast.error(`Failed to download image: ${JSON.stringify(err)}`, { - duration: 6 * 1000, - }); + toast.error( + t("sharePortfolio.toast.downloadFailed", { + error: JSON.stringify(err), + }), + { duration: 6 * 1000 }, + ); } finally { setIsDownloading(false); } @@ -116,7 +121,9 @@ const SharePortfolioModal: FC<{ className="h-[600px] w-[434px] overflow-hidden border-none bg-transparent p-0 shadow-none" showCloseButton={false} > - Share Portfolio + + {t("sharePortfolio.title")} + {/* Card to be captured */}
-

Model

+

{t("sharePortfolio.fields.model")}

{data.llm_model_id} -

Exchange

+

{t("sharePortfolio.fields.exchange")}

-

Strategy

+

{t("sharePortfolio.fields.strategy")}

{data.strategy_type}
@@ -219,7 +226,7 @@ const SharePortfolioModal: FC<{ className="h-12 flex-1 rounded-xl border-border bg-card font-medium text-base hover:bg-muted" onClick={() => setOpen(false)} > - Cancel + {t("strategy.action.cancel")} diff --git a/frontend/src/app/setting/general.tsx b/frontend/src/app/setting/general.tsx index c2661deff..2aa592d51 100644 --- a/frontend/src/app/setting/general.tsx +++ b/frontend/src/app/setting/general.tsx @@ -96,10 +96,16 @@ export default function GeneralPage() { - English (United States) - 简体中文 - 繁體中文 - 日本語 + + {t("general.language.options.en")} + + + {t("general.language.options.zh_CN")} + + + {t("general.language.options.zh_TW")} + + {t("general.language.options.ja")} diff --git a/frontend/src/components/valuecell/app/app-sidebar.tsx b/frontend/src/components/valuecell/app/app-sidebar.tsx index 3d3f1f0b5..a004b73de 100644 --- a/frontend/src/components/valuecell/app/app-sidebar.tsx +++ b/frontend/src/components/valuecell/app/app-sidebar.tsx @@ -5,6 +5,7 @@ import { type ReactNode, useMemo, } from "react"; +import { useTranslation } from "react-i18next"; import { NavLink, useLocation } from "react-router"; import { useGetAgentList } from "@/api/agent"; import { @@ -126,6 +127,7 @@ const SidebarMenuItem: FC = ({ }; const AppSidebar: FC = () => { + const { t } = useTranslation(); const pathArray = useLocation().pathname.split("/"); const prefix = useMemo(() => { @@ -144,25 +146,25 @@ const AppSidebar: FC = () => { { id: "home", icon: Logo, - label: "Home", + label: t("nav.home"), to: "/home", }, { id: "strategy", icon: StrategyAgent, - label: "Strategy", + label: t("nav.strategy"), to: "/agent/StrategyAgent", }, { id: "ranking", icon: Ranking, - label: "Ranking", + label: t("nav.ranking"), to: "/ranking", }, { id: "market", icon: Market, - label: "Market", + label: t("nav.market"), to: "/market", }, ], @@ -170,12 +172,12 @@ const AppSidebar: FC = () => { { id: "setting", icon: Setting, - label: "Setting", + label: t("nav.setting"), to: "/setting", }, ], }; - }, []); + }, [t]); const { data: agentList } = useGetAgentList({ enabled_only: "true" }); const agentItems = useMemo(() => { diff --git a/frontend/src/components/valuecell/app/backend-health-check.tsx b/frontend/src/components/valuecell/app/backend-health-check.tsx index c36d47914..8b2dd0eb1 100644 --- a/frontend/src/components/valuecell/app/backend-health-check.tsx +++ b/frontend/src/components/valuecell/app/backend-health-check.tsx @@ -1,6 +1,7 @@ import { AnimatePresence, motion } from "framer-motion"; import type React from "react"; import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useBackendHealth } from "@/api/system"; import ValuecellLogo from "@/assets/png/logo/valuecell-logo.webp"; import { Progress } from "@/components/ui/progress"; @@ -10,6 +11,7 @@ export function BackendHealthCheck({ }: { children: React.ReactNode; }) { + const { t } = useTranslation(); const { isError, isSuccess } = useBackendHealth(); const [showError, setShowError] = useState(false); const [progress, setProgress] = useState(0); @@ -92,7 +94,7 @@ export function BackendHealthCheck({ className="space-y-4" >

- Setting up environment... + {t("common.settingUpEnvironment")}

@@ -106,7 +108,7 @@ export function BackendHealthCheck({
- Loading + {t("common.loading")} {Math.round(progress)}%
diff --git a/frontend/src/components/valuecell/modal/copy-strategy-modal.tsx b/frontend/src/components/valuecell/modal/copy-strategy-modal.tsx index 3d9e49355..8e95e8b48 100644 --- a/frontend/src/components/valuecell/modal/copy-strategy-modal.tsx +++ b/frontend/src/components/valuecell/modal/copy-strategy-modal.tsx @@ -1,7 +1,8 @@ import { useStore } from "@tanstack/react-form"; import { AlertCircleIcon } from "lucide-react"; import type { FC, RefObject } from "react"; -import { memo, useImperativeHandle, useState } from "react"; +import { memo, useImperativeHandle, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useGetModelProviderDetail } from "@/api/setting"; import { useCreateStrategy, @@ -27,9 +28,9 @@ import { import { StepIndicator } from "@/components/valuecell/step-indicator"; import { TRADING_SYMBOLS } from "@/constants/agent"; import { - aiModelSchema, - copyTradingStrategySchema, - exchangeSchema, + createAiModelSchema, + createCopyTradingStrategySchema, + createExchangeSchema, } from "@/constants/schema"; import { useAppForm } from "@/hooks/use-form"; import { tracker } from "@/lib/tracker"; @@ -45,22 +46,32 @@ interface CopyStrategyModalProps { callback?: () => void; } -const STEPS = [ - { step: 1, title: "AI Models" }, - { step: 2, title: "Exchanges" }, - { step: 3, title: "Trading strategy" }, -]; - const CopyStrategyModal: FC = ({ ref, children, callback, }) => { + const { t } = useTranslation(); + const aiModelSchema = useMemo(() => createAiModelSchema(t), [t]); + const exchangeSchema = useMemo(() => createExchangeSchema(t), [t]); + const copyTradingStrategySchema = useMemo( + () => createCopyTradingStrategySchema(t), + [t], + ); const [open, setOpen] = useState(false); const [currentStep, setCurrentStep] = useState(1); const [defaultValues, setDefaultValues] = useState(); const [error, setError] = useState(null); + const STEPS = useMemo( + () => [ + { step: 1, title: t("strategy.create.steps.aiModels") }, + { step: 2, title: t("strategy.create.steps.exchanges") }, + { step: 3, title: t("strategy.create.steps.tradingStrategy") }, + ], + [t], + ); + const { data: strategies = [] } = useGetStrategyList(); const { mutateAsync: createStrategy, isPending: isCreatingStrategy } = useCreateStrategy(); @@ -201,7 +212,7 @@ const CopyStrategyModal: FC = ({

- Duplicate trading strategy + {t("strategy.copy.title")}

@@ -230,7 +241,7 @@ const CopyStrategyModal: FC = ({ {error && ( - Error Duplicating Strategy + {t("strategy.copy.error")} {error} )} @@ -241,7 +252,9 @@ const CopyStrategyModal: FC = ({ onClick={currentStep === 1 ? resetAll : handleBack} className="py-4 font-semibold text-base" > - {currentStep === 1 ? "Cancel" : "Back"} + {currentStep === 1 + ? t("strategy.action.cancel") + : t("strategy.action.back")} diff --git a/frontend/src/constants/schema.ts b/frontend/src/constants/schema.ts index 60184b036..cc9de7936 100644 --- a/frontend/src/constants/schema.ts +++ b/frontend/src/constants/schema.ts @@ -1,10 +1,12 @@ +import type { TFunction } from "i18next"; import { z } from "zod"; -export const aiModelSchema = z.object({ - provider: z.string().min(1, "Model platform is required"), - model_id: z.string().min(1, "Model selection is required"), - api_key: z.string().min(1, "API key is required"), -}); +export const createAiModelSchema = (t: TFunction) => + z.object({ + provider: z.string().min(1, t("validation.aiModel.providerRequired")), + model_id: z.string().min(1, t("validation.aiModel.modelIdRequired")), + api_key: z.string().min(1, t("validation.aiModel.apiKeyRequired")), + }); const baseStep2Fields = { exchange_id: z.string(), @@ -16,74 +18,82 @@ const baseStep2Fields = { }; // Step 2 Schema: Exchanges (conditional validation with superRefine) -export const exchangeSchema = z.union([ - // Virtual Trading - z.object({ - ...baseStep2Fields, - trading_mode: z.literal("virtual"), - }), +export const createExchangeSchema = (t: TFunction) => + z.union([ + z.object({ + ...baseStep2Fields, + trading_mode: z.literal("virtual"), + }), // Live Trading - Hyperliquid - z.object({ - ...baseStep2Fields, - trading_mode: z.literal("live"), - exchange_id: z.literal("hyperliquid"), - wallet_address: z - .string() - .min(1, "Wallet Address is required for Hyperliquid"), - private_key: z.string().min(1, "Private Key is required for Hyperliquid"), - }), + z.object({ + ...baseStep2Fields, + trading_mode: z.literal("live"), + exchange_id: z.literal("hyperliquid"), + wallet_address: z + .string() + .min(1, t("validation.exchange.walletAddressHyperliquidRequired")), + private_key: z + .string() + .min(1, t("validation.exchange.privateKeyHyperliquidRequired")), + }), // Live Trading - OKX & Coinbase (Require Passphrase) - z.object({ - ...baseStep2Fields, - trading_mode: z.literal("live"), - exchange_id: z.enum(["okx", "coinbaseexchange"]), - api_key: z.string().min(1, "API key is required"), - secret_key: z.string().min(1, "Secret key is required"), - passphrase: z.string().min(1, "Passphrase is required"), - }), + z.object({ + ...baseStep2Fields, + trading_mode: z.literal("live"), + exchange_id: z.enum(["okx", "coinbaseexchange"]), + api_key: z.string().min(1, t("validation.exchange.apiKeyRequired")), + secret_key: z.string().min(1, t("validation.exchange.secretKeyRequired")), + passphrase: z.string().min(1, t("validation.exchange.passphraseRequired")), + }), // Live Trading - Standard Exchanges - z.object({ - ...baseStep2Fields, - trading_mode: z.literal("live"), - exchange_id: z.enum(["binance", "blockchaincom", "gate", "mexc"]), - api_key: z.string().min(1, "API key is required"), - secret_key: z.string().min(1, "Secret key is required"), - }), -]); + z.object({ + ...baseStep2Fields, + trading_mode: z.literal("live"), + exchange_id: z.enum(["binance", "blockchaincom", "gate", "mexc"]), + api_key: z.string().min(1, t("validation.exchange.apiKeyRequired")), + secret_key: z.string().min(1, t("validation.exchange.secretKeyRequired")), + }), + ]); // Step 3 Schema: Trading Strategy -export const tradingStrategySchema = z.object({ - strategy_type: z.enum(["PromptBasedStrategy", "GridStrategy"]), - strategy_name: z.string().min(1, "Strategy name is required"), - initial_capital: z.number().min(1, "Initial capital must be at least 1"), - max_leverage: z - .number() - .min(1, "Leverage must be at least 1") - .max(5, "Leverage must be at most 5"), - symbols: z.array(z.string()).min(1, "At least one symbol is required"), - template_id: z.string().min(1, "Template selection is required"), - decide_interval: z - .number() - .min(10, "Interval must be at least 10 seconds") - .max(3600, "Interval must be at most 3600 seconds"), -}); + export const createTradingStrategySchema = (t: TFunction) => + z.object({ + strategy_type: z.enum(["PromptBasedStrategy", "GridStrategy"]), + strategy_name: z.string().min(1, t("validation.trading.strategyNameRequired")), + initial_capital: z + .number() + .min(1, t("validation.trading.initialCapitalMin")), + max_leverage: z + .number() + .min(1, t("validation.trading.maxLeverageMin")) + .max(5, t("validation.trading.maxLeverageMax")), + symbols: z.array(z.string()).min(1, t("validation.trading.symbolsMin")), + template_id: z.string().min(1, t("validation.trading.templateRequired")), + decide_interval: z + .number() + .min(10, t("validation.trading.decideIntervalMin")) + .max(3600, t("validation.trading.decideIntervalMax")), + }); -export const copyTradingStrategySchema = z.object({ - strategy_name: z.string().min(1, "Strategy name is required"), - initial_capital: z.number().min(1, "Initial capital must be at least 1"), - max_leverage: z - .number() - .min(1, "Leverage must be at least 1") - .max(5, "Leverage must be at most 5"), - symbols: z.array(z.string()).min(1, "At least one symbol is required"), - decide_interval: z - .number() - .min(10, "Interval must be at least 10 seconds") - .max(3600, "Interval must be at most 3600 seconds"), - strategy_type: z.enum(["PromptBasedStrategy", "GridStrategy"]), - prompt_name: z.string().min(1, "Prompt name is required"), - prompt: z.string().min(1, "Prompt is required"), -}); +export const createCopyTradingStrategySchema = (t: TFunction) => + z.object({ + strategy_name: z.string().min(1, t("validation.trading.strategyNameRequired")), + initial_capital: z + .number() + .min(1, t("validation.trading.initialCapitalMin")), + max_leverage: z + .number() + .min(1, t("validation.trading.maxLeverageMin")) + .max(5, t("validation.trading.maxLeverageMax")), + symbols: z.array(z.string()).min(1, t("validation.trading.symbolsMin")), + decide_interval: z + .number() + .min(10, t("validation.trading.decideIntervalMin")) + .max(3600, t("validation.trading.decideIntervalMax")), + strategy_type: z.enum(["PromptBasedStrategy", "GridStrategy"]), + prompt_name: z.string().min(1, t("validation.copy.promptNameRequired")), + prompt: z.string().min(1, t("validation.copy.promptRequired")), + }); diff --git a/frontend/src/hooks/use-update-toast.tsx b/frontend/src/hooks/use-update-toast.tsx index db08d4e1f..5433048be 100644 --- a/frontend/src/hooks/use-update-toast.tsx +++ b/frontend/src/hooks/use-update-toast.tsx @@ -1,9 +1,11 @@ import { relaunch } from "@tauri-apps/plugin-process"; import { check, type DownloadEvent } from "@tauri-apps/plugin-updater"; import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "sonner"; export function useUpdateToast() { + const { t } = useTranslation(); const downloadAndInstallUpdate = useCallback( async (update: Awaited>) => { if (!update) return; @@ -17,7 +19,9 @@ export function useUpdateToast() { switch (event.event) { case "Started": contentLength = event.data.contentLength; - progressToastId = toast.loading("Downloading update... 0%"); + progressToastId = toast.loading( + t("updates.toast.downloading", { percentage: 0 }), + ); break; case "Progress": @@ -27,7 +31,7 @@ export function useUpdateToast() { Math.round((downloaded / contentLength) * 100), 100, ); - toast.loading(`Downloading update... ${percentage}%`, { + toast.loading(t("updates.toast.downloading", { percentage }), { id: progressToastId, }); } @@ -38,16 +42,16 @@ export function useUpdateToast() { toast.dismiss(progressToastId); } // Show toast with relaunch or later options - toast.success("Update installed successfully!", { - description: "The app needs to restart to apply the update.", + toast.success(t("updates.toast.installedTitle"), { + description: t("updates.toast.installedDesc"), action: { - label: "Relaunch", + label: t("updates.toast.relaunch"), onClick: async () => { await relaunch(); }, }, cancel: { - label: "Later", + label: t("updates.toast.later"), onClick: () => toast.dismiss(), }, duration: Infinity, @@ -59,39 +63,43 @@ export function useUpdateToast() { } catch (downloadError) { toast.dismiss(progressToastId); toast.error( - `Failed to download update: ${JSON.stringify(downloadError)}`, + t("updates.toast.downloadFailed", { + error: JSON.stringify(downloadError), + }), ); } }, - [], + [t], ); const checkAndUpdate = useCallback(async () => { - const checkToastId = toast.loading("Checking for updates..."); + const checkToastId = toast.loading(t("updates.toast.checking")); try { const update = await check(); if (!update) { toast.dismiss(checkToastId); - toast.success("You are using the latest version"); + toast.success(t("updates.toast.latest")); return; } toast.dismiss(checkToastId); // Show toast asking user to install - const installToastId = toast.info("Update available", { - description: `A new version (${update.version}) is available. Would you like to install it now?`, + const installToastId = toast.info(t("updates.toast.availableTitle"), { + description: t("updates.toast.availableDesc", { + version: update.version, + }), action: { - label: "Install", + label: t("updates.toast.install"), onClick: async () => { toast.dismiss(installToastId); await downloadAndInstallUpdate(update); }, }, cancel: { - label: "Later", + label: t("updates.toast.later"), onClick: () => toast.dismiss(installToastId), }, duration: Infinity, @@ -99,9 +107,11 @@ export function useUpdateToast() { }); } catch (error) { toast.dismiss(checkToastId); - toast.error(`Failed to check for updates: ${JSON.stringify(error)}`); + toast.error( + t("updates.toast.checkFailed", { error: JSON.stringify(error) }), + ); } - }, [downloadAndInstallUpdate]); + }, [downloadAndInstallUpdate, t]); const checkForUpdatesSilent = useCallback(async () => { try { @@ -112,17 +122,19 @@ export function useUpdateToast() { } // Show toast asking user to install - const installToastId = toast.info("Update available", { - description: `A new version (${update.version}) is available. Would you like to install it now?`, + const installToastId = toast.info(t("updates.toast.availableTitle"), { + description: t("updates.toast.availableDesc", { + version: update.version, + }), action: { - label: "Install", + label: t("updates.toast.install"), onClick: async () => { toast.dismiss(installToastId); await downloadAndInstallUpdate(update); }, }, cancel: { - label: "Later", + label: t("updates.toast.later"), onClick: () => toast.dismiss(installToastId), }, duration: Infinity, @@ -131,7 +143,7 @@ export function useUpdateToast() { } catch { // Silently fail for auto checks } - }, [downloadAndInstallUpdate]); + }, [downloadAndInstallUpdate, t]); return { checkAndUpdate, checkForUpdatesSilent }; } diff --git a/frontend/src/i18n/index.ts b/frontend/src/i18n/index.ts index c762619b1..1e7047ed4 100644 --- a/frontend/src/i18n/index.ts +++ b/frontend/src/i18n/index.ts @@ -26,7 +26,9 @@ i18n.use(initReactI18next).init({ }, saveMissing: true, missingKeyHandler: (_lngs, _ns, key) => { - console.log("🚀 ~ missing key:", key); + if (import.meta.env.DEV) { + console.log("🚀 ~ missing key:", key); + } }, }); diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index d337bbc54..3eaf87c75 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -12,10 +12,10 @@ "title": "Language and Region", "description": "Select your preferred language and regional format for the app.", "options": { - "en-US": "English (United States)", - "zh-Hans": "简体中文", - "zh-Hant": "繁體中文", - "ja-JP": "日本語" + "en": "English (United States)", + "zh_CN": "简体中文", + "zh_TW": "繁體中文", + "ja": "日本語" } }, "theme": { @@ -103,7 +103,32 @@ } }, "common": { - "back": "Back" + "back": "Back", + "loading": "Loading", + "settingUpEnvironment": "Setting up environment..." + }, + "nav": { + "home": "Home", + "strategy": "Strategy", + "ranking": "Ranking", + "market": "Market", + "setting": "Setting" + }, + "updates": { + "toast": { + "checking": "Checking for updates...", + "latest": "You are using the latest version", + "availableTitle": "Update available", + "availableDesc": "A new version ({{version}}) is available. Would you like to install it now?", + "install": "Install", + "later": "Later", + "downloading": "Downloading update... {{percentage}}%", + "installedTitle": "Update installed successfully!", + "installedDesc": "The app needs to restart to apply the update.", + "relaunch": "Relaunch", + "downloadFailed": "Failed to download update: {{error}}", + "checkFailed": "Failed to check for updates: {{error}}" + } }, "market": { "title": "Agent Market" @@ -208,6 +233,13 @@ }, "error": "Error Creating Strategy" }, + "copy": { + "title": "Duplicate trading strategy", + "error": "Error Duplicating Strategy" + }, + "toast": { + "published": "Strategy published successfully" + }, "form": { "strategyType": { "label": "Strategy Type", diff --git a/frontend/src/i18n/locales/ja.json b/frontend/src/i18n/locales/ja.json index 5cddac8f7..b09f36495 100644 --- a/frontend/src/i18n/locales/ja.json +++ b/frontend/src/i18n/locales/ja.json @@ -12,10 +12,10 @@ "title": "言語と地域", "description": "アプリの言語と地域形式を選択してください。", "options": { - "en-US": "English (United States)", - "zh-Hans": "简体中文", - "zh-Hant": "繁體中文", - "ja-JP": "日本語" + "en": "English (United States)", + "zh_CN": "简体中文", + "zh_TW": "繁體中文", + "ja": "日本語" } }, "theme": { @@ -103,7 +103,32 @@ } }, "common": { - "back": "戻る" + "back": "戻る", + "loading": "読み込み中...", + "settingUpEnvironment": "環境をセットアップ中..." + }, + "nav": { + "home": "ホーム", + "strategy": "戦略", + "ranking": "ランキング", + "market": "マーケット", + "setting": "設定" + }, + "updates": { + "toast": { + "checking": "アップデートを確認中...", + "latest": "最新バージョンを使用しています", + "availableTitle": "アップデートがあります", + "availableDesc": "新しいバージョン ({{version}}) が利用可能です。今すぐインストールしますか?", + "install": "インストール", + "later": "後で", + "downloading": "アップデートをダウンロード中... {{percentage}}%", + "installedTitle": "アップデートが正常にインストールされました!", + "installedDesc": "適用するにはアプリの再起動が必要です。", + "relaunch": "再起動", + "downloadFailed": "アップデートのダウンロードに失敗しました: {{error}}", + "checkFailed": "アップデートの確認に失敗しました: {{error}}" + } }, "market": { "title": "Agent マーケット" @@ -208,6 +233,13 @@ }, "error": "戦略作成エラー" }, + "copy": { + "title": "取引戦略を複製", + "error": "戦略複製エラー" + }, + "toast": { + "published": "戦略が正常に公開されました" + }, "form": { "strategyType": { "label": "戦略タイプ", diff --git a/frontend/src/i18n/locales/zh_CN.json b/frontend/src/i18n/locales/zh_CN.json index 800ba46f2..36b478ab8 100644 --- a/frontend/src/i18n/locales/zh_CN.json +++ b/frontend/src/i18n/locales/zh_CN.json @@ -12,10 +12,10 @@ "title": "语言和区域", "description": "选择您喜欢的应用程序语言和区域格式。", "options": { - "en-US": "English (United States)", - "zh-Hans": "简体中文", - "zh-Hant": "繁體中文", - "ja-JP": "日本語" + "en": "English (United States)", + "zh_CN": "简体中文", + "zh_TW": "繁體中文", + "ja": "日本語" } }, "theme": { @@ -103,7 +103,32 @@ } }, "common": { - "back": "返回" + "back": "返回", + "loading": "加载中...", + "settingUpEnvironment": "正在配置环境..." + }, + "nav": { + "home": "首页", + "strategy": "策略", + "ranking": "排行榜", + "market": "市场", + "setting": "设置" + }, + "updates": { + "toast": { + "checking": "正在检查更新...", + "latest": "您正在使用最新版本", + "availableTitle": "发现可用更新", + "availableDesc": "新版本 ({{version}}) 可用。是否现在安装?", + "install": "安装", + "later": "稍后", + "downloading": "正在下载更新... {{percentage}}%", + "installedTitle": "更新安装成功!", + "installedDesc": "需要重启应用以应用更新。", + "relaunch": "重启", + "downloadFailed": "下载更新失败:{{error}}", + "checkFailed": "检查更新失败:{{error}}" + } }, "market": { "title": "Agent市场" @@ -208,6 +233,13 @@ }, "error": "创建策略失败" }, + "copy": { + "title": "复制交易策略", + "error": "复制策略失败" + }, + "toast": { + "published": "策略发布成功" + }, "form": { "strategyType": { "label": "策略类型", diff --git a/frontend/src/i18n/locales/zh_TW.json b/frontend/src/i18n/locales/zh_TW.json index 5810f0d63..32247aba6 100644 --- a/frontend/src/i18n/locales/zh_TW.json +++ b/frontend/src/i18n/locales/zh_TW.json @@ -12,10 +12,10 @@ "title": "語言與區域", "description": "選擇您偏好的應用程式語言和區域格式。", "options": { - "en-US": "English (United States)", - "zh-Hans": "简体中文", - "zh-Hant": "繁體中文", - "ja-JP": "日本語" + "en": "English (United States)", + "zh_CN": "简体中文", + "zh_TW": "繁體中文", + "ja": "日本語" } }, "theme": { @@ -103,7 +103,32 @@ } }, "common": { - "back": "返回" + "back": "返回", + "loading": "載入中...", + "settingUpEnvironment": "正在配置環境..." + }, + "nav": { + "home": "首頁", + "strategy": "策略", + "ranking": "排行榜", + "market": "市場", + "setting": "設定" + }, + "updates": { + "toast": { + "checking": "正在檢查更新...", + "latest": "您正在使用最新版本", + "availableTitle": "發現可用更新", + "availableDesc": "新版本 ({{version}}) 可用。是否現在安裝?", + "install": "安裝", + "later": "稍後", + "downloading": "正在下載更新... {{percentage}}%", + "installedTitle": "更新安裝成功!", + "installedDesc": "需要重啟應用以套用更新。", + "relaunch": "重啟", + "downloadFailed": "下載更新失敗:{{error}}", + "checkFailed": "檢查更新失敗:{{error}}" + } }, "market": { "title": "Agent 市場" @@ -208,6 +233,13 @@ }, "error": "建立策略失敗" }, + "copy": { + "title": "複製交易策略", + "error": "複製策略失敗" + }, + "toast": { + "published": "策略發布成功" + }, "form": { "strategyType": { "label": "策略類型", diff --git a/frontend/src/root.tsx b/frontend/src/root.tsx index bd53b7954..df49a2b41 100644 --- a/frontend/src/root.tsx +++ b/frontend/src/root.tsx @@ -3,14 +3,24 @@ import { ThemeProvider } from "next-themes"; import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router"; import "@/i18n"; import AppSidebar from "@/components/valuecell/app/app-sidebar"; +import { useLanguage } from "@/store/settings-store"; import { Toaster } from "./components/ui/sonner"; import "./global.css"; import { SidebarProvider } from "./components/ui/sidebar"; export function Layout({ children }: { children: React.ReactNode }) { + const language = useLanguage(); + const htmlLang = + { + en: "en", + zh_CN: "zh-CN", + zh_TW: "zh-TW", + ja: "ja", + }[language] ?? "en"; + return ( - + From fd936b64085786d85d955aed4c6d96dd59e85b67 Mon Sep 17 00:00:00 2001 From: byronwang2005 Date: Wed, 17 Dec 2025 22:30:54 +0800 Subject: [PATCH 2/2] fix: biome error --- .../modals/create-strategy-modal.tsx | 5 ++++- frontend/src/app/setting/general.tsx | 4 +++- frontend/src/constants/schema.ts | 20 ++++++++++++------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx b/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx index a5dcd0a32..6a20ff24a 100644 --- a/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx +++ b/frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx @@ -52,7 +52,10 @@ const CreateStrategyModal: FC = ({ const { t } = useTranslation(); const aiModelSchema = useMemo(() => createAiModelSchema(t), [t]); const exchangeSchema = useMemo(() => createExchangeSchema(t), [t]); - const tradingStrategySchema = useMemo(() => createTradingStrategySchema(t), [t]); + const tradingStrategySchema = useMemo( + () => createTradingStrategySchema(t), + [t], + ); const [open, setOpen] = useState(false); const [currentStep, setCurrentStep] = useState(1); const [error, setError] = useState(null); diff --git a/frontend/src/app/setting/general.tsx b/frontend/src/app/setting/general.tsx index 2aa592d51..d942bf80c 100644 --- a/frontend/src/app/setting/general.tsx +++ b/frontend/src/app/setting/general.tsx @@ -105,7 +105,9 @@ export default function GeneralPage() { {t("general.language.options.zh_TW")} - {t("general.language.options.ja")} + + {t("general.language.options.ja")} + diff --git a/frontend/src/constants/schema.ts b/frontend/src/constants/schema.ts index cc9de7936..2e025ca75 100644 --- a/frontend/src/constants/schema.ts +++ b/frontend/src/constants/schema.ts @@ -25,7 +25,7 @@ export const createExchangeSchema = (t: TFunction) => trading_mode: z.literal("virtual"), }), - // Live Trading - Hyperliquid + // Live Trading - Hyperliquid z.object({ ...baseStep2Fields, trading_mode: z.literal("live"), @@ -38,17 +38,19 @@ export const createExchangeSchema = (t: TFunction) => .min(1, t("validation.exchange.privateKeyHyperliquidRequired")), }), - // Live Trading - OKX & Coinbase (Require Passphrase) + // Live Trading - OKX & Coinbase (Require Passphrase) z.object({ ...baseStep2Fields, trading_mode: z.literal("live"), exchange_id: z.enum(["okx", "coinbaseexchange"]), api_key: z.string().min(1, t("validation.exchange.apiKeyRequired")), secret_key: z.string().min(1, t("validation.exchange.secretKeyRequired")), - passphrase: z.string().min(1, t("validation.exchange.passphraseRequired")), + passphrase: z + .string() + .min(1, t("validation.exchange.passphraseRequired")), }), - // Live Trading - Standard Exchanges + // Live Trading - Standard Exchanges z.object({ ...baseStep2Fields, trading_mode: z.literal("live"), @@ -59,10 +61,12 @@ export const createExchangeSchema = (t: TFunction) => ]); // Step 3 Schema: Trading Strategy - export const createTradingStrategySchema = (t: TFunction) => +export const createTradingStrategySchema = (t: TFunction) => z.object({ strategy_type: z.enum(["PromptBasedStrategy", "GridStrategy"]), - strategy_name: z.string().min(1, t("validation.trading.strategyNameRequired")), + strategy_name: z + .string() + .min(1, t("validation.trading.strategyNameRequired")), initial_capital: z .number() .min(1, t("validation.trading.initialCapitalMin")), @@ -80,7 +84,9 @@ export const createExchangeSchema = (t: TFunction) => export const createCopyTradingStrategySchema = (t: TFunction) => z.object({ - strategy_name: z.string().min(1, t("validation.trading.strategyNameRequired")), + strategy_name: z + .string() + .min(1, t("validation.trading.strategyNameRequired")), initial_capital: z .number() .min(1, t("validation.trading.initialCapitalMin")),