Skip to content

Commit 2233043

Browse files
feat(i18n): implement full internationalization support, superseding #570 (#573)
## 📝 Pull Request Template ### 1. Related Issue Closes #557 #232 ### 2. Type of Change (select one) Type of Change: New Feature ### 3. Description supersedes #570 ### 4. Testing - [x] I have tested this locally. - [ ] I have updated or added relevant tests. ### 5. Checklist - [x] I have read the [Code of Conduct](./CODE_OF_CONDUCT.md) - [x] I have followed the [Contributing Guidelines](./CONTRIBUTING.md) - [x] My changes follow the project's coding style --------- Co-authored-by: DigHuang <114602213+DigHuang@users.noreply.github.com>
1 parent c5224d7 commit 2233043

53 files changed

Lines changed: 2513 additions & 662 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ API_I18N_ENABLED=true
1515
API_HOST=localhost
1616
API_PORT=8000
1717

18-
# Supported languages: en_US, en_GB, zh-Hans, zh-Hant
19-
# You can interact with the agent in your preferred language.
18+
# Supported languages: en, zh_CN, zh_TW, ja
19+
# You can interact with the agent in your preferred language.
2020
# This variable is mainly intended for frontend use; setting LANG is not required.
21-
LANG=en-US
21+
LANG=en
2222
TIMEZONE=America/New_York
2323
PYTHONIOENCODING=utf-8
2424

frontend/bun.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,14 @@
5656
"dayjs": "^1.11.19",
5757
"echarts": "^6.0.0",
5858
"framer-motion": "^12.23.26",
59+
"i18next": "^25.7.2",
5960
"isbot": "5.1.31",
6061
"lucide-react": "^0.559.0",
6162
"mutative": "^1.3.0",
6263
"next-themes": "^0.4.6",
6364
"react": "^19.2.0",
6465
"react-dom": "^19.2.0",
66+
"react-i18next": "^16.5.0",
6567
"react-markdown": "^10.1.0",
6668
"rehype-raw": "^7.0.0",
6769
"remark-gfm": "^4.0.1",

frontend/src/api/agent.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,25 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
22
import { VALUECELL_AGENT } from "@/constants/agent";
33
import { API_QUERY_KEYS } from "@/constants/api";
44
import { type ApiResponse, apiClient } from "@/lib/api-client";
5+
import { useLanguage } from "@/store/settings-store";
56
import type { AgentInfo } from "@/types/agent";
67

