diff --git a/next.config.mjs b/next.config.mjs index f5cbc38..3f80b85 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,14 +1,18 @@ +import createNextIntlPlugin from "next-intl/plugin"; + /** @type {import('next').NextConfig} */ const nextConfig = { - eslint: { - ignoreDuringBuilds: true, - }, - typescript: { - ignoreBuildErrors: true, - }, - images: { - unoptimized: true, - }, + eslint: { + ignoreDuringBuilds: true, + }, + typescript: { + ignoreBuildErrors: true, + }, + images: { + unoptimized: true, + }, } -export default nextConfig +const withNextIntl = createNextIntlPlugin(); + +export default withNextIntl(nextConfig); diff --git a/package.json b/package.json index 870f454..83decc3 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "input-otp": "1.4.1", "lucide-react": "^0.454.0", "next": "15.2.4", + "next-intl": "^4.1.0", "next-themes": "latest", "react": "^19", "react-day-picker": "8.10.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 45adb35..02a0240 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,6 +131,9 @@ importers: next: specifier: 15.2.4 version: 15.2.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next-intl: + specifier: ^4.1.0 + version: 4.1.0(next@15.2.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3) next-themes: specifier: latest version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -221,6 +224,24 @@ packages: '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + '@formatjs/ecma402-abstract@2.3.4': + resolution: {integrity: sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==} + + '@formatjs/fast-memoize@2.2.7': + resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} + + '@formatjs/icu-messageformat-parser@2.11.2': + resolution: {integrity: sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==} + + '@formatjs/icu-skeleton-parser@1.8.14': + resolution: {integrity: sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==} + + '@formatjs/intl-localematcher@0.5.10': + resolution: {integrity: sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==} + + '@formatjs/intl-localematcher@0.6.1': + resolution: {integrity: sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==} + '@hookform/resolvers@3.10.0': resolution: {integrity: sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==} peerDependencies: @@ -1082,6 +1103,9 @@ packages: '@radix-ui/rect@1.1.0': resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + '@schummar/icu-type-parser@1.21.5': + resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==} + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -1293,6 +1317,9 @@ packages: decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + decimal.js@10.5.0: + resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + detect-libc@2.0.4: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} @@ -1421,6 +1448,9 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + intl-messageformat@10.7.16: + resolution: {integrity: sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==} + is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} @@ -1513,6 +1543,20 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + next-intl@4.1.0: + resolution: {integrity: sha512-JNJRjc7sdnfUxhZmGcvzDszZ60tQKrygV/VLsgzXhnJDxQPn1cN2rVpc53adA1SvBJwPK2O6Sc6b4gYSILjCzw==} + peerDependencies: + next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: @@ -1879,6 +1923,11 @@ packages: '@types/react': optional: true + use-intl@4.1.0: + resolution: {integrity: sha512-mQvDYFvoGn+bm/PWvlQOtluKCknsQ5a9F1Cj0hMfBjMBVTwnOqLPd6srhjvVdEQEQFVyHM1PfyifKqKYb11M9Q==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 + use-sidecar@1.1.3: resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} engines: {node: '>=10'} @@ -1961,6 +2010,36 @@ snapshots: '@floating-ui/utils@0.2.9': {} + '@formatjs/ecma402-abstract@2.3.4': + dependencies: + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/intl-localematcher': 0.6.1 + decimal.js: 10.5.0 + tslib: 2.8.1 + + '@formatjs/fast-memoize@2.2.7': + dependencies: + tslib: 2.8.1 + + '@formatjs/icu-messageformat-parser@2.11.2': + dependencies: + '@formatjs/ecma402-abstract': 2.3.4 + '@formatjs/icu-skeleton-parser': 1.8.14 + tslib: 2.8.1 + + '@formatjs/icu-skeleton-parser@1.8.14': + dependencies: + '@formatjs/ecma402-abstract': 2.3.4 + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.5.10': + dependencies: + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.6.1': + dependencies: + tslib: 2.8.1 + '@hookform/resolvers@3.10.0(react-hook-form@7.56.2(react@19.1.0))': dependencies: react-hook-form: 7.56.2(react@19.1.0) @@ -2807,6 +2886,8 @@ snapshots: '@radix-ui/rect@1.1.0': {} + '@schummar/icu-type-parser@1.21.5': {} + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.15': @@ -3013,6 +3094,8 @@ snapshots: decimal.js-light@2.5.1: {} + decimal.js@10.5.0: {} + detect-libc@2.0.4: optional: true @@ -3125,6 +3208,13 @@ snapshots: internmap@2.0.3: {} + intl-messageformat@10.7.16: + dependencies: + '@formatjs/ecma402-abstract': 2.3.4 + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/icu-messageformat-parser': 2.11.2 + tslib: 2.8.1 + is-arrayish@0.3.2: optional: true @@ -3201,6 +3291,18 @@ snapshots: nanoid@3.3.11: {} + negotiator@1.0.0: {} + + next-intl@4.1.0(next@15.2.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(typescript@5.8.3): + dependencies: + '@formatjs/intl-localematcher': 0.5.10 + negotiator: 1.0.0 + next: 15.2.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + use-intl: 4.1.0(react@19.1.0) + optionalDependencies: + typescript: 5.8.3 + next-themes@0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: react: 19.1.0 @@ -3576,6 +3678,13 @@ snapshots: optionalDependencies: '@types/react': 19.1.2 + use-intl@4.1.0(react@19.1.0): + dependencies: + '@formatjs/fast-memoize': 2.2.7 + '@schummar/icu-type-parser': 1.21.5 + intl-messageformat: 10.7.16 + react: 19.1.0 + use-sidecar@1.1.3(@types/react@19.1.2)(react@19.1.0): dependencies: detect-node-es: 1.1.0 diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx new file mode 100644 index 0000000..5135277 --- /dev/null +++ b/src/app/[locale]/layout.tsx @@ -0,0 +1,44 @@ +import type React from "react" +import "@/app/globals.css" +import {ThemeProvider} from "@/components/theme-provider" +import {NextIntlClientProvider} from 'next-intl'; +import {setRequestLocale} from 'next-intl/server'; +import {hasLocale} from 'next-intl'; +import {notFound} from 'next/navigation'; +import {getMessages} from 'next-intl/server'; +import {routing} from "@/i18n/routing"; + +export const metadata = { + title: "grtsinry43 - 全栈开发者", + description: "grtsinry43的个人网站,全栈开发者,专注于Java/JavaScript,并正在转向Kotlin/TypeScript全栈。", +} + +export default async function RootLayout({ + children, + params + }: { + children: React.ReactNode; + params: { locale: string }; +}) { + const messages = await getMessages(); + const {locale} = await params; + + if (!hasLocale(routing.locales, locale)) { + notFound(); + } + + // Enable static rendering + setRequestLocale(locale); + + return ( + + + + + {children} + + + + + ) +} diff --git a/src/app/page.tsx b/src/app/[locale]/page.tsx similarity index 91% rename from src/app/page.tsx rename to src/app/[locale]/page.tsx index ba9e2e8..6206e11 100644 --- a/src/app/page.tsx +++ b/src/app/[locale]/page.tsx @@ -24,8 +24,10 @@ import GsapPersonalIntro from "@/components/sections/personal-intro"; import GsapPhotographySection from "@/components/sections/photography-section"; import GsapRhythmGamesSection from "@/components/sections/rhythm-games-section"; import FinalSection from "@/components/sections/final-section"; +import {useTranslations} from 'next-intl'; export default function HomePage() { + const t = useTranslations('HomePage'); const {theme} = useTheme() const [scrolled, setScrolled] = useState(false) const {scrollYProgress} = useScroll() @@ -82,7 +84,7 @@ export default function HomePage() { > -
grtsinry43
+
{t('greeting')}
@@ -96,8 +98,8 @@ export default function HomePage() {
- - grtsinry43 + + {t('title')} @@ -130,10 +132,10 @@ export default function HomePage() { {/* Horizontal scrolling text */}
- Full Stack • Java • JavaScript • Kotlin • TypeScript • React • Next.js • Spring Boot + {t('parallaxText1')} - Vue.js • Android • Jetpack Compose • WeChat Miniprogram • Arch Linux • 创新 • 探索 • 开发 + {t('parallaxText2')}
@@ -164,9 +166,9 @@ export default function HomePage() { className="mt-8 bg-gradient-to-br from-slate-300 to-slate-500 py-4 font-bold bg-clip-text text-center text-3xl tracking-tight text-transparent md:text-5xl" > - 要看更多? + {t('moreToSee')}

- 下面,继续看看我的个性和兴趣吧~ + {t('personalityAndInterests')}

@@ -198,8 +200,7 @@ export default function HomePage() { whileInView={{opacity: 1}} transition={{duration: 0.8}} > -

© {new Date().getFullYear()} grtsinry43. - 保留所有权利。

+

{t('footerRights', {year: new Date().getFullYear()})}

- GitHub + {t('srGitHub')} - Email + {t('srEmail')} - Blog + {t('srBlog')}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx deleted file mode 100644 index f8526f8..0000000 --- a/src/app/layout.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import type React from "react" -import "@/app/globals.css" -import {ThemeProvider} from "@/components/theme-provider" - -export const metadata = { - title: "grtsinry43 - 全栈开发者", - description: "grtsinry43的个人网站,全栈开发者,专注于Java/JavaScript,并正在转向Kotlin/TypeScript全栈。", -} - -export default function RootLayout({ - children, - }: { - children: React.ReactNode -}) { - return ( - - - - {children} - - - - ) -} diff --git a/src/i18n/navigation.ts b/src/i18n/navigation.ts new file mode 100644 index 0000000..cc09471 --- /dev/null +++ b/src/i18n/navigation.ts @@ -0,0 +1,7 @@ +import {createNavigation} from 'next-intl/navigation'; +import {routing} from './routing'; + +// Lightweight wrappers around Next.js' navigation +// APIs that consider the routing configuration +export const {Link, redirect, usePathname, useRouter, getPathname} = + createNavigation(routing); \ No newline at end of file diff --git a/src/i18n/request.ts b/src/i18n/request.ts new file mode 100644 index 0000000..f91cfc7 --- /dev/null +++ b/src/i18n/request.ts @@ -0,0 +1,16 @@ +import {getRequestConfig} from 'next-intl/server'; +import {hasLocale} from 'next-intl'; +import {routing} from './routing'; + +export default getRequestConfig(async ({requestLocale}) => { + // Typically corresponds to the `[locale]` segment + const requested = await requestLocale; + const locale = hasLocale(routing.locales, requested) + ? requested + : routing.defaultLocale; + + return { + locale, + messages: (await import(`../../messages/${locale}.json`)).default + }; +}); \ No newline at end of file diff --git a/src/i18n/routing.ts b/src/i18n/routing.ts new file mode 100644 index 0000000..c55156f --- /dev/null +++ b/src/i18n/routing.ts @@ -0,0 +1,7 @@ +import {defineRouting} from 'next-intl/routing'; + +export const routing = defineRouting({ + locales: ['en', 'zh'], + defaultLocale: 'zh', + localePrefix: 'as-needed' +}); \ No newline at end of file diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..65b803f --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,11 @@ +import createMiddleware from 'next-intl/middleware'; +import {routing} from './i18n/routing'; + +export default createMiddleware(routing); + +export const config = { + // Match all pathnames except for + // - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel` + // - … the ones containing a dot (e.g. `favicon.ico`) + matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)' +}; \ No newline at end of file