From 99e26f3a2f892323587e58b3a8d02f90bce332d7 Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Tue, 28 Apr 2026 12:02:55 +0800 Subject: [PATCH] feat(i18n): implement internationalization support for website Add i18next with Chinese and English language support across all website sections. Convert hardcoded strings to translation keys in about, cases, services, team, footer, hero, and navbar components. Update document title and meta description to use translated values. Add language toggle in navbar. Remove contact section and unused UI components (button, card, input, textarea). BREAKING CHANGE: Site content now requires translation files to render properly. Removed @radix-ui/react-slot and class-variance-authority dependencies as contact form UI components were deleted. --- package-lock.json | 156 +++++++++++++++------- package.json | 5 +- src/App.tsx | 11 ++ src/components/sections/about.tsx | 109 +++++++-------- src/components/sections/cases.tsx | 93 +++++-------- src/components/sections/contact.tsx | 162 ----------------------- src/components/sections/footer.tsx | 32 ++--- src/components/sections/hero.tsx | 66 ++++----- src/components/sections/navbar.tsx | 128 ++++++++++-------- src/components/sections/services.tsx | 142 ++++++++------------ src/components/sections/team.tsx | 73 ++++------ src/components/ui/button.tsx | 56 -------- src/components/ui/card.tsx | 75 ----------- src/components/ui/input.tsx | 21 --- src/components/ui/textarea.tsx | 20 --- src/i18n/index.ts | 21 +++ src/i18n/locales/en.ts | 191 +++++++++++++++++++++++++++ src/i18n/locales/zh.ts | 191 +++++++++++++++++++++++++++ src/main.tsx | 1 + 19 files changed, 803 insertions(+), 750 deletions(-) delete mode 100644 src/components/sections/contact.tsx delete mode 100644 src/components/ui/button.tsx delete mode 100644 src/components/ui/card.tsx delete mode 100644 src/components/ui/input.tsx delete mode 100644 src/components/ui/textarea.tsx create mode 100644 src/i18n/index.ts create mode 100644 src/i18n/locales/en.ts create mode 100644 src/i18n/locales/zh.ts diff --git a/package-lock.json b/package-lock.json index 1d6a839..372346f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,14 @@ "name": "qczl-website", "version": "0.0.0", "dependencies": { - "@radix-ui/react-slot": "^1.2.4", - "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "geist": "^1.7.0", + "i18next": "^26.0.8", + "i18next-browser-languagedetector": "^8.2.1", "lucide-react": "^0.503.0", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-i18next": "^17.0.6", "tailwind-merge": "^3.2.0" }, "devDependencies": { @@ -261,6 +262,15 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.28.6.tgz", @@ -1487,39 +1497,6 @@ "node": ">= 10" } }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.4", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", - "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -2226,7 +2203,7 @@ "version": "19.2.14", "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2329,18 +2306,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } - }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz", @@ -2368,7 +2333,7 @@ "version": "3.2.3", "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -2531,6 +2496,52 @@ "dev": true, "license": "ISC" }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "26.0.8", + "resolved": "https://registry.npmmirror.com/i18next/-/i18next-26.0.8.tgz", + "integrity": "sha512-BRzLom0mhDhV9v0QhgUUHWQJuwFmnr1194xEcNLYD6ym8y8s542n4jXUvRLnhNTbh9PmpU6kGZamyuGHQMsGjw==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.1", + "resolved": "https://registry.npmmirror.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz", + "integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz", @@ -3048,6 +3059,33 @@ "react": "^19.2.5" } }, + "node_modules/react-i18next": { + "version": "17.0.6", + "resolved": "https://registry.npmmirror.com/react-i18next/-/react-i18next-17.0.6.tgz", + "integrity": "sha512-WzJ6SMKF+GTD7JZZqxSR1AKKmXjaSu39sClUrNlwxS4Tl7a99O+ltFy6yhPMO+wgZuxpQjJ2PZkfrQKmAqrLhw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.0.1", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", @@ -3270,7 +3308,7 @@ "version": "6.0.3", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -3311,6 +3349,15 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "6.4.2", "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.2.tgz", @@ -3386,6 +3433,15 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 4b1900e..8fd79c4 100644 --- a/package.json +++ b/package.json @@ -9,13 +9,14 @@ "preview": "vite preview" }, "dependencies": { - "@radix-ui/react-slot": "^1.2.4", - "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "geist": "^1.7.0", + "i18next": "^26.0.8", + "i18next-browser-languagedetector": "^8.2.1", "lucide-react": "^0.503.0", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-i18next": "^17.0.6", "tailwind-merge": "^3.2.0" }, "devDependencies": { diff --git a/src/App.tsx b/src/App.tsx index 333396f..6db0bca 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,5 @@ +import { useEffect } from "react" +import { useTranslation } from "react-i18next" import { Navbar } from "./components/sections/navbar" import { Hero } from "./components/sections/hero" import { About } from "./components/sections/about" @@ -6,6 +8,15 @@ import { Team } from "./components/sections/team" import { Footer } from "./components/sections/footer" function App() { + const { t, i18n } = useTranslation() + + useEffect(() => { + document.title = `${t("site.title")} - ${t("site.subtitle")}` + document + .querySelector('meta[name="description"]') + ?.setAttribute("content", `${t("site.title")} - ${t("site.subtitle")}`) + }, [t, i18n.language]) + return (
diff --git a/src/components/sections/about.tsx b/src/components/sections/about.tsx index 76803ba..d93dc7e 100644 --- a/src/components/sections/about.tsx +++ b/src/components/sections/about.tsx @@ -1,53 +1,42 @@ +import { useTranslation } from "react-i18next" import { Zap, Shield, Code2, Rocket, Eye, Heart, Bot, Gauge, Layers, MessageSquare } from "lucide-react" -const stats = [ - { label: "projects shipped", value: "100+", desc: "交付项目" }, - { label: "years building", value: "5+", desc: "持续开发" }, - { label: "client retention", value: "95%", desc: "客户留存" }, -] - -const advantages = [ - { icon: Bot, label: "AI 驱动", desc: "深度整合 AI 工具链,开发效率提升数倍" }, - { icon: Gauge, label: "快速交付", desc: "两周一个迭代,持续交付可运行的成果" }, - { icon: Layers, label: "全栈覆盖", desc: "前端、后端、移动端、基础设施一个团队搞定" }, - { icon: MessageSquare, label: "直接沟通", desc: "你直接和写代码的人对话,需求零损耗" }, -] - -const principles = [ - { icon: Code2, title: "Clean Code", desc: "代码即文档。我们写人类可读的代码。" }, - { icon: Zap, title: "Ship Fast", desc: "两周一个迭代,快速验证想法。" }, - { icon: Shield, title: "Own It", desc: "全栈负责到底。从需求到部署。" }, - { icon: Eye, title: "Transparency", desc: "进度完全透明,随时可查看代码和部署。" }, - { icon: Heart, title: "Craftsmanship", desc: "把每个项目当作自己的作品。" }, - { icon: Rocket, title: "Scale Ready", desc: "架构为增长设计,今天的小项目明天的大平台。" }, -] +const principleIcons = [Code2, Zap, Shield, Eye, Heart, Rocket] as const +const advantageIcons = [Bot, Gauge, Layers, MessageSquare] as const export function About() { + const { t } = useTranslation() + + type StatItem = { value: string; label: string; desc: string } + type AdvantageItem = { title: string; desc: string } + type PrincipleItem = { title: string; desc: string } + + const stats = t("about.stats", { returnObjects: true }) as StatItem[] + const advantages = t("about.whyUs.items", { returnObjects: true }) as AdvantageItem[] + const principles = t("about.principles.items", { returnObjects: true }) as PrincipleItem[] + return (
- +
- {/* Section header */}
- about us + {t("about.label")}

- 不是外包公司 + {t("about.title")}

- 我们是一个由全栈工程师组成的小团队,相信小团队能做出好产品。 - 没有项目经理层层转达,你直接和写代码的人对话。 + {t("about.description")}

- {/* Stats row */}
- {stats.map((stat) => ( -
( +
@@ -59,48 +48,52 @@ export function About() { ))}
- {/* Advantages */}
- why us + {t("about.whyUs.label")}
- {advantages.map((item) => ( -
-
- + {Array.isArray(advantages) && advantages.map((item, index) => { + const Icon = advantageIcons[index] + return ( +
+
+ +
+

{item.title}

+

{item.desc}

-

{item.label}

-

{item.desc}

-
- ))} + ) + })}
- {/* Principles */}
- our principles + {t("about.principles.label")}
- {principles.map((item, index) => ( -
- - {String(index + 1).padStart(2, "0")} - - -
-

{item.title}

-
-

{item.desc}

+ {Array.isArray(principles) && principles.map((item, index) => { + const Icon = principleIcons[index] + return ( +
+ + {String(index + 1).padStart(2, "0")} + + +
+

{item.title}

+
+

{item.desc}

+
-
- ))} + ) + })}
diff --git a/src/components/sections/cases.tsx b/src/components/sections/cases.tsx index c1d613f..dfd4557 100644 --- a/src/components/sections/cases.tsx +++ b/src/components/sections/cases.tsx @@ -1,83 +1,60 @@ -import { Smartphone, Building2, Printer } from "lucide-react" +import { useTranslation } from "react-i18next" +import { Smartphone, Building2, Printer, LucideIcon } from "lucide-react" -const cases = [ - { - id: "01", - title: "威友", - subtitle: "Weiyou", - category: "校园社交", - description: "面向高校学生的校园交友 APP。支持兴趣匹配、活动发布、匿名树洞等功能,覆盖 iOS、Android 双端及 Web 管理后台。", - platforms: ["iOS", "Android", "Web"], - tags: ["React Native", "Go", "PostgreSQL"], - icon: Smartphone, - status: "live", - }, - { - id: "02", - title: "CarrotERP", - subtitle: "Enterprise Resource Planning", - category: "企业管理", - description: "内部使用的 ERP 管理软件,对标行业领军产品。涵盖采购、库存、财务、人事等核心模块,支撑企业日常运营全流程。", - platforms: ["Web", "Desktop"], - tags: ["React", "Go", "PostgreSQL"], - icon: Building2, - status: "live", - }, - { - id: "03", - title: "CarrotPrint", - subtitle: "Self-service Printing", - category: "自助服务", - description: "自助打印解决方案,覆盖微信小程序、Windows 客户端、Linux 服务端三端。用户扫码上传文件,就近打印取件。", - platforms: ["小程序", "Windows", "Linux"], - tags: ["Rust", "C#", "Taro", "Go"], - icon: Printer, - status: "live", - }, -] +const caseIcons: Record = { + "威友": Smartphone, + "CarrotERP": Building2, + "CarrotPrint": Printer, +} export function Cases() { + const { t } = useTranslation() + + type CaseItem = { + id: string + title: string + subtitle: string + category: string + description: string + platforms: string[] + tags: string[] + status: string + } + + const items = t("cases.items", { returnObjects: true }) as CaseItem[] + return (
- {/* Section header */}
- selected work + {t("cases.label")}

- 项目案例 + {t("cases.title")}

- 以下是我们自主研发的核心产品,每个项目都经过真实场景验证。 + {t("cases.description")}

- {/* Cases list - one column, alternating layout */}
- {cases.map((item, index) => { + {Array.isArray(items) && items.map((item, index) => { const isEven = index % 2 === 0 - const Icon = item.icon + const Icon = caseIcons[item.title] || Smartphone return ( -
+
- {/* Left side - Visual */}
- {/* Card mockup */}
- {/* Background glow */}
- {/* Icon & Title */}
@@ -88,9 +65,8 @@ export function Cases() {
- {/* Platform badges */}
- {item.platforms.map((platform) => ( + {item.platforms.map((platform: string) => ( - {/* Status */}
{item.status} @@ -108,36 +83,30 @@ export function Cases() {
- {/* Index number */}
{item.id}
- {/* Right side - Details */}
- {/* Category */} {item.category} - {/* Title */}

{item.title}

- {/* Description */}

{item.description}

- {/* Tech stack */}
- tech stack + {t("cases.techStack")}
- {item.tags.map((tag) => ( + {item.tags.map((tag: string) => ( { - e.preventDefault() - setSubmitted(true) - setTimeout(() => { - setSubmitted(false) - setFormData({ name: "", email: "", company: "", message: "" }) - }, 3000) - } - - return ( -
-
-
-

- 联系我们 -

-

- 无论您有任何技术需求或合作意向,欢迎随时与我们联系 -

-
- -
-
- {contactInfo.map((info) => ( - - -
- -
-
-

- {info.title} -

-

- {info.content} -

-
-
-
- ))} -
- -
- - - {submitted ? ( -
-
- -
-

- 提交成功 -

-

- 感谢您的留言,我们会尽快与您联系! -

-
- ) : ( -
-
-
- - - setFormData({ ...formData, name: e.target.value }) - } - required - /> -
-
- - - setFormData({ ...formData, email: e.target.value }) - } - required - /> -
-
-
- - - setFormData({ ...formData, company: e.target.value }) - } - /> -
-
- -