78
export const useGetAgentInfo = (params: { agentName: string }) => {
9+
const language = useLanguage();
10+
811
return useQuery({
9-
queryKey: API_QUERY_KEYS.AGENT.agentInfo(Object.values(params)),
12+
queryKey: API_QUERY_KEYS.AGENT.agentInfo([
13+
...Object.values(params),
14+
language,
15+
]),
1016
queryFn: async () => {
1117
// Return hardcoded data for ValueCellAgent
1218
if (params.agentName === "ValueCellAgent") {
1319
return Promise.resolve({ data: VALUECELL_AGENT });
1420
}
1521
// Fetch from API for other agents
1622
return apiClient.get<ApiResponse<AgentInfo>>(
17-
`/agents/by-name/${params.agentName}`,
23+
`/agents/by-name/${params.agentName}?language=${language}`,
1824
);
1925
},
2026
select: (data) => data.data,
@@ -24,11 +30,16 @@ export const useGetAgentInfo = (params: { agentName: string }) => {
2430
export const useGetAgentList = (
2531
params: { enabled_only: string } = { enabled_only: "false" },
2632
) => {
33+
const language = useLanguage();
34+
2735
return useQuery({
28-
queryKey: API_QUERY_KEYS.AGENT.agentList(Object.values(params)),
36+
queryKey: API_QUERY_KEYS.AGENT.agentList([
37+
...Object.values(params),
38+
language,
39+
]),
2940
queryFn: () =>
3041
apiClient.get<ApiResponse<{ agents: AgentInfo[] }>>(
31-
`/agents/?enabled_only=${params.enabled_only}`,
42+
`/agents/?enabled_only=${params.enabled_only}&language=${language}`,
3243
),
3344
select: (data) => data.data.agents,
3445
});

frontend/src/api/stock.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2-
import {
3-
API_QUERY_KEYS,
4-
USER_LANGUAGE,
5-
VALUECELL_BACKEND_URL,
6-
} from "@/constants/api";
2+
import { API_QUERY_KEYS, VALUECELL_BACKEND_URL } from "@/constants/api";
73
import { type ApiResponse, apiClient } from "@/lib/api-client";
4+
import { useLanguage } from "@/store/settings-store";
85
import { useSystemStore } from "@/store/system-store";
96
import type {
107
Stock,
@@ -22,17 +19,20 @@ export const useGetWatchlist = () =>
2219
select: (data) => data.data,
2320
});
2421

25-
export const useGetStocksList = (params: { query: string }) =>
26-
useQuery({
27-
queryKey: API_QUERY_KEYS.STOCK.stockSearch(Object.values(params)),
22+
export const useGetStocksList = (params: { query: string }) => {
23+
const language = useLanguage();
24+
25+
return useQuery({
26+
queryKey: API_QUERY_KEYS.STOCK.stockSearch([params.query, language]),
2827
queryFn: ({ signal }) =>
2928
apiClient.get<ApiResponse<{ results: Stock[] }>>(
30-
`watchlist/asset/search?q=${params.query}&language=${USER_LANGUAGE}`,
29+
`watchlist/asset/search?q=${params.query}&language=${language}`,
3130
{ signal },
3231
),
3332
select: (data) => data.data.results,
3433
enabled: !!params.query,
3534
});
35+
};
3636

3737
export const useAddStockToWatchlist = () => {
3838
const queryClient = useQueryClient();

frontend/src/api/system.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
22
import { toast } from "sonner";
33
import { API_QUERY_KEYS, VALUECELL_BACKEND_URL } from "@/constants/api";
44
import { type ApiResponse, apiClient } from "@/lib/api-client";
5+
import { useLanguage } from "@/store/settings-store";
56
import { useSystemStore } from "@/store/system-store";
67
import type {
78
StrategyDetail,
@@ -68,22 +69,29 @@ export const useSignOut = () => {
6869
export const useGetStrategyList = (
6970
params: { limit: number; days: number } = { limit: 10, days: 7 },
7071
) => {
72+
const language = useLanguage();
73+
7174
return useQuery({
72-
queryKey: API_QUERY_KEYS.SYSTEM.strategyList(Object.values(params)),
75+
queryKey: API_QUERY_KEYS.SYSTEM.strategyList([
76+
...Object.values(params),
77+
language,
78+
]),
7379
queryFn: () =>
7480
apiClient.get<ApiResponse<StrategyRankItem[]>>(
75-
`${VALUECELL_BACKEND_URL}/strategy/list?limit=${params.limit}&days=${params.days}`,
81+
`${VALUECELL_BACKEND_URL}/strategy/list?limit=${params.limit}&days=${params.days}&language=${language}`,
7682
),
7783
select: (data) => data.data,
7884
});
7985
};
8086

8187
export const useGetStrategyDetail = (id: number | null) => {
88+
const language = useLanguage();
89+
8290
return useQuery({
83-
queryKey: API_QUERY_KEYS.SYSTEM.strategyDetail([id ?? ""]),
91+
queryKey: API_QUERY_KEYS.SYSTEM.strategyDetail([id ?? "", language]),
8492
queryFn: () =>
8593
apiClient.get<ApiResponse<StrategyDetail>>(
86-
`${VALUECELL_BACKEND_URL}/strategy/detail/${id}`,
94+
`${VALUECELL_BACKEND_URL}/strategy/detail/${id}?language=${language}`,
8795
),
8896
select: (data) => data.data,
8997
enabled: !!id,
@@ -122,15 +130,21 @@ export const usePublishStrategy = () => {
122130
* @param region - Optional region override for testing (e.g., "cn" or "default").
123131
* In development, you can set this to test different regions.
124132
*/
125-
export const useGetDefaultTickers = (region?: string) =>
126-
useQuery({
127-
queryKey: ["system", "default-tickers", region],
133+
export const useGetDefaultTickers = (region?: string) => {
134+
const language = useLanguage();
135+
136+
return useQuery({
137+
queryKey: ["system", "default-tickers", region, language],
128138
queryFn: () => {
129-
const params = region ? `?region=${region}` : "";
139+
const regionParam = region ? `region=${region}` : "";
140+
const langParam = `language=${language}`;
141+
const params = [regionParam, langParam].filter(Boolean).join("&");
142+
130143
return apiClient.get<ApiResponse<DefaultTickersResponse>>(
131-
`system/default-tickers${params}`,
144+
`system/default-tickers?${params}`,
132145
);
133146
},
134147
select: (data) => data.data,
135148
staleTime: 1000 * 60 * 60, // Cache for 1 hour, region doesn't change frequently
136149
});
150+
};

frontend/src/app/agent/components/agent-view/common-agent-area.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useQueryClient } from "@tanstack/react-query";
22
import { type FC, memo, useCallback, useEffect, useState } from "react";
3+
import { useTranslation } from "react-i18next";
34
import {
45
Navigate,
56
useLocation,
@@ -42,6 +43,7 @@ interface CommonAgentAreaProps {
4243
}
4344

4445
const CommonAgentAreaContent: FC<CommonAgentAreaProps> = ({ agentName }) => {
46+
const { t } = useTranslation();
4547
const { data: agent, isLoading: isLoadingAgent } = useGetAgentInfo({
4648
agentName: agentName ?? "",
4749
});
@@ -208,7 +210,7 @@ const CommonAgentAreaContent: FC<CommonAgentAreaProps> = ({ agentName }) => {
208210
<>
209211
<ChatConversationHeader agent={agent} />
210212
<ChatWelcomeScreen
211-
title={`Welcome to ${agent.display_name}!`}
213+
title={t("agent.welcome", { name: agent.display_name })}
212214
inputValue={inputValue}
213215
onInputChange={handleInputChange}
214216
onSendMessage={handleSendMessage}
@@ -235,7 +237,7 @@ const CommonAgentAreaContent: FC<CommonAgentAreaProps> = ({ agentName }) => {
235237
value={inputValue}
236238
onChange={handleInputChange}
237239
onSend={handleSendMessage}
238-
placeholder="Type your message..."
240+
placeholder={t("chat.input.placeholder")}
239241
disabled={isStreaming}
240242
variant="chat"
241243
/>

frontend/src/app/agent/components/agent-view/strategy-agent-area.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Plus } from "lucide-react";
22
import { type FC, useEffect, useState } from "react";
3+
import { useTranslation } from "react-i18next";
34
import {
45
useDeleteStrategy,
56
useGetStrategyDetails,
@@ -34,6 +35,7 @@ const EmptyIllustration = () => (
3435
);
3536

3637
const StrategyAgentArea: FC<AgentViewProps> = () => {
38+
const { t } = useTranslation();
3739
const { data: strategies = [], isLoading: isLoadingStrategies } =
3840
useGetStrategyList();
3941
const [selectedStrategy, setSelectedStrategy] = useState<Strategy | null>(
@@ -80,7 +82,7 @@ const StrategyAgentArea: FC<AgentViewProps> = () => {
8082
<div className="flex flex-1 overflow-hidden">
8183
{/* Left section: Strategy list */}
8284
<div className="flex w-96 flex-col gap-4 border-r py-6 *:px-6">
83-
<p className="font-semibold text-base">Trading Strategies</p>
85+
<p className="font-semibold text-base">{t("strategy.title")}</p>
8486

8587
{strategies && strategies.length > 0 ? (
8688
<TradeStrategyGroup
@@ -97,10 +99,9 @@ const StrategyAgentArea: FC<AgentViewProps> = () => {
9799
) : (
98100
<div className="flex flex-1 flex-col items-center justify-center gap-4">
99101
<EmptyIllustration />
100-
101102
<div className="flex flex-col gap-3 text-center text-base text-gray-400">
102-
<p>No trading strategies</p>
103-
<p>Create your first trading strategy</p>
103+
<p>{t("strategy.noStrategies")}</p>
104+
<p>{t("strategy.createFirst")}</p>
104105
</div>
105106

106107
<CreateStrategyModal>
@@ -109,7 +110,7 @@ const StrategyAgentArea: FC<AgentViewProps> = () => {
109110
className="w-full gap-3 rounded-lg py-4 text-base"
110111
>
111112
<Plus className="size-6" />
112-
Add trading strategy
113+
{t("strategy.add")}
113114
</Button>
114115
</CreateStrategyModal>
115116
</div>
@@ -135,7 +136,7 @@ const StrategyAgentArea: FC<AgentViewProps> = () => {
135136
<div className="flex size-full flex-col items-center justify-center gap-8">
136137
<EmptyIllustration />
137138
<p className="font-normal text-base text-gray-400">
138-
No running strategies
139+
{t("strategy.noStrategies")}
139140
</p>
140141
</div>
141142
)}

frontend/src/app/agent/components/chat-conversation/chat-conversation-header.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { MessageCircle, Settings } from "lucide-react";
22
import { type FC, memo } from "react";
3+
import { useTranslation } from "react-i18next";
34
import { Link } from "react-router";
45
import { Button } from "@/components/ui/button";
56
import {
@@ -16,6 +17,7 @@ interface ChatConversationHeaderProps {
1617
}
1718

1819
const ChatConversationHeader: FC<ChatConversationHeaderProps> = ({ agent }) => {
20+
const { t } = useTranslation();
1921
return (
2022
<header className="flex w-full items-center justify-between p-6">
2123
<div className="flex items-center gap-2">
@@ -44,7 +46,7 @@ const ChatConversationHeader: FC<ChatConversationHeaderProps> = ({ agent }) => {
4446
<MessageCircle size={16} className="text-gray-700" />
4547
</Button>
4648
</TooltipTrigger>
47-
<TooltipContent>New Conversation</TooltipContent>
49+
<TooltipContent>{t("chat.newConversation")}</TooltipContent>
4850
</Tooltip>
4951
</Link>
5052
<Link to="./config">
@@ -58,7 +60,7 @@ const ChatConversationHeader: FC<ChatConversationHeaderProps> = ({ agent }) => {
5860
<Settings size={16} className="text-gray-700" />
5961
</Button>
6062
</TooltipTrigger>
61-
<TooltipContent>Settings</TooltipContent>
63+
<TooltipContent>{t("chat.settings")}</TooltipContent>
6264
</Tooltip>
6365
</Link>
6466
</div>

frontend/src/app/agent/components/chat-conversation/chat-input-area.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ArrowUp } from "lucide-react";
22
import { type FC, memo } from "react";
3+
import { useTranslation } from "react-i18next";
34
import { Button } from "@/components/ui/button";
45
import ScrollTextarea from "@/components/valuecell/scroll/scroll-textarea";
56
import { cn } from "@/lib/utils";
@@ -20,11 +21,14 @@ const ChatInputArea: FC<ChatInputAreaProps> = ({
2021
onChange,
2122
onSend,
2223
onKeyDown,
23-
placeholder = "Type your message...",
24+
placeholder,
2425
disabled = false,
2526
className,
2627
variant = "chat",
2728
}) => {
29+
const { t } = useTranslation();
30+
const resolvedPlaceholder = placeholder ?? t("chat.input.placeholder");
31+
2832
const handleKeyDown = async (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
2933
// Send message on Enter key (excluding Shift+Enter line breaks and IME composition state)
3034
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
@@ -56,7 +60,7 @@ const ChatInputArea: FC<ChatInputAreaProps> = ({
5660
value={value}
5761
onInput={(e) => onChange(e.currentTarget.value)}
5862
onKeyDown={handleKeyDown}
59-
placeholder={placeholder}
63+
placeholder={resolvedPlaceholder}
6064
maxHeight={120}
6165
minHeight={24}
6266
disabled={disabled}

0 commit comments

Comments
 (0)