feat(i18n): implement internationalization support for en and zh

Add i18n infrastructure with translation files and utility functions
Update components to use translations and language switching
Create localized pages for en and zh languages
Add language detection and path localization utilities
This commit is contained in:
joyzhao
2025-06-15 09:08:41 +08:00
parent 4ab809ed94
commit ee0fbcceb2
9 changed files with 470 additions and 450 deletions

View File

@@ -1,7 +1,13 @@
import { personalInfo } from "@/lib/data";
import { useTranslations, type Lang } from "@/i18n/utils";
import { motion } from "framer-motion";
export default function Footer() {
interface FooterProps {
lang: Lang;
}
export default function Footer({ lang }: FooterProps) {
const t = useTranslations(lang);
return (
<footer className="border-t border-purple-500/10 py-6 bg-gradient-to-b from-background to-muted/20 backdrop-blur-sm">
<div className="container max-w-4xl mx-auto px-6 md:px-4">
@@ -16,8 +22,7 @@ export default function Footer() {
className="text-sm text-muted-foreground text-center md:text-left"
whileHover={{ scale: 1.01 }}
>
&copy; {new Date().getFullYear()} {personalInfo.name}. All rights
reserved.
&copy; {new Date().getFullYear()} {personalInfo.name}. {t('footer.rights')}
</motion.p>
<motion.p
className="text-sm text-muted-foreground mt-2 md:mt-0 text-center md:text-left"

View File

@@ -1,11 +1,17 @@
import ThemeToggle from "./ui/theme-toggle";
import LanguageSwitcher from "./LanguageSwitcher"; // Added import for LanguageSwitcher
import LanguageSwitcher from "./LanguageSwitcher";
import { useTranslations, getLocalizedPath, type Lang } from "@/i18n/utils";
import { personalInfo } from "@/lib/data";
import { useState } from "react";
import { Menu, X } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
export default function GlassHeader() {
interface GlassHeaderProps {
lang: Lang;
}
export default function GlassHeader({ lang }: GlassHeaderProps) {
const t = useTranslations(lang);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const toggleMenu = () => setIsMenuOpen(!isMenuOpen);
@@ -15,7 +21,7 @@ export default function GlassHeader() {
<div className="container max-w-4xl mx-auto p-4 flex justify-between items-center">
<motion.a
className="flex items-center text-lg font-medium"
href="/"
href={getLocalizedPath('/', lang)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
@@ -24,23 +30,25 @@ export default function GlassHeader() {
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center space-x-6 text-sm font-medium">
{["experience", "skills", "projects", "awards", "education"].map(
{[
{ key: 'nav.experience', icon: '💼 ', sectionId: 'experience' },
{ key: 'nav.skills', icon: '🛠️ ', sectionId: 'skills' },
{ key: 'nav.projects', icon: '🚀 ', sectionId: 'projects' },
{ key: 'nav.awards', icon: '🏆 ', sectionId: 'awards' },
{ key: 'nav.education', icon: '🎓 ', sectionId: 'education' },
].map(
(item, index) => (
<motion.a
key={item}
href={`#${item}`}
key={item.key} // Changed from item to item.key
href={`#${item.sectionId}`}
className="transition-colors hover:text-foreground/80 text-foreground/60"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, delay: index * 0.1 }}
whileHover={{ y: -2 }}
>
{item === "experience" && "💼 "}
{item === "skills" && "🛠️ "}
{item === "projects" && "🚀 "}
{item === "awards" && "🏆 "}
{item === "education" && "🎓 "}
{item.charAt(0).toUpperCase() + item.slice(1)}
{item.icon}
{t(item.key as any) /* Type assertion needed if UiKeys is strict */}
</motion.a>
)
)}
@@ -48,7 +56,7 @@ export default function GlassHeader() {
<div className="flex items-center space-x-2">
{/* Language Switcher added here */}
<LanguageSwitcher />
<LanguageSwitcher lang={lang} />
<ThemeToggle />
{/* Mobile Menu Button */}
@@ -74,23 +82,25 @@ export default function GlassHeader() {
transition={{ duration: 0.3 }}
>
<nav className="flex flex-col space-y-4 text-sm font-medium">
{["experience", "skills", "projects", "awards", "education"].map(
{[
{ key: 'nav.experience', icon: '💼 ', sectionId: 'experience' },
{ key: 'nav.skills', icon: '🛠️ ', sectionId: 'skills' },
{ key: 'nav.projects', icon: '🚀 ', sectionId: 'projects' },
{ key: 'nav.awards', icon: '🏆 ', sectionId: 'awards' },
{ key: 'nav.education', icon: '🎓 ', sectionId: 'education' },
].map(
(item, index) => (
<motion.a
key={item}
href={`#${item}`}
key={item.key} // Changed from item to item.key
href={`#${item.sectionId}`}
className="transition-colors hover:text-foreground/80 text-foreground/60 py-2"
onClick={toggleMenu}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.2, delay: index * 0.1 }}
>
{item === "experience" && "💼 "}
{item === "skills" && "🛠️ "}
{item === "projects" && "🚀 "}
{item === "awards" && "🏆 "}
{item === "education" && "🎓 "}
{item.charAt(0).toUpperCase() + item.slice(1)}
{item.icon}
{t(item.key as any) /* Type assertion needed if UiKeys is strict */}
</motion.a>
)
)}

View File

@@ -1,24 +1,43 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { getLangFromUrl, getLocalizedPath, type Lang } from "@/i18n/utils";
import { languages as i18nLanguages, defaultLang } from "@/i18n/ui";
import { Languages, Check } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
const languages = [
{ code: "en", name: "English", icon: "🇬🇧" }, // Added icon for English
{ code: "zh", name: "中文", icon: "🇨🇳" }, // Added icon for Chinese
];
const availableLanguages = Object.entries(i18nLanguages).map(([code, name]) => ({
code: code as Lang,
name,
// You can add icons here if you have a mapping or a more complex structure in ui.ts
icon: code === 'en' ? '🇬🇧' : code === 'zh' ? '🇨🇳' : '🌐'
}));
export default function LanguageSwitcher() {
interface LanguageSwitcherProps {
lang: Lang;
}
export default function LanguageSwitcher({ lang: initialLang }: LanguageSwitcherProps) {
const [isOpen, setIsOpen] = useState(false);
// TODO: Implement actual language switching logic, e.g., using a context or a library
const [selectedLanguage, setSelectedLanguage] = useState(languages[0]);
const [selectedLanguage, setSelectedLanguage] = useState(() => {
return availableLanguages.find(l => l.code === initialLang) || availableLanguages.find(l => l.code === defaultLang) || availableLanguages[0];
});
useEffect(() => {
const currentLangObject = availableLanguages.find(l => l.code === initialLang);
if (currentLangObject && currentLangObject.code !== selectedLanguage.code) {
setSelectedLanguage(currentLangObject);
}
}, [initialLang, selectedLanguage.code]);
const toggleOpen = () => setIsOpen(!isOpen);
const handleSelectLanguage = (lang: typeof languages[0]) => {
const handleSelectLanguage = (lang: typeof availableLanguages[0]) => {
setSelectedLanguage(lang);
setIsOpen(false);
// Here you would typically call a function to change the language of the application
console.log(`Language changed to: ${lang.name}`);
if (typeof window !== 'undefined') {
const currentPath = window.location.pathname.replace(/\/en|\/zh/, ''); // Remove lang prefix
const newPath = getLocalizedPath(currentPath || '/', lang.code);
window.location.href = newPath;
}
};
return (
@@ -41,7 +60,7 @@ export default function LanguageSwitcher() {
className="absolute right-0 mt-2 w-40 origin-top-right rounded-md bg-popover text-popover-foreground shadow-md focus:outline-none z-50 border border-border/20"
>
<div className="p-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
{languages.map((lang) => (
{availableLanguages.map((lang) => (
<button
key={lang.code}
onClick={() => handleSelectLanguage(lang)}