Skip to content

Commit 02f8753

Browse files
author
codeErrorSleep
committed
feat(i18n): Add internationalization support, including Chinese and English language switching
- Introduce i18next and react-i18next libraries to configure multi-language initialization - Create a language selector component and integrate it into the settings panel - Add translation keys for UI elements such as AI assistants, chat components, table selectors, etc. - Complete English and Simplified Chinese translations provided - Modify application entry point to asynchronously initialize i18n and wait for language to load - Update package.json dependencies to ensure that internationalization libraries are correctly introduced
1 parent 4e1ecf2 commit 02f8753

13 files changed

Lines changed: 2484 additions & 234 deletions

File tree

package-lock.json

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

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
"cmdk": "1.1.1",
7474
"date-fns": "3.6.0",
7575
"embla-carousel-react": "8.6.0",
76+
"i18next": "^25.8.14",
7677
"input-otp": "1.4.2",
7778
"lucide-react": "0.487.0",
7879
"motion": "12.23.24",
@@ -83,15 +84,16 @@
8384
"react-dnd-html5-backend": "16.0.1",
8485
"react-dom": "^19.1.0",
8586
"react-hook-form": "7.55.0",
87+
"react-i18next": "^16.5.4",
8688
"react-markdown": "^10.1.0",
8789
"react-popper": "2.3.0",
8890
"react-resizable-panels": "2.1.7",
8991
"react-responsive-masonry": "2.7.1",
9092
"react-slick": "0.31.0",
9193
"recharts": "2.15.2",
94+
"remark-gfm": "^4.0.1",
9295
"simple-icons": "^16.9.0",
9396
"sonner": "2.0.3",
94-
"remark-gfm": "^4.0.1",
9597
"sql-formatter": "^15.7.0",
9698
"tailwind-merge": "3.2.0",
9799
"tw-animate-css": "1.3.8",
@@ -108,4 +110,4 @@
108110
"typescript": "~5.8.3",
109111
"vite": "^7.0.4"
110112
}
111-
}
113+
}

src/App.tsx

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
horizontalListSortingStrategy,
5252
} from "@dnd-kit/sortable";
5353
import { SortableTab } from "@/components/ui/sortable-tab";
54+
import { useTranslation } from "react-i18next";
5455

