feat: implement internationalization support with next-intl and routing configuration

This commit is contained in:
grtsinry43 2025-05-10 21:08:39 +08:00
parent 90e99c0c51
commit 31dbaf0261
Signed by: grtsinry43
GPG Key ID: F3305FB3A978C934
10 changed files with 222 additions and 46 deletions

View File

@ -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);

View File

@ -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",

109
pnpm-lock.yaml generated
View File

@ -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

View File

@ -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 (
<html lang={locale} suppressHydrationWarning>
<body>
<NextIntlClientProvider messages={messages}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{children}
</ThemeProvider>
</NextIntlClientProvider>
</body>
</html>
)
}

View File

@ -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() {
>
<img className="w-12 h-12 mr-4 rounded-full"
src={"https://dogeoss.grtsinry43.com/img/author.jpeg"}/>
<div>grtsinry43</div>
<div>{t('greeting')}</div>
</motion.div>
</motion.div>
@ -96,8 +98,8 @@ export default function HomePage() {
<div className="container flex items-center justify-between h-16 px-4 mx-auto">
<motion.div initial={{opacity: 0, y: -20}} animate={{opacity: 1, y: 0}}
transition={{duration: 0.5}}>
<Link href="/" className="text-xl font-bold tracking-tight">
grtsinry43
<Link href="/public" className="text-xl font-bold tracking-tight">
{t('title')}
</Link>
</motion.div>
@ -130,10 +132,10 @@ export default function HomePage() {
{/* Horizontal scrolling text */}
<section className="py-20 overflow-hidden opacity-55">
<ParallaxText baseVelocity={-3}>
Full Stack Java JavaScript Kotlin TypeScript React Next.js Spring Boot
{t('parallaxText1')}
</ParallaxText>
<ParallaxText baseVelocity={3}>
Vue.js Android Jetpack Compose WeChat Miniprogram Arch Linux
{t('parallaxText2')}
</ParallaxText>
</section>
@ -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')}
<p className="mt-4">
~
{t('personalityAndInterests')}
</p>
</motion.h1>
</LampContainer>
@ -198,8 +200,7 @@ export default function HomePage() {
whileInView={{opacity: 1}}
transition={{duration: 0.8}}
>
<p className="text-sm text-muted-foreground">© {new Date().getFullYear()} grtsinry43.
</p>
<p className="text-sm text-muted-foreground">{t('footerRights', {year: new Date().getFullYear()})}</p>
</motion.div>
<motion.div
className="flex items-center space-x-6"
@ -214,14 +215,14 @@ export default function HomePage() {
className="text-muted-foreground hover:text-foreground transition-all duration-300 hover:scale-110"
>
<Github className="h-5 w-5"/>
<span className="sr-only">GitHub</span>
<span className="sr-only">{t('srGitHub')}</span>
</a>
<a
href="mailto:grtsinry43@outlook.com"
className="text-muted-foreground hover:text-foreground transition-all duration-300 hover:scale-110"
>
<Mail className="h-5 w-5"/>
<span className="sr-only">Email</span>
<span className="sr-only">{t('srEmail')}</span>
</a>
<a
href="https://blog.grtsinry43.com"
@ -230,7 +231,7 @@ export default function HomePage() {
className="text-muted-foreground hover:text-foreground transition-all duration-300 hover:scale-110"
>
<ExternalLink className="h-5 w-5"/>
<span className="sr-only">Blog</span>
<span className="sr-only">{t('srBlog')}</span>
</a>
</motion.div>
</div>

View File

@ -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 (
<html lang="zh-CN" suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{children}
</ThemeProvider>
</body>
</html>
)
}

7
src/i18n/navigation.ts Normal file
View File

@ -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);

16
src/i18n/request.ts Normal file
View File

@ -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
};
});

7
src/i18n/routing.ts Normal file
View File

@ -0,0 +1,7 @@
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
locales: ['en', 'zh'],
defaultLocale: 'zh',
localePrefix: 'as-needed'
});

11
src/middleware.ts Normal file
View File

@ -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|.*\\..*).*)'
};