Skip to content

Commit b47fbcf

Browse files
Add internationalization support with English and French translations
- Implemented i18next for language management in the application. - Created language switch functionality that saves user preference in local storage. - Added English and French translation files for various application texts.
1 parent dd841b5 commit b47fbcf

19 files changed

Lines changed: 1022 additions & 266 deletions

frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"cmdk": "^1.1.1",
4848
"date-fns": "^3.6.0",
4949
"embla-carousel-react": "^8.6.0",
50+
"i18next": "^26.0.4",
5051
"input-otp": "^1.4.2",
5152
"lucide-react": "^0.462.0",
5253
"next-themes": "^0.3.0",
@@ -55,6 +56,7 @@
5556
"react-day-picker": "^8.10.1",
5657
"react-dom": "^18.3.1",
5758
"react-hook-form": "^7.61.1",
59+
"react-i18next": "^17.0.2",
5860
"react-resizable-panels": "^2.1.9",
5961
"react-router-dom": "^6.30.1",
6062
"recharts": "^2.15.4",

frontend/src/components/AppSidebar.tsx

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useState, useRef } from "react";
22
import {
33
Star, FolderOpen, Plus, Download, Upload, Sun, Moon, Menu, X, LogOut, Loader2, BarChart3,
44
} from "lucide-react";
5+
import { useTranslation } from "react-i18next";
56
import { Group, CommandStats } from "@/hooks/useCommandVault";
67
import { Skeleton } from "@/components/ui/skeleton";
78
import {
@@ -37,6 +38,7 @@ export default function AppSidebar({
3738
}: SidebarProps) {
3839
const [mobileOpen, setMobileOpen] = useState(false);
3940
const fileRef = useRef<HTMLInputElement>(null);
41+
const { t, i18n } = useTranslation();
4042

4143
const commandCount = (groupId: number) => stats.byGroup[groupId] ?? 0;
4244
const favCount = stats.favorites ?? 0;
@@ -72,7 +74,7 @@ export default function AppSidebar({
7274
}`}
7375
>
7476
<BarChart3 className="w-4 h-4" />
75-
<span>Dashboard</span>
77+
<span>{t("sidebar.dashboard")}</span>
7678
</button>
7779

7880
<button
@@ -82,7 +84,7 @@ export default function AppSidebar({
8284
}`}
8385
>
8486
<FolderOpen className="w-4 h-4" />
85-
<span>All Commands</span>
87+
<span>{t("sidebar.allCommands")}</span>
8688
<span className="ml-auto text-xs text-sidebar-muted">
8789
{loading ? <Skeleton className="h-3 w-6" /> : (stats.total ?? 0)}
8890
</span>
@@ -95,7 +97,7 @@ export default function AppSidebar({
9597
}`}
9698
>
9799
<Star className="w-4 h-4" />
98-
<span>Favorites</span>
100+
<span>{t("sidebar.favorites")}</span>
99101
<span className="ml-auto text-xs text-sidebar-muted">
100102
{loading ? <Skeleton className="h-3 w-6" /> : favCount}
101103
</span>
@@ -104,7 +106,7 @@ export default function AppSidebar({
104106
{/* Groups */}
105107
<div className="pt-4">
106108
<div className="flex items-center justify-between px-3 mb-2">
107-
<span className="text-[11px] font-semibold uppercase tracking-wider text-sidebar-muted">Groups</span>
109+
<span className="text-[11px] font-semibold uppercase tracking-wider text-sidebar-muted">{t("sidebar.groups")}</span>
108110
<button onClick={onNewGroup} disabled={loading} className="p-0.5 rounded hover:bg-surface-hover text-sidebar-muted transition-colors disabled:opacity-50">
109111
<Plus className="w-3.5 h-3.5" />
110112
</button>
@@ -148,7 +150,7 @@ export default function AppSidebar({
148150
<DropdownMenuTrigger asChild>
149151
<button
150152
type="button"
151-
title="Import/Export"
153+
title={t("sidebar.importExport")}
152154
className="p-2 rounded-md hover:bg-surface-hover text-sidebar-muted transition-colors"
153155
>
154156
{importing ? <Loader2 className="w-4 h-4 animate-spin" /> : <Download className="w-4 h-4" />}
@@ -161,29 +163,42 @@ export default function AppSidebar({
161163
className="gap-2"
162164
>
163165
<Upload className="w-4 h-4" />
164-
Importer
166+
{t("sidebar.import")}
165167
</DropdownMenuItem>
166168
<DropdownMenuItem
167169
onSelect={onImportHistory}
168170
disabled={loading || importing}
169171
className="gap-2"
170172
>
171173
<Upload className="w-4 h-4" />
172-
Importer historique
174+
{t("sidebar.importHistory")}
173175
</DropdownMenuItem>
174176
<DropdownMenuItem onSelect={onExport} className="gap-2">
175177
<Download className="w-4 h-4" />
176-
Exporter
178+
{t("sidebar.export")}
177179
</DropdownMenuItem>
178180
</DropdownMenuContent>
179181
</DropdownMenu>
182+
<button
183+
type="button"
184+
onClick={() => {
185+
const next = i18n.language === "fr" ? "en" : "fr";
186+
void i18n.changeLanguage(next);
187+
}}
188+
title={t("language.switch")}
189+
className="p-2 rounded-md hover:bg-surface-hover text-sidebar-muted transition-colors"
190+
>
191+
<span className="text-[11px] font-semibold tracking-wide">
192+
{(i18n.language === "fr" ? "FR" : "EN")}
193+
</span>
194+
</button>
180195
<div className="flex-1" />
181196
<button
182197
onClick={() => {
183198
onLogout();
184199
setMobileOpen(false);
185200
}}
186-
title="Logout"
201+
title={t("sidebar.logout")}
187202
className="p-2 rounded-md hover:bg-surface-hover text-sidebar-muted transition-colors"
188203
>
189204
<LogOut className="w-4 h-4" />

frontend/src/components/BulkMoveModal.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from "react";
22
import { Loader2 } from "lucide-react";
33
import { Group } from "@/hooks/useCommandVault";
44
import { Button } from "@/components/ui/button";
5+
import { useTranslation } from "react-i18next";
56
import {
67
Dialog,
78
DialogContent,
@@ -18,6 +19,7 @@ export default function BulkMoveModal(props: {
1819
onClose: () => void;
1920
onConfirm: (groupId: number) => Promise<void>;
2021
}) {
22+
const { t } = useTranslation();
2123
const { open, groups, count, onClose, onConfirm } = props;
2224

2325
const [groupId, setGroupId] = useState<number | null>(null);
@@ -37,22 +39,22 @@ export default function BulkMoveModal(props: {
3739
<Dialog open={open} onOpenChange={(v) => { if (!v) onClose(); }}>
3840
<DialogContent className="max-w-md">
3941
<DialogHeader>
40-
<DialogTitle>Move commands</DialogTitle>
42+
<DialogTitle>{t("bulkMove.title")}</DialogTitle>
4143
<DialogDescription>
42-
Move {count} selected command{count === 1 ? "" : "s"} to a group.
44+
{t("bulkMove.description", { count })}
4345
</DialogDescription>
4446
</DialogHeader>
4547

4648
<div className="space-y-2">
47-
<label className="text-xs font-medium text-muted-foreground">Destination group</label>
49+
<label className="text-xs font-medium text-muted-foreground">{t("bulkMove.destinationGroup")}</label>
4850
<select
4951
value={groupId ?? ""}
5052
onChange={(e) => setGroupId(e.target.value ? Number(e.target.value) : null)}
5153
className="w-full px-3 py-2 text-sm rounded-md border border-input bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-accent-blue"
5254
disabled={moving || sortedGroups.length === 0}
5355
>
5456
{sortedGroups.length === 0 ? (
55-
<option value="">No groups</option>
57+
<option value="">{t("bulkMove.noGroups")}</option>
5658
) : (
5759
sortedGroups.map((g) => (
5860
<option key={g.id} value={g.id}>
@@ -65,7 +67,7 @@ export default function BulkMoveModal(props: {
6567

6668
<DialogFooter>
6769
<Button type="button" variant="ghost" onClick={onClose} disabled={moving}>
68-
Cancel
70+
{t("common.cancel")}
6971
</Button>
7072
<Button
7173
type="button"
@@ -86,7 +88,7 @@ export default function BulkMoveModal(props: {
8688
disabled={moving || groupId == null}
8789
>
8890
{moving && <Loader2 className="w-4 h-4 animate-spin" />}
89-
Move
91+
{t("common.move")}
9092
</Button>
9193
</DialogFooter>
9294
</DialogContent>

frontend/src/components/CommandCard.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react";
22
import { Star, Copy, Check, Pencil, Trash2, ChevronDown, ChevronUp, Eye } from "lucide-react";
33
import { Command } from "@/hooks/useCommandVault";
44
import { Checkbox } from "@/components/ui/checkbox";
5+
import { useTranslation } from "react-i18next";
56

67
function escapeRegex(s: string) {
78
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -109,6 +110,7 @@ export default function CommandCard({
109110
selected,
110111
onSelectChange,
111112
}: Props) {
113+
const { t } = useTranslation();
112114
const [expanded, setExpanded] = useState(false);
113115
const [justCopied, setJustCopied] = useState(false);
114116
const copiedTimeoutRef = useRef<number | null>(null);
@@ -129,7 +131,7 @@ export default function CommandCard({
129131
<Checkbox
130132
checked={!!selected}
131133
onCheckedChange={(v) => onSelectChange?.(Boolean(v))}
132-
aria-label={selected ? "Deselect command" : "Select command"}
134+
aria-label={selected ? t("commandCard.deselect") : t("commandCard.select")}
133135
className="mt-0.5"
134136
/>
135137
)}
@@ -152,7 +154,7 @@ export default function CommandCard({
152154
className="flex items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground mt-1 transition-colors"
153155
>
154156
{expanded ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
155-
{expanded ? "Show less" : "Show more"}
157+
{expanded ? t("commandCard.showLess") : t("commandCard.showMore")}
156158
</button>
157159
)}
158160
</div>
@@ -180,15 +182,15 @@ export default function CommandCard({
180182
{/* Footer */}
181183
<div className="flex items-center justify-between mt-3 pt-3 border-t border-border">
182184
<span className="text-[11px] text-muted-foreground">
183-
Copied {cmd.copyCount} time{cmd.copyCount !== 1 ? "s" : ""}
185+
{t("commandCard.copiedTimes", { count: cmd.copyCount })}
184186
</span>
185187
<div className="flex items-center gap-1">
186188
{onView && (
187189
<button
188190
onClick={() => onView(cmd)}
189191
className="flex items-center gap-1 px-2 py-1 text-xs rounded-md hover:bg-surface-hover text-muted-foreground hover:text-foreground transition-colors"
190192
>
191-
<Eye className="w-3.5 h-3.5" /> View
193+
<Eye className="w-3.5 h-3.5" /> {t("commandCard.view")}
192194
</button>
193195
)}
194196
<button
@@ -203,13 +205,13 @@ export default function CommandCard({
203205
}}
204206
className="flex items-center gap-1 px-2 py-1 text-xs rounded-md hover:bg-surface-hover text-muted-foreground hover:text-foreground transition-colors transition-transform active:scale-95"
205207
>
206-
{justCopied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />} {justCopied ? "Copied" : "Copy"}
208+
{justCopied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />} {justCopied ? t("commandCard.copied") : t("commandCard.copy")}
207209
</button>
208210
<button onClick={() => onEdit(cmd)} className="flex items-center gap-1 px-2 py-1 text-xs rounded-md hover:bg-surface-hover text-muted-foreground hover:text-foreground transition-colors">
209-
<Pencil className="w-3.5 h-3.5" /> Edit
211+
<Pencil className="w-3.5 h-3.5" /> {t("commandCard.edit")}
210212
</button>
211213
<button onClick={() => onDelete(cmd)} className="flex items-center gap-1 px-2 py-1 text-xs rounded-md hover:bg-destructive/10 text-muted-foreground hover:text-destructive transition-colors">
212-
<Trash2 className="w-3.5 h-3.5" /> Delete
214+
<Trash2 className="w-3.5 h-3.5" /> {t("commandCard.delete")}
213215
</button>
214216
</div>
215217
</div>

0 commit comments

Comments
 (0)