diff --git a/astro.config.mjs b/astro.config.mjs index e680c46..696a74f 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -9,6 +9,15 @@ export default defineConfig({ vite: { plugins: [tailwindcss()], }, - + i18n: { + // The default locale to fall back to if a page isn't available in the active locale + defaultLocale: "en", + // A list of all locales supported by the site + locales: ["en", "zh"], + routing: { + // URLs for the defaultLocale (en) will not have a /en/ prefix + prefixDefaultLocale: false, + } + }, integrations: [react()] }); \ No newline at end of file diff --git a/src/components/LanguageSwitcher.tsx b/src/components/LanguageSwitcher.tsx index 9cc102d..0e81e7c 100644 --- a/src/components/LanguageSwitcher.tsx +++ b/src/components/LanguageSwitcher.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { getLangFromUrl, getLocalizedPath, type Lang } from "@/i18n/utils"; +import { getLocalizedPath, type Lang } from "@/i18n/utils"; // getLangFromUrl is removed as Astro.currentLocale is used now. import { languages as i18nLanguages, defaultLang } from "@/i18n/ui"; import { Languages, Check } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; @@ -30,12 +30,41 @@ export default function LanguageSwitcher({ lang: initialLang }: LanguageSwitcher const toggleOpen = () => setIsOpen(!isOpen); - const handleSelectLanguage = (lang: typeof availableLanguages[0]) => { - setSelectedLanguage(lang); + const handleSelectLanguage = (languageOpt: typeof availableLanguages[0]) => { + setSelectedLanguage(languageOpt); setIsOpen(false); if (typeof window !== 'undefined') { - const currentPath = window.location.pathname.replace(/\/en|\/zh/, ''); // Remove lang prefix - const newPath = getLocalizedPath(currentPath || '/', lang.code); + const currentPathname = window.location.pathname; + const currentPathParts = currentPathname.split('/').filter(p => p); + let basePath = ''; + + // Check if the first part of the path is a known language code + if (currentPathParts.length > 0 && Object.keys(i18nLanguages).includes(currentPathParts[0])) { + basePath = '/' + currentPathParts.slice(1).join('/'); + } else { + basePath = currentPathname; + } + + // Ensure basePath always starts with a slash, or is just a slash for the root + if (!basePath.startsWith('/')) { + basePath = '/' + basePath; + } + if (basePath === '//') basePath = '/'; + + let newPath; + // If the target language is the default language and prefixDefaultLocale is false (as per our astro.config.mjs) + // then no language prefix is needed. + if (languageOpt.code === defaultLang) { + newPath = basePath; + } else { + // For non-default languages, prefix with the language code. + newPath = `/${languageOpt.code}${basePath}`; + } + + // Clean up double slashes, just in case + newPath = newPath.replace(/\/\/+/g, '/'); + if (newPath === '') newPath = '/'; // Handle case where basePath might be empty resulting in just /zh or /en + window.location.href = newPath; } }; diff --git a/src/components/MotionWrapper.tsx b/src/components/MotionWrapper.tsx index 0cea241..8da4b61 100644 --- a/src/components/MotionWrapper.tsx +++ b/src/components/MotionWrapper.tsx @@ -2,25 +2,11 @@ import React from "react"; import { motion } from "framer-motion"; import type { MotionProps } from "framer-motion"; -interface MotionWrapperProps extends MotionProps { +interface MotionWrapperProps extends Omit { children: React.ReactNode; delay?: number; } -// Default animations for sections -const defaultAnimations = { - hidden: { opacity: 0, y: 20 }, - visible: (delay: number = 0) => ({ - opacity: 1, - y: 0, - transition: { - duration: 0.6, - delay: delay, - ease: "easeOut", - }, - }), -}; - export default function MotionWrapper({ children, delay = 0, @@ -31,7 +17,18 @@ export default function MotionWrapper({ initial="hidden" whileInView="visible" viewport={{ once: true, margin: "-100px" }} - variants={defaultAnimations} + variants={{ + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.6, + delay, + ease: [0.43, 0.13, 0.23, 0.96] + } + } + }} custom={delay} {...props} > diff --git a/src/i18n/ui.ts b/src/i18n/ui.ts index 92d5a2e..9ec87ee 100644 --- a/src/i18n/ui.ts +++ b/src/i18n/ui.ts @@ -16,6 +16,7 @@ export const ui = { 'nav.education': 'Education', 'footer.rights': 'All rights reserved.', 'site.title': 'My Portfolio', + 'page.home.title': 'Home', // Personal Info 'personal.name': 'Rishikesh S', @@ -158,6 +159,7 @@ export const ui = { 'nav.education': '教育背景', 'footer.rights': '版权所有。', 'site.title': '我的作品集', + 'page.home.title': '首页', // Personal Info 'personal.name': 'Rishikesh S', diff --git a/src/i18n/utils.ts b/src/i18n/utils.ts index 3c4c641..831b245 100644 --- a/src/i18n/utils.ts +++ b/src/i18n/utils.ts @@ -4,11 +4,7 @@ import { ui, defaultLang, languages } from './ui'; export type Lang = keyof typeof languages; export type UiKeys = keyof typeof ui[typeof defaultLang]; -export function getLangFromUrl(url: URL): Lang { - const [, lang] = url.pathname.split('/'); - if (lang in languages) return lang as Lang; - return defaultLang; -} + export function useTranslations(lang: Lang | undefined) { const currentLang = lang || defaultLang; @@ -27,15 +23,30 @@ export function getLocalizedPath(path: string, lang: Lang | undefined): string { const currentLang = lang || defaultLang; const basePath = import.meta.env.BASE_URL === '/' ? '' : import.meta.env.BASE_URL; - // If the current language is the default language, do not add a language prefix. - if (currentLang === defaultLang) { - const fullPath = `${basePath}${path.startsWith('/') ? path : `/${path}`}`; - return fullPath.replace(/\/\/+/g, '/'); + // Astro's i18n routing handles prefixing automatically based on astro.config.mjs settings. + // We just need to ensure the path is correctly formed relative to the base. + // If a language is explicitly provided and it's not the default, + // and prefixDefaultLocale is false (meaning default lang has no prefix), + // we might need to add it. However, Astro typically handles this. + // For now, let's assume Astro's routing takes care of the prefix. + // This function might become simpler or even unnecessary depending on how LanguageSwitcher is used. + + let newPath = path; + // Ensure path starts with a slash if it's not an external URL + if (!newPath.startsWith('/') && !newPath.match(/^https?:\/\//)) { + newPath = `/${newPath}`; } - // Otherwise, add the language prefix. - const langPrefix = `/${currentLang}`; - const fullPath = `${basePath}${langPrefix}${path.startsWith('/') ? path : `/${path}`}`; - // Remove any double slashes that might occur. - return fullPath.replace(/\/\/+/g, '/'); + // If prefixDefaultLocale is false (default in our config) and currentLang is not defaultLang, + // Astro will expect /zh/path. If currentLang is defaultLang, it expects /path. + // If prefixDefaultLocale is true, all locales get a prefix: /en/path, /zh/path. + + // Given our astro.config.mjs: prefixDefaultLocale: false + // If lang is 'zh', the path should be /zh/your-path + // If lang is 'en' (default), the path should be /your-path + // Astro's or Astro.redirect should handle this correctly when given a root-relative path. + // This function's main job is to ensure the base path and the target path are combined correctly. + + const fullPath = `${basePath}${newPath}`; + return fullPath.replace(/\/\/+/g, '/'); // Clean up any double slashes } \ No newline at end of file diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index fe8b2cc..0002c59 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -1,14 +1,15 @@ --- -import { type Lang, useTranslations } from "@/i18n/utils"; +import { useTranslations, type Lang } from "@/i18n/utils"; +import { defaultLang } from "@/i18n/ui"; import "../styles/global.css"; interface Props { title?: string; description?: string; - lang: Lang; } -const { title = "Rishikesh S - Portfolio", description = "My Portfolio", lang } = +const lang = Astro.currentLocale as Lang || defaultLang; +const { title = "Rishikesh S - Portfolio", description = "My Portfolio" } = Astro.props; const t = useTranslations(lang); --- diff --git a/src/pages/index.astro b/src/pages/index.astro index 3755686..5499947 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,5 +1,4 @@ --- -import { getLangFromUrl } from "@/i18n/utils"; import Layout from "@/layouts/Layout.astro"; import GlassHeader from "@/components/GlassHeader"; import HeroSection from "@/components/HeroSection"; @@ -7,11 +6,15 @@ import ExperienceSection from "@/components/ExperienceSection"; import SkillsSection from "@/components/SkillsSection"; import ProjectsSection from "@/components/ProjectsSection"; import Footer from "@/components/Footer"; +import { useTranslations, type Lang } from "@/i18n/utils"; +import { defaultLang } from "@/i18n/ui"; -const lang = getLangFromUrl(Astro.url); +const lang: Lang = defaultLang; +const t = useTranslations(lang); +const pageTitle = t('page.home.title'); --- - +
diff --git a/src/pages/zh/index.astro b/src/pages/zh/index.astro index ae0ad17..2ae7037 100644 --- a/src/pages/zh/index.astro +++ b/src/pages/zh/index.astro @@ -1,17 +1,21 @@ --- -import { getLangFromUrl } from "@/i18n/utils"; import Layout from "@/layouts/Layout.astro"; -import GlassHeader from "@/components/GlassHeader"; -import HeroSection from "@/components/HeroSection"; -import ExperienceSection from "@/components/ExperienceSection"; -import SkillsSection from "@/components/SkillsSection"; -import ProjectsSection from "@/components/ProjectsSection"; -import Footer from "@/components/Footer"; +import GlassHeader from "@/components/GlassHeader.tsx"; +import HeroSection from "@/components/HeroSection.tsx"; +import ExperienceSection from "@/components/ExperienceSection.tsx"; +import SkillsSection from "@/components/SkillsSection.tsx"; +import ProjectsSection from "@/components/ProjectsSection.tsx"; +import Footer from "@/components/Footer.tsx"; +import { useTranslations } from "@/i18n/utils"; -const lang = getLangFromUrl(Astro.url); +// For /zh/ pages, Astro.currentLocale should be 'zh'. +// We explicitly set lang to 'zh' to ensure type correctness and intent. +const lang: "en" | "zh" = 'zh'; +const t = useTranslations(lang); +const pageTitle = t('page.home.title'); --- - +
@@ -19,5 +23,5 @@ const lang = getLangFromUrl(Astro.url);
-