5556
interface TabItem {
5657
id: string;
@@ -106,6 +107,7 @@ const TAB_TRIGGER_CLASS =
106107
"gap-2 group relative pr-8 bg-transparent data-[state=active]:bg-background border-b-2 border-b-transparent data-[state=active]:border-b-primary rounded-none h-9 hover:bg-muted/50 border-r border-r-border/40 last:border-r-0 shrink-0";
107108

108109
export default function App() {
110+
const { t } = useTranslation();
109111
const resolveTableScope = (driver: string, database?: string) => {
110112
const isDatabaseScoped = driver === "mysql" || driver === "clickhouse";
111113
return {
@@ -125,6 +127,8 @@ export default function App() {
125127
const [aiVisible, setAiVisible] = useState(false);
126128
const [openSettings, setOpenSettings] = useState(false);
127129
const [isFullscreen, setIsFullscreen] = useState(false);
130+
const isDefaultQueryTitle = (title?: string) =>
131+
!!title && /^(Query \(|)/.test(title);
128132
const [queriesLastUpdated, setQueriesLastUpdated] = useState(0);
129133
const [pendingCloseTabIds, setPendingCloseTabIds] = useState<string[]>([]);
130134
const [currentCloseTabId, setCurrentCloseTabId] = useState<string | null>(
@@ -174,8 +178,8 @@ export default function App() {
174178
size="sm"
175179
className="h-7 w-7 p-0"
176180
onClick={() => setOpenSettings(true)}
177-
title="Settings (Cmd/Ctrl+,)"
178-
aria-label="Open settings"
181+
title={t("app.window.settingsTooltip")}
182+
aria-label={t("app.window.openSettings")}
179183
>
180184
<Settings className="w-4 h-4" />
181185
</Button>
@@ -187,10 +191,10 @@ export default function App() {
187191
onClick={() => setAiVisible((v) => !v)}
188192
title={
189193
aiVisible
190-
? "Hide AI Panel (Cmd/Ctrl+\\)"
191-
: "Show AI Panel (Cmd/Ctrl+\\)"
194+
? t("app.window.hideAiPanel")
195+
: t("app.window.showAiPanel")
192196
}
193-
aria-label={aiVisible ? "Hide AI panel" : "Show AI panel"}
197+
aria-label={aiVisible ? t("app.window.hideAiPanelAria") : t("app.window.showAiPanelAria")}
194198
>
195199
<Sparkles className="w-4 h-4" />
196200
</Button>
@@ -266,7 +270,7 @@ export default function App() {
266270
const newTab: TabItem = {
267271
id: newTabId,
268272
type: "editor",
269-
title: `Query (${databaseName})`,
273+
title: t("app.tab.queryTitle", { database: databaseName }),
270274
connectionId,
271275
database: databaseName,
272276
driver,
@@ -374,7 +378,7 @@ export default function App() {
374378
const tab = tabs.find((t) => t.id === tabId);
375379
if (!tab || !tab.connectionId) {
376380
// TODO: Prompt user to select connection if missing
377-
alert("Please select a connection first (feature pending)");
381+
alert(t("app.error.selectConnectionFirst"));
378382
return;
379383
}
380384

@@ -489,7 +493,7 @@ export default function App() {
489493
} catch (e) {
490494
const errorMessage = e instanceof Error ? e.message : String(e);
491495
console.error("get_table_data failed", errorMessage);
492-
toast.error("Failed to load table data", {
496+
toast.error(t("app.error.loadTableData"), {
493497
description: errorMessage,
494498
});
495499
}
@@ -517,11 +521,11 @@ export default function App() {
517521
scope: "full_table",
518522
filePath,
519523
});
520-
toast.success(`Export completed (${result.rowCount} rows)`, {
524+
toast.success(t("app.success.exportCompleted", { count: result.rowCount }), {
521525
description: result.filePath,
522526
});
523527
} catch (e) {
524-
toast.error("Export failed", {
528+
toast.error(t("app.error.exportFailed"), {
525529
description: e instanceof Error ? e.message : String(e),
526530
});
527531
}
@@ -543,7 +547,7 @@ export default function App() {
543547
const newTab: TabItem = {
544548
id: tabId,
545549
type: "ddl",
546-
title: `DDL: ${ctx.table}`,
550+
title: t("app.tab.ddlTitle", { table: ctx.table }),
547551
connectionId: ctx.connectionId,
548552
database: ctx.database,
549553
schema: ctx.schema,
@@ -601,7 +605,7 @@ export default function App() {
601605
} catch (e) {
602606
const errorMessage = e instanceof Error ? e.message : String(e);
603607
console.error("handleTableRefresh failed", errorMessage);
604-
toast.error("Failed to refresh table", {
608+
toast.error(t("app.error.refreshTable"), {
605609
description: errorMessage,
606610
});
607611
}
@@ -641,7 +645,7 @@ export default function App() {
641645
} catch (e) {
642646
const errorMessage = e instanceof Error ? e.message : String(e);
643647
console.error("handlePageChange failed", errorMessage);
644-
toast.error("Failed to change page", {
648+
toast.error(t("app.error.changePage"), {
645649
description: errorMessage,
646650
});
647651
}
@@ -682,7 +686,7 @@ export default function App() {
682686
} catch (e) {
683687
const errorMessage = e instanceof Error ? e.message : String(e);
684688
console.error("handlePageSizeChange failed", errorMessage);
685-
toast.error("Failed to change page size", {
689+
toast.error(t("app.error.changePageSize"), {
686690
description: errorMessage,
687691
});
688692
}
@@ -736,7 +740,7 @@ export default function App() {
736740
} catch (e) {
737741
const errorMessage = e instanceof Error ? e.message : String(e);
738742
console.error("handleSortChange failed", errorMessage);
739-
toast.error("Failed to sort table", {
743+
toast.error(t("app.error.sortTable"), {
740744
description: errorMessage,
741745
});
742746
}
@@ -793,7 +797,7 @@ export default function App() {
793797
} catch (e) {
794798
const errorMessage = e instanceof Error ? e.message : String(e);
795799
console.error("handleFilterChange failed", errorMessage);
796-
toast.error("Failed to filter table", {
800+
toast.error(t("app.error.filterTable"), {
797801
description: errorMessage,
798802
});
799803
}
@@ -854,7 +858,7 @@ export default function App() {
854858
),
855859
);
856860
} catch (e) {
857-
toast.error("Failed to save query", {
861+
toast.error(t("app.error.saveQuery"), {
858862
description: e instanceof Error ? e.message : String(e),
859863
});
860864
throw e;
@@ -1209,13 +1213,13 @@ export default function App() {
12091213
{tab.type === "editor" && tab.isDirty && (
12101214
<span
12111215
className="inline-block w-1.5 h-1.5 rounded-full bg-amber-500 ml-1 shrink-0"
1212-
aria-label="Unsaved changes"
1216+
aria-label={t("app.tab.unsavedChanges")}
12131217
/>
12141218
)}
12151219
</span>
12161220
<button
12171221
type="button"
1218-
aria-label={`Close ${title}`}
1222+
aria-label={t("app.tab.closeAria", { title })}
12191223
className="absolute right-1 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 p-1 hover:bg-accent rounded-sm cursor-pointer transition-opacity"
12201224
onClick={(e) => {
12211225
e.stopPropagation();
@@ -1231,12 +1235,12 @@ export default function App() {
12311235
<ContextMenuItem
12321236
onClick={() => handleCloseTab(tab.id)}
12331237
>
1234-
Close Tab
1238+
{t("app.tab.closeTab")}
12351239
</ContextMenuItem>
12361240
<ContextMenuItem
12371241
onClick={() => handleCloseOtherTabs(tab.id)}
12381242
>
1239-
Close Other Tabs
1243+
{t("app.tab.closeOtherTabs")}
12401244
</ContextMenuItem>
12411245
</ContextMenuContent>
12421246
</ContextMenu>
@@ -1263,7 +1267,7 @@ export default function App() {
12631267
<div className="text-center">
12641268
<FileCode className="w-12 h-12 mx-auto mb-2 opacity-50" />
12651269
<p>
1266-
Select a table or create a new query from the sidebar
1270+
{t("app.empty.hint")}
12671271
</p>
12681272
</div>
12691273
</div>
@@ -1289,7 +1293,7 @@ export default function App() {
12891293
schemaOverview={tab.schemaOverview}
12901294
savedQueryId={tab.savedQueryId}
12911295
initialName={
1292-
tab.title.startsWith("Query (") ? "" : tab.title
1296+
isDefaultQueryTitle(tab.title) ? "" : tab.title
12931297
}
12941298
initialDescription={tab.savedQueryDescription}
12951299
onSaveSuccess={(savedQuery) => {
@@ -1413,21 +1417,20 @@ export default function App() {
14131417
>
14141418
<AlertDialogContent>
14151419
<AlertDialogHeader>
1416-
<AlertDialogTitle>Unsaved changes</AlertDialogTitle>
1420+
<AlertDialogTitle>{t("app.dialog.unsavedTitle")}</AlertDialogTitle>
14171421
<AlertDialogDescription>
1418-
This SQL tab has unsaved changes. Do you want to save before
1419-
closing?
1422+
{t("app.dialog.unsavedDescription")}
14201423
</AlertDialogDescription>
14211424
</AlertDialogHeader>
14221425
<AlertDialogFooter>
14231426
<AlertDialogCancel onClick={handleUnsavedCloseCancel}>
1424-
Cancel
1427+
{t("common.cancel")}
14251428
</AlertDialogCancel>
14261429
<AlertDialogAction onClick={handleUnsavedCloseWithoutSave}>
1427-
Don't Save
1430+
{t("app.dialog.dontSave")}
14281431
</AlertDialogAction>
14291432
<AlertDialogAction onClick={handleUnsavedCloseSave}>
1430-
Save
1433+
{t("common.save")}
14311434
</AlertDialogAction>
14321435
</AlertDialogFooter>
14331436
</AlertDialogContent>
@@ -1437,7 +1440,7 @@ export default function App() {
14371440
onOpenChange={handleCloseSaveDialogOpenChange}
14381441
onSave={handleCloseFlowSave}
14391442
initialName={
1440-
currentCloseTab && !currentCloseTab.title.startsWith("Query (")
1443+
currentCloseTab && !isDefaultQueryTitle(currentCloseTab.title)
14411444
? currentCloseTab.title
14421445
: ""
14431446
}

src/components/business/Sidebar/AISidebar.tsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { AIHistoryPopover } from "./AIHistoryPopover";
2121
import { ChatComposer } from "./chat/ChatComposer";
2222
import { ChatMessageList } from "./chat/ChatMessageList";
2323
import { type SelectedTableRef } from "./chat/TableSelector";
24+
import { useTranslation } from "react-i18next";
2425

2526
interface AISidebarProps {
2627
connectionId?: number;
@@ -62,6 +63,7 @@ export function AISidebar({
6263
database,
6364
schemaOverview,
6465
}: AISidebarProps) {
66+
const { t } = useTranslation();
6567
const [providers, setProviders] = useState<AIProviderConfig[]>([]);
6668
const [selectedProviderId, setSelectedProviderId] = useState<string>("");
6769
const [conversations, setConversations] = useState<AIConversation[]>([]);
@@ -142,7 +144,7 @@ export function AISidebar({
142144
streamQueueRef.current = "";
143145
}
144146
} catch (e) {
145-
toast.error("Failed to load conversation", {
147+
toast.error(t("aiSidebar.errors.loadConversation"), {
146148
description: e instanceof Error ? e.message : String(e),
147149
});
148150
}
@@ -234,19 +236,19 @@ export function AISidebar({
234236
registerListener<AiStartedPayload>("ai/started", (evt) => {
235237
if (evt.payload.requestId !== requestIdRef.current) return;
236238
setStreamStatus(
237-
`Request sent (${evt.payload.model}), waiting for first token...`,
239+
t("aiSidebar.status.requestSent", { model: evt.payload.model }),
238240
);
239241
});
240242

241243
registerListener<AiChunkPayload>("ai/chunk", (evt) => {
242244
if (evt.payload.requestId !== requestIdRef.current) return;
243-
setStreamStatus("Receiving response...");
245+
setStreamStatus(t("aiSidebar.status.receiving"));
244246
streamQueueRef.current += evt.payload.chunk;
245247
});
246248

247249
registerListener<AiDonePayload>("ai/done", (evt) => {
248250
if (evt.payload.requestId !== requestIdRef.current) return;
249-
setStreamStatus("Finalizing response...");
251+
setStreamStatus(t("aiSidebar.status.finalizing"));
250252
setActiveConversationId(evt.payload.conversationId);
251253
void reloadConversationsRef.current();
252254
void loadConversationRef.current(evt.payload.conversationId);
@@ -278,7 +280,7 @@ export function AISidebar({
278280
streamFinalizeTimerRef.current = null;
279281
}
280282
errorNotifiedRef.current = true;
281-
toast.error("AI request failed", {
283+
toast.error(t("aiSidebar.errors.requestFailed"), {
282284
id: "ai-request-error",
283285
description: evt.payload.error,
284286
});
@@ -294,7 +296,7 @@ export function AISidebar({
294296
streamFinalizeTimerRef.current = null;
295297
}
296298
};
297-
}, []);
299+
}, [t]);
298300

299301
useEffect(() => {
300302
if (!isLoading) {
@@ -328,7 +330,7 @@ export function AISidebar({
328330
if (!text || isLoading) return;
329331

330332
if (!selectedProviderId) {
331-
toast.error("Please configure and select an AI provider in Settings.");
333+
toast.error(t("aiSidebar.errors.providerMissing"));
332334
return;
333335
}
334336

@@ -348,7 +350,7 @@ export function AISidebar({
348350
setInput("");
349351
setIsLoading(true);
350352
setStreamingContent("");
351-
setStreamStatus("Sending request...");
353+
setStreamStatus(t("aiSidebar.status.sending"));
352354
streamQueueRef.current = "";
353355
if (streamFinalizeTimerRef.current) {
354356
clearTimeout(streamFinalizeTimerRef.current);
@@ -404,7 +406,7 @@ export function AISidebar({
404406
streamFinalizeTimerRef.current = null;
405407
}
406408
if (!errorNotifiedRef.current) {
407-
toast.error("Failed to send AI message", {
409+
toast.error(t("aiSidebar.errors.sendFailed"), {
408410
id: "ai-request-error",
409411
description: e instanceof Error ? e.message : String(e),
410412
});
@@ -421,7 +423,7 @@ export function AISidebar({
421423
}
422424
await reloadConversations();
423425
} catch (e) {
424-
toast.error("Failed to delete conversation", {
426+
toast.error(t("aiSidebar.errors.deleteConversation"), {
425427
description: e instanceof Error ? e.message : String(e),
426428
});
427429
}
@@ -449,7 +451,7 @@ export function AISidebar({
449451
<div className="flex min-w-0 items-center gap-2">
450452
<Sparkles className="h-4 w-4 text-primary" />
451453
<h2 className="truncate text-sm font-semibold text-foreground">
452-
AI Assistant
454+
{t("aiSidebar.title")}
453455
</h2>
454456
</div>
455457
<div className="flex shrink-0 items-center gap-1">
@@ -469,8 +471,8 @@ export function AISidebar({
469471
variant="ghost"
470472
size="icon"
471473
className="h-7 w-7 rounded-md"
472-
title="New chat"
473-
aria-label="Start new chat"
474+
title={t("aiSidebar.newChat")}
475+
aria-label={t("aiSidebar.startNewChat")}
474476
onClick={handleNewConversation}
475477
disabled={isLoading}
476478
>

0 commit comments

Comments
 (0)