Skip to content

Commit ca993f0

Browse files
committed
feat: add EN/ZH bilingual language toggle
2 parents dcb6209 + 94dbd07 commit ca993f0

8 files changed

Lines changed: 341 additions & 96 deletions

File tree

src/App.tsx

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
66
import { BrowserRouter, Routes, Route } from "react-router-dom";
77
import Index from "./pages/Index";
88
import NotFound from "./pages/NotFound";
9+
import { LanguageProvider } from "./i18n/LanguageContext";
910

1011
const Resume = lazy(() => import("./pages/Resume"));
1112
const Projects = lazy(() => import("./pages/Projects"));
@@ -55,22 +56,24 @@ const queryClient = new QueryClient();
5556

5657
const App = () => (
5758
<ErrorBoundary>
58-
<QueryClientProvider client={queryClient}>
59-
<TooltipProvider>
60-
<Toaster />
61-
<Sonner />
62-
<BrowserRouter>
63-
<Suspense fallback={null}>
64-
<Routes>
65-
<Route path="/" element={<Index />} />
66-
<Route path="/resume" element={<Resume />} />
67-
<Route path="/projects" element={<Projects />} />
68-
<Route path="*" element={<NotFound />} />
69-
</Routes>
70-
</Suspense>
71-
</BrowserRouter>
72-
</TooltipProvider>
73-
</QueryClientProvider>
59+
<LanguageProvider>
60+
<QueryClientProvider client={queryClient}>
61+
<TooltipProvider>
62+
<Toaster />
63+
<Sonner />
64+
<BrowserRouter>
65+
<Suspense fallback={null}>
66+
<Routes>
67+
<Route path="/" element={<Index />} />
68+
<Route path="/resume" element={<Resume />} />
69+
<Route path="/projects" element={<Projects />} />
70+
<Route path="*" element={<NotFound />} />
71+
</Routes>
72+
</Suspense>
73+
</BrowserRouter>
74+
</TooltipProvider>
75+
</QueryClientProvider>
76+
</LanguageProvider>
7477
</ErrorBoundary>
7578
);
7679

src/components/ChatSection.tsx

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,7 @@ import MessageBubble from "./MessageBubble";
44
import { sendMessageStream, checkHealth } from "@/utils/api";
55
import { toast } from "sonner";
66
import type { Message } from "@/types/chat";
7-
8-
const PREDEFINED_QUESTIONS = [
9-
"What is your background?",
10-
"Tell me about your projects",
11-
"What are your technical skills?",
12-
"Where did you study?",
13-
"What's your work experience?",
14-
"Any patents or publications?",
15-
];
7+
import { useLang } from "@/i18n/LanguageContext";
168

