diff --git a/apps/fronted/next.config.mjs b/apps/fronted/next.config.mjs index 25ae6da..5ae9bbd 100644 --- a/apps/fronted/next.config.mjs +++ b/apps/fronted/next.config.mjs @@ -1,18 +1,30 @@ -/** @type {import('next').NextConfig} */ +import createNextIntlPlugin from "next-intl/plugin"; -const nextConfig = { +const withNextIntl = createNextIntlPlugin(); + +/** @type {import('next').NextConfig} */ +const config = { typescript: { - ignoreBuildErrors: true, + ignoreBuildErrors: true + }, + async redirects() { + return [ + { + source: "/", + destination: "/en", + permanent: true + } + ]; }, async rewrites() { return [ { source: "/generate-pdf", destination: - "http://1255612844-0z3iovadu8.ap-chengdu.tencentscf.com/generate-pdf", - }, + "http://1255612844-0z3iovadu8.ap-chengdu.tencentscf.com/generate-pdf" + } ]; - }, + } }; -export default nextConfig; +export default withNextIntl(config); diff --git a/apps/fronted/src/app/[locale]/layout.tsx b/apps/fronted/src/app/[locale]/layout.tsx new file mode 100644 index 0000000..106f458 --- /dev/null +++ b/apps/fronted/src/app/[locale]/layout.tsx @@ -0,0 +1,27 @@ +import { notFound } from "next/navigation"; +import { NextIntlClientProvider, useMessages } from "next-intl"; +import { locales } from "@/i18n/config"; + +export function generateStaticParams() { + return locales.map((locale) => ({ locale })); +} + +export default function LocaleLayout({ + children, + params: { locale } +}: { + children: React.ReactNode; + params: { locale: string }; +}) { + const messages = useMessages(); + + if (!locales.includes(locale as any)) { + notFound(); + } + + return ( + + {children} + + ); +} diff --git a/apps/fronted/src/app/page.tsx b/apps/fronted/src/app/[locale]/page.tsx similarity index 78% rename from apps/fronted/src/app/page.tsx rename to apps/fronted/src/app/[locale]/page.tsx index 95d9cff..85b5570 100644 --- a/apps/fronted/src/app/page.tsx +++ b/apps/fronted/src/app/[locale]/page.tsx @@ -1,11 +1,14 @@ import Link from "next/link"; import Image from "next/image"; +import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import Header from "@/components/home/Header"; -import { HeroAnimation } from "@/components/home/HeroAnimation"; -import { FeaturesAnimation } from "@/components/home/FeaturesAnimation"; +import FeaturesAnimation from "@/components/home/FeaturesAnimation"; +import HeroAnimation from "@/components/home/HeroAnimation"; export default function LandingPage() { + const t = useTranslations("home"); + return (
@@ -15,11 +18,11 @@ export default function LandingPage() {
-

- 开源免费,隐私优先 +

+ {t("hero.title")}

- 无需注册登录,数据完全存储在本地。 + {t("hero.subtitle")}

@@ -27,13 +30,13 @@ export default function LandingPage() { size="lg" className="rounded-full px-10 py-6 text-lg font-semibold shadow-lg hover:shadow-xl transition-all duration-300" > - 立即创建简历 + {t("hero.cta")}
-
+
- AI 智能纠错 + {t("features.ai.badge")}
-

智能识别,专业建议

+

+ {t("features.ai.title")} +

- 内置智能语法检查,自动识别不恰当的表达, - 提供专业的修改建议,让您的简历更加出色。 + {t("features.ai.description")}

@@ -96,14 +100,14 @@ export default function LandingPage() {
- 本地存储 + {t("features.storage.badge")}
-

数据安全,隐私优先

+

+ {t("features.storage.title")} +

- 所有简历数据完全存储在您的本地设备中,无需担心隐私泄露。 - 支持数据导出备份,确保您的简历数据随时可用。无需注册登录, - 即可享受所有功能。 + {t("features.storage.description")}

@@ -116,13 +120,14 @@ export default function LandingPage() {
- 实时预览 + {t("features.preview.badge")}
-

所见即所得

+

+ {t("features.preview.title")} +

- 边编辑边预览,实时查看简历效果。支持多种专业模板, - 让您的简历既美观又规范。快速导出PDF,随时投递简历。 + {t("features.preview.description")}

@@ -145,7 +150,7 @@ export default function LandingPage() {
-

© 2024 Magic Resume. All rights reserved.

+

{t("footer.copyright")}

diff --git a/apps/fronted/src/app/layout.tsx b/apps/fronted/src/app/layout.tsx index 3aa56a7..f0fd4f2 100644 --- a/apps/fronted/src/app/layout.tsx +++ b/apps/fronted/src/app/layout.tsx @@ -8,11 +8,11 @@ const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "魔法简历", - description: "极度自由的在线简历编辑器", + description: "极度自由的在线简历编辑器" }; export default function RootLayout({ - children, + children }: Readonly<{ children: React.ReactNode; }>) { diff --git a/apps/fronted/src/components/home/FeaturesAnimation.tsx b/apps/fronted/src/components/home/FeaturesAnimation.tsx index 9520dfd..84cc68e 100644 --- a/apps/fronted/src/components/home/FeaturesAnimation.tsx +++ b/apps/fronted/src/components/home/FeaturesAnimation.tsx @@ -1,13 +1,10 @@ "use client"; - -import { motion } from "framer-motion"; import { PropsWithChildren } from "react"; - +import { motion } from "framer-motion"; interface FeaturesAnimationProps extends PropsWithChildren { index: number; } - -export function FeaturesAnimation({ children, index }: FeaturesAnimationProps) { +const FeaturesAnimation = ({ children, index }: FeaturesAnimationProps) => { return ( ); -} +}; +export default FeaturesAnimation; diff --git a/apps/fronted/src/components/home/Header.tsx b/apps/fronted/src/components/home/Header.tsx index 988a1ef..3344fda 100644 --- a/apps/fronted/src/components/home/Header.tsx +++ b/apps/fronted/src/components/home/Header.tsx @@ -1,20 +1,29 @@ "use client"; - import { useEffect, useState } from "react"; import { Moon, Sun } from "lucide-react"; import { motion } from "framer-motion"; import Link from "next/link"; +import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import Logo from "@/components/shared/Logo"; import ThemeToggle from "@/components/shared/ThemeToggle"; import GitHubStars from "@/components/shared/GitHubStars"; +import LanguageSwitch from "../shared/LanguageSwitch"; export default function Header() { + const [mounted, setMounted] = useState(false); const [isVisible, setIsVisible] = useState(true); const [lastScrollY, setLastScrollY] = useState(0); + const t = useTranslations("home"); useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (!mounted) return; + const handleScroll = () => { const currentScrollY = window.scrollY; @@ -35,7 +44,11 @@ export default function Header() { window.addEventListener("scroll", handleScroll, { passive: true }); return () => window.removeEventListener("scroll", handleScroll); - }, [lastScrollY]); + }, [lastScrollY, mounted]); + + if (!mounted) { + return null; + } return (
@@ -44,19 +57,21 @@ export default function Header() { initial={{ y: 0, opacity: 1 }} animate={{ y: isVisible ? 0 : -100, - opacity: isVisible ? 1 : 0, + opacity: isVisible ? 1 : 0 }} transition={{ - duration: 0.2, + duration: 0.2 }} >
- Magic Resume + {t("header.title")}
+ +
@@ -69,7 +84,7 @@ export default function Header() { variant="default" className="bg-primary hover:opacity-90 text-white h-8 text-sm rounded-full px-4" > - 开始使用 + {t("header.startButton")}
diff --git a/apps/fronted/src/components/home/HeroAnimation.tsx b/apps/fronted/src/components/home/HeroAnimation.tsx index 3fe5e87..718ec18 100644 --- a/apps/fronted/src/components/home/HeroAnimation.tsx +++ b/apps/fronted/src/components/home/HeroAnimation.tsx @@ -1,9 +1,7 @@ "use client"; - -import { motion } from "framer-motion"; import { PropsWithChildren } from "react"; - -export function HeroAnimation({ children }: PropsWithChildren) { +import { motion } from "framer-motion"; +const HeroAnimation = ({ children }: PropsWithChildren) => { return ( ); -} +}; +export default HeroAnimation; diff --git a/apps/fronted/src/components/shared/LanguageSwitch.tsx b/apps/fronted/src/components/shared/LanguageSwitch.tsx new file mode 100644 index 0000000..7c2c00d --- /dev/null +++ b/apps/fronted/src/components/shared/LanguageSwitch.tsx @@ -0,0 +1,57 @@ +"use client"; +import { useLocale } from "next-intl"; +import { useRouter } from "next/navigation"; +import { Languages } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; +import { locales, localeNames } from "@/i18n/config"; + +export default function LanguageSwitch() { + const locale = useLocale(); + const router = useRouter(); + + const handleLanguageChange = (newLocale: string) => { + const currentPath = window.location.pathname; + if (currentPath === `/${locale}`) { + router.push(`/${newLocale}`); + } else { + const newPath = currentPath.replace(`/${locale}/`, `/${newLocale}/`); + router.push(newPath); + } + }; + + return ( + + + + + + {locales.map((loc) => ( + handleLanguageChange(loc)} + className={locale === loc ? "bg-accent" : ""} + > + + {localeNames[loc]} + {locale === loc && ( + + )} + + + ))} + + + ); +} diff --git a/apps/fronted/src/components/shared/ThemeToggle.tsx b/apps/fronted/src/components/shared/ThemeToggle.tsx index 7ba4f09..67a9911 100644 --- a/apps/fronted/src/components/shared/ThemeToggle.tsx +++ b/apps/fronted/src/components/shared/ThemeToggle.tsx @@ -1,19 +1,27 @@ "use client"; - import * as React from "react"; import { Moon, Sun } from "lucide-react"; import { useTheme } from "next-themes"; - import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuTrigger, + DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; const ThemeToggle = ({ children }: { children?: React.ReactNode }) => { const { setTheme } = useTheme(); + const [mounted, setMounted] = React.useState(false); + + React.useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return null; + } + return ( diff --git a/apps/fronted/src/i18n/config.ts b/apps/fronted/src/i18n/config.ts new file mode 100644 index 0000000..46e54c7 --- /dev/null +++ b/apps/fronted/src/i18n/config.ts @@ -0,0 +1,9 @@ +export const locales = ["zh", "en"] as const; +export type Locale = (typeof locales)[number]; + +export const defaultLocale: Locale = "zh"; + +export const localeNames: Record = { + zh: "中文", + en: "English" +}; diff --git a/apps/fronted/src/i18n/locales/en.json b/apps/fronted/src/i18n/locales/en.json new file mode 100644 index 0000000..29d191c --- /dev/null +++ b/apps/fronted/src/i18n/locales/en.json @@ -0,0 +1,33 @@ +{ + "home": { + "header": { + "title": "Magic Resume", + "startButton": "Get Started" + }, + "hero": { + "title": "Open Source, Privacy First", + "subtitle": "No registration required, all data stored locally.", + "cta": "Create Resume Now" + }, + "features": { + "ai": { + "badge": "AI Smart Check", + "title": "Smart Detection, Professional Advice", + "description": "Built-in intelligent grammar check, automatically identifies inappropriate expressions, and provides professional modification suggestions to make your resume stand out." + }, + "storage": { + "badge": "Local Storage", + "title": "Data Security, Privacy First", + "description": "All resume data is stored entirely on your local device, no need to worry about privacy leaks. Supports data export backup, ensuring your resume data is always available. No registration required to enjoy all features." + }, + "preview": { + "badge": "Real-time Preview", + "title": "What You See Is What You Get", + "description": "Edit and preview simultaneously, see resume effects in real-time. Supports multiple professional templates, making your resume both beautiful and standardized. Quick PDF export for immediate resume submission." + } + }, + "footer": { + "copyright": "© 2024 Magic Resume. All rights reserved." + } + } +} diff --git a/apps/fronted/src/i18n/locales/zh.json b/apps/fronted/src/i18n/locales/zh.json new file mode 100644 index 0000000..1d6064d --- /dev/null +++ b/apps/fronted/src/i18n/locales/zh.json @@ -0,0 +1,33 @@ +{ + "home": { + "header": { + "title": "魔法简历", + "startButton": "开始使用" + }, + "hero": { + "title": "开源免费,隐私优先", + "subtitle": "无需注册登录,数据完全存储在本地。", + "cta": "立即创建简历" + }, + "features": { + "ai": { + "badge": "AI 智能纠错", + "title": "智能识别,专业建议", + "description": "内置智能语法检查,自动识别不恰当的表达,提供专业的修改建议,让您的简历更加出色。" + }, + "storage": { + "badge": "本地存储", + "title": "数据安全,隐私优先", + "description": "所有简历数据完全存储在您的本地设备中,无需担心隐私泄露。支持数据导出备份,确保您的简历数据随时可用。无需注册登录,即可享受所有功能。" + }, + "preview": { + "badge": "实时预览", + "title": "所见即所得", + "description": "边编辑边预览,实时查看简历效果。支持多种专业模板,让您的简历既美观又规范。快速导出PDF,随时投递简历。" + } + }, + "footer": { + "copyright": "© 2024 Magic Resume. All rights reserved." + } + } +} diff --git a/apps/fronted/src/i18n/request.ts b/apps/fronted/src/i18n/request.ts new file mode 100644 index 0000000..99edff7 --- /dev/null +++ b/apps/fronted/src/i18n/request.ts @@ -0,0 +1,8 @@ +import { getRequestConfig } from "next-intl/server"; + +export default getRequestConfig(async ({ requestLocale }) => { + const locale = await requestLocale; + return { + messages: (await import(`./locales/${locale}.json`)).default + }; +}); diff --git a/apps/fronted/src/middleware.ts b/apps/fronted/src/middleware.ts new file mode 100644 index 0000000..146009e --- /dev/null +++ b/apps/fronted/src/middleware.ts @@ -0,0 +1,12 @@ +import createMiddleware from "next-intl/middleware"; +import { locales, defaultLocale } from "./i18n/config"; + +export default createMiddleware({ + locales, + defaultLocale, + localePrefix: "always" +}); + +export const config = { + matcher: ["/", "/(zh|en)"] +};