179
// ── Neural network thinking indicator ─────────────────────────────────────
1810
const NN_FRAMES = [
@@ -53,11 +45,14 @@ const ThinkingIndicator = () => {
5345
};
5446

5547
// ── ChatSection ────────────────────────────────────────────────────────────
48+
const INITIAL_MSG_ID = "1";
49+
5650
const ChatSection = () => {
51+
const { t, lang } = useLang();
5752
const [messages, setMessages] = useState<Message[]>([
5853
{
59-
id: "1",
60-
text: "Hi! I'm representing Yifei. Ask me anything about my background, skills, projects, or experience!",
54+
id: INITIAL_MSG_ID,
55+
text: t.chat.greeting,
6156
isUser: false,
6257
timestamp: new Date(),
6358
},
@@ -74,6 +69,16 @@ const ChatSection = () => {
7469
const historyIndexRef = useRef(-1);
7570
const pendingInputRef = useRef("");
7671

72+
// Update greeting when language changes (if no user messages yet)
73+
useEffect(() => {
74+
setMessages((prev) => {
75+
const hasUserMsg = prev.some((m) => m.isUser);
76+
if (hasUserMsg) return prev;
77+
return [{ id: INITIAL_MSG_ID, text: t.chat.greeting, isUser: false, timestamp: prev[0].timestamp }];
78+
});
79+
// eslint-disable-next-line react-hooks/exhaustive-deps
80+
}, [lang]);
81+
7782
const isNearBottom = (): boolean => {
7883
const el = scrollContainerRef.current;
7984
if (!el) return true;
@@ -99,26 +104,27 @@ const ChatSection = () => {
99104
const isOnline = await checkHealth();
100105
setIsServerOnline(isOnline);
101106
if (!isOnline) {
102-
toast.error("AI server is currently offline. Please try again later.");
107+
toast.error(t.chat.offlineToast);
103108
}
104109
};
105110
checkServerStatus();
106111
const interval = setInterval(checkServerStatus, 30000);
107112
return () => clearInterval(interval);
113+
// eslint-disable-next-line react-hooks/exhaustive-deps
108114
}, []);
109115

110116
const handleExport = () => {
111117
if (messages.length <= 1) {
112-
toast.info("Nothing to export yet — start a conversation first!");
118+
toast.info(t.chat.exportNothing);
113119
return;
114120
}
115121
const lines = messages.map((m) => {
116122
const time = m.timestamp.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
117123
return m.isUser
118-
? `**[${time}] You:** ${m.text}`
119-
: `**[${time}] AI:** ${m.text}`;
124+
? `**[${time}] ${t.chat.youLabel}:** ${m.text}`
125+
: `**[${time}] ${t.chat.aiLabel}:** ${m.text}`;
120126
});
121-
const content = `# Chat Export\n\n_Exported on ${new Date().toLocaleString()}_\n\n---\n\n${lines.join("\n\n---\n\n")}`;
127+
const content = `${t.chat.exportHeader}\n\n_${t.chat.exportedOn} ${new Date().toLocaleString()}_\n\n---\n\n${lines.join("\n\n---\n\n")}`;
122128
const blob = new Blob([content], { type: "text/markdown" });
123129
const url = URL.createObjectURL(blob);
124130
const a = document.createElement("a");
@@ -131,7 +137,7 @@ const ChatSection = () => {
131137
const handleSendMessage = async (messageText: string) => {
132138
if (!messageText.trim()) return;
133139
if (!isServerOnline) {
134-
toast.error("AI server is currently offline. Please try again later.");
140+
toast.error(t.chat.offlineToast);
135141
return;
136142
}
137143

@@ -183,15 +189,15 @@ const ChatSection = () => {
183189
console.error("Stream error:", error);
184190
setIsLoading(false);
185191
setStreamingMessageId(null);
186-
toast.error("Failed to get response. Please try again later.");
192+
toast.error(t.chat.streamError);
187193
setMessages((prev) => {
188194
const hasAiMessage = prev.some((m) => m.id === aiId);
189-
if (hasAiMessage) return prev; // partial response already shown, keep it
195+
if (hasAiMessage) return prev;
190196
return [
191197
...prev,
192198
{
193199
id: aiId,
194-
text: "Sorry, I'm having trouble connecting right now. Please try again later.",
200+
text: t.chat.fallbackError,
195201
isUser: false,
196202
timestamp: new Date(),
197203
},
@@ -233,7 +239,7 @@ const ChatSection = () => {
233239
<section className="px-4 pb-6" style={{ position: "relative", zIndex: 1 }}>
234240
<div className="max-w-4xl mx-auto">
235241
<div style={{ border: "1px solid var(--term-border)", backgroundColor: "var(--term-bg)" }}>
236-
{/* Terminal title bar — enhanced AI style */}
242+
{/* Terminal title bar */}
237243
<div
238244
style={{
239245
borderBottom: "1px solid var(--term-border)",
@@ -248,16 +254,16 @@ const ChatSection = () => {
248254
>
249255
<span style={{ color: "var(--term-dim)", fontSize: "12px" }}>
250256
🧠{" "}
251-
<span style={{ color: "var(--term-text)" }}>AI Assistant v2.0</span>
257+
<span style={{ color: "var(--term-text)" }}>{t.chat.titleBar}</span>
252258
<span style={{ color: "var(--term-dim)" }}>
253-
{" "}| model:{" "}
259+
{" "}| {t.chat.modelLabel}:{" "}
254260
</span>
255261
<span style={{ color: "var(--term-cyan)" }}>rag-powered</span>
256262
<span style={{ color: "var(--term-dim)" }}>
257-
{" "}| status:{" "}
263+
{" "}| {t.chat.statusLabel}:{" "}
258264
</span>
259265
<span style={{ color: isServerOnline ? "var(--term-green)" : "var(--term-red)" }}>
260-
{isServerOnline ? "online" : "offline"}
266+
{isServerOnline ? t.chat.statusOnline : t.chat.statusOffline}
261267
</span>
262268
</span>
263269
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
@@ -281,12 +287,12 @@ const ChatSection = () => {
281287
animation: isServerOnline ? "neuralPulse 2s ease-in-out infinite" : "none",
282288
}}
283289
/>
284-
{isServerOnline ? "server:online" : "server:offline"}
290+
{isServerOnline ? t.chat.serverOnline : t.chat.serverOffline}
285291
</span>
286292
{/* Export button */}
287293
<button
288294
onClick={handleExport}
289-
title="Export chat as Markdown"
295+
title={t.chat.exportTooltip}
290296
style={{
291297
background: "transparent",
292298
border: "1px solid var(--term-border)",
@@ -303,7 +309,7 @@ const ChatSection = () => {
303309
className="chat-quick-btn"
304310
>
305311
<Download size={11} />
306-
export
312+
{t.chat.exportBtn}
307313
</button>
308314
</div>
309315
</div>
@@ -336,7 +342,7 @@ const ChatSection = () => {
336342
}}
337343
>
338344
<AlertCircle size={13} />
339-
error: AI server is offline. Responses unavailable.
345+
{t.chat.offlineError}
340346
</div>
341347
)}
342348

@@ -377,10 +383,10 @@ const ChatSection = () => {
377383
{/* Predefined questions */}
378384
<div style={{ marginBottom: "10px" }}>
379385
<div style={{ fontSize: "11px", color: "var(--term-dim)", marginBottom: "6px" }}>
380-
# quick commands:
386+
{t.chat.quickCommands}
381387
</div>
382388
<div className="grid grid-cols-2 sm:flex sm:flex-wrap gap-1.5">
383-
{PREDEFINED_QUESTIONS.map((question, index) => (
389+
{t.chat.questions.map((question, index) => (
384390
<button
385391
key={index}
386392
onClick={() => handleSendMessage(question)}
@@ -422,7 +428,7 @@ const ChatSection = () => {
422428
value={inputValue}
423429
onChange={(e) => setInputValue(e.target.value)}
424430
onKeyDown={handleKeyPress}
425-
placeholder="type your question..."
431+
placeholder={t.chat.placeholder}
426432
disabled={isLoading || !isServerOnline}
427433
style={{
428434
flex: 1,
@@ -455,7 +461,7 @@ const ChatSection = () => {
455461
flexShrink: 0,
456462
}}
457463
>
458-
[enter]
464+
{t.chat.enterBtn}
459465
</button>
460466
</div>
461467
</div>

src/components/GetInTouchSection.tsx

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,28 @@
11
import { useState } from "react";
22
import { toast } from "@/components/ui/use-toast";
33
import { personalInfo } from "@/data/personalInfo";
4+
import { useLang } from "@/i18n/LanguageContext";
45

56
const GetInTouchSection = () => {
7+
const { t } = useLang();
68
const [form, setForm] = useState({ name: "", company: "", email: "", message: "" });
79
const [submitting, setSubmitting] = useState(false);
810

911
const validate = () => {
1012
if (!form.name.trim()) {
11-
toast({ title: "error: name is required" });
13+
toast({ title: t.contact.errName });
1214
return false;
1315
}
1416
if (!form.email.trim()) {
15-
toast({ title: "error: email is required" });
17+
toast({ title: t.contact.errEmail });
1618
return false;
1719
}
1820
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) {
19-
toast({ title: "error: invalid email format" });
21+
toast({ title: t.contact.errEmailFmt });
2022
return false;
2123
}
2224
if (!form.message.trim()) {
23-
toast({ title: "error: message is required" });
25+
toast({ title: t.contact.errMessage });
2426
return false;
2527
}
2628
return true;
@@ -82,7 +84,7 @@ const GetInTouchSection = () => {
8284
color: "var(--term-dim)",
8385
}}
8486
>
85-
bash — ~/contact
87+
{t.contact.titleBar}
8688
</div>
8789

8890
<div style={{ padding: "20px 24px" }}>
@@ -97,7 +99,7 @@ const GetInTouchSection = () => {
9799
contact --send-message
98100
</span>
99101
</div>
100-
<div style={{ color: "var(--term-dim)", fontSize: "11px", marginTop: "6px", marginLeft: "0" }}>
102+
<div style={{ color: "var(--term-dim)", fontSize: "11px", marginTop: "6px" }}>
101103
# opens default email client with form pre-filled
102104
</div>
103105
</div>
@@ -112,11 +114,11 @@ const GetInTouchSection = () => {
112114
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-3">
113115
<div>
114116
<label htmlFor="contact-name" style={labelStyle}>
115-
name <span style={{ color: "var(--term-red)" }}>*</span>
117+
{t.contact.nameLabel} <span style={{ color: "var(--term-red)" }}>*</span>
116118
</label>
117119
<input
118120
id="contact-name"
119-
placeholder="your name"
121+
placeholder={t.contact.namePlaceholder}
120122
value={form.name}
121123
onChange={(e) => setForm({ ...form, name: e.target.value })}
122124
style={inputStyle}
@@ -126,12 +128,12 @@ const GetInTouchSection = () => {
126128
</div>
127129
<div>
128130
<label htmlFor="contact-company" style={labelStyle}>
129-
company{" "}
130-
<span style={{ color: "var(--term-dim)", fontSize: "10px" }}>(optional)</span>
131+
{t.contact.companyLabel}{" "}
132+
<span style={{ color: "var(--term-dim)", fontSize: "10px" }}>{t.contact.optional}</span>
131133
</label>
132134
<input
133135
id="contact-company"
134-
placeholder="company name"
136+
placeholder={t.contact.companyPlaceholder}
135137
value={form.company}
136138
onChange={(e) => setForm({ ...form, company: e.target.value })}
137139
style={inputStyle}
@@ -143,12 +145,12 @@ const GetInTouchSection = () => {
143145

144146
<div style={{ marginBottom: "12px" }}>
145147
<label htmlFor="contact-email" style={labelStyle}>
146-
email <span style={{ color: "var(--term-red)" }}>*</span>
148+
{t.contact.emailLabel} <span style={{ color: "var(--term-red)" }}>*</span>
147149
</label>
148150
<input
149151
id="contact-email"
150152
type="email"
151-
placeholder="you@example.com"
153+
placeholder={t.contact.emailPlaceholder}
152154
value={form.email}
153155
onChange={(e) => setForm({ ...form, email: e.target.value })}
154156
style={inputStyle}
@@ -159,11 +161,11 @@ const GetInTouchSection = () => {
159161

160162
<div style={{ marginBottom: "16px" }}>
161163
<label htmlFor="contact-message" style={labelStyle}>
162-
message <span style={{ color: "var(--term-red)" }}>*</span>
164+
{t.contact.messageLabel} <span style={{ color: "var(--term-red)" }}>*</span>
163165
</label>
164166
<textarea
165167
id="contact-message"
166-
placeholder="your message..."
168+
placeholder={t.contact.messagePlaceholder}
167169
rows={5}
168170
value={form.message}
169171
onChange={(e) => setForm({ ...form, message: e.target.value })}
@@ -198,18 +200,18 @@ const GetInTouchSection = () => {
198200
e.currentTarget.style.color = "var(--term-green)";
199201
}}
200202
>
201-
{submitting ? "opening email client..." : "[send-message]"}
203+
{submitting ? t.contact.sending : t.contact.sendBtn}
202204
</button>
203205
</form>
204206

205207
{/* Contact info */}
206208
<div>
207209
<div style={{ marginBottom: "16px" }}>
208210
<div style={{ color: "var(--term-green)", fontWeight: 600, marginBottom: "4px", fontSize: "14px" }}>
209-
let's connect
211+
{t.contact.connectTitle}
210212
</div>
211213
<div style={{ color: "var(--term-dim)", fontSize: "12px", lineHeight: "1.6" }}>
212-
Happy to connect about opportunities, collaboration, or any interesting ideas.
214+
{t.contact.connectDesc}
213215
</div>
214216
</div>
215217

0 commit comments

Comments
 (0)