feat: implement internationalization with next-intl
- Add multi-language support using next-intl - Configure routing and localization for English and Chinese - Update project structure to support i18n routing - Add middleware and navigation helpers for localized routing - Create message files for translations - Modify layout and page components to support internationalization
This commit is contained in:
parent
24eb819a85
commit
4c6997a012
12
messages/cn.json
Normal file
12
messages/cn.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"HomePage": {
|
||||
"title": "你好,世界!",
|
||||
"about": "去关于页面"
|
||||
},
|
||||
"AboutPage": {
|
||||
"title": "关于"
|
||||
},
|
||||
"BlogPage": {
|
||||
"title": "博客"
|
||||
}
|
||||
}
|
12
messages/en.json
Normal file
12
messages/en.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"HomePage": {
|
||||
"title": "Hello world!",
|
||||
"about": "Go to the about page"
|
||||
},
|
||||
"AboutPage": {
|
||||
"title": "About"
|
||||
},
|
||||
"BlogPage": {
|
||||
"title": "Blog"
|
||||
}
|
||||
}
|
@ -1,6 +1,15 @@
|
||||
import type { NextConfig } from "next";
|
||||
import createNextIntlPlugin from 'next-intl/plugin';
|
||||
import { withContentCollections } from "@content-collections/next";
|
||||
|
||||
/**
|
||||
* https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing#next-config
|
||||
*/
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
/**
|
||||
* https://nextjs.org/docs/app/api-reference/config/next-config-js
|
||||
*/
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
|
||||
@ -13,11 +22,13 @@ const nextConfig: NextConfig = {
|
||||
images: {
|
||||
// https://vercel.com/docs/image-optimization/managing-image-optimization-costs#minimizing-image-optimization-costs
|
||||
// vercel has limits on image optimization, 1000 images per month
|
||||
unoptimized: true,
|
||||
// unoptimized: true,
|
||||
|
||||
// https://medium.com/@niniroula/nextjs-upgrade-next-image-and-dangerouslyallowsvg-c934060d79f8
|
||||
// The requested resource "https://cdn.sanity.io/images/58a2mkbj/preview/xxx.svg?fit=max&auto=format" has type "image/svg+xml"
|
||||
// but dangerouslyAllowSVG is disabled
|
||||
dangerouslyAllowSVG: true,
|
||||
// dangerouslyAllowSVG: true,
|
||||
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
@ -31,18 +42,13 @@ const nextConfig: NextConfig = {
|
||||
protocol: "https",
|
||||
hostname: "randomuser.me",
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "cdn.sanity.io", // https://www.sanity.io/learn/course/day-one-with-sanity-studio/bringing-content-to-a-next-js-front-end
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "via.placeholder.com", // https://www.sanity.io/learn/course/day-one-with-sanity-studio/bringing-content-to-a-next-js-front-end
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// https://www.content-collections.dev/docs/quickstart/next
|
||||
// withContentCollections must be the outermost plugin
|
||||
export default withContentCollections(nextConfig);
|
||||
/**
|
||||
* withContentCollections must be the outermost plugin
|
||||
*
|
||||
* https://www.content-collections.dev/docs/quickstart/next
|
||||
*/
|
||||
export default withContentCollections(withNextIntl(nextConfig));
|
||||
|
@ -42,6 +42,7 @@
|
||||
"mdast-util-toc": "^7.1.0",
|
||||
"motion": "^12.4.3",
|
||||
"next": "15.1.7",
|
||||
"next-intl": "^3.26.5",
|
||||
"next-plausible": "^3.12.4",
|
||||
"next-safe-action": "^7.10.4",
|
||||
"next-themes": "^0.4.4",
|
||||
|
97
pnpm-lock.yaml
generated
97
pnpm-lock.yaml
generated
@ -107,6 +107,9 @@ importers:
|
||||
next:
|
||||
specifier: 15.1.7
|
||||
version: 15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
next-intl:
|
||||
specifier: ^3.26.5
|
||||
version: 3.26.5(next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
|
||||
next-plausible:
|
||||
specifier: ^3.12.4
|
||||
version: 3.12.4(next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
@ -976,6 +979,24 @@ packages:
|
||||
'@floating-ui/utils@0.2.9':
|
||||
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
|
||||
|
||||
'@formatjs/ecma402-abstract@2.3.3':
|
||||
resolution: {integrity: sha512-pJT1OkhplSmvvr6i3CWTPvC/FGC06MbN5TNBfRO6Ox62AEz90eMq+dVvtX9Bl3jxCEkS0tATzDarRZuOLw7oFg==}
|
||||
|
||||
'@formatjs/fast-memoize@2.2.6':
|
||||
resolution: {integrity: sha512-luIXeE2LJbQnnzotY1f2U2m7xuQNj2DA8Vq4ce1BY9ebRZaoPB1+8eZ6nXpLzsxuW5spQxr7LdCg+CApZwkqkw==}
|
||||
|
||||
'@formatjs/icu-messageformat-parser@2.11.1':
|
||||
resolution: {integrity: sha512-o0AhSNaOfKoic0Sn1GkFCK4MxdRsw7mPJ5/rBpIqdvcC7MIuyUSW8WChUEvrK78HhNpYOgqCQbINxCTumJLzZA==}
|
||||
|
||||
'@formatjs/icu-skeleton-parser@1.8.13':
|
||||
resolution: {integrity: sha512-N/LIdTvVc1TpJmMt2jVg0Fr1F7Q1qJPdZSCs19unMskCmVQ/sa0H9L8PWt13vq+gLdLg1+pPsvBLydL1Apahjg==}
|
||||
|
||||
'@formatjs/intl-localematcher@0.5.10':
|
||||
resolution: {integrity: sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==}
|
||||
|
||||
'@formatjs/intl-localematcher@0.6.0':
|
||||
resolution: {integrity: sha512-4rB4g+3hESy1bHSBG3tDFaMY2CH67iT7yne1e+0CLTsGLDcmoEWWpJjjpWVaYgYfYuohIRuo0E+N536gd2ZHZA==}
|
||||
|
||||
'@hexagon/base64@1.1.28':
|
||||
resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
|
||||
|
||||
@ -2063,6 +2084,9 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
decimal.js@10.5.0:
|
||||
resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==}
|
||||
|
||||
decode-named-character-reference@1.0.2:
|
||||
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
|
||||
|
||||
@ -2494,6 +2518,9 @@ packages:
|
||||
inline-style-parser@0.2.4:
|
||||
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
|
||||
|
||||
intl-messageformat@10.7.15:
|
||||
resolution: {integrity: sha512-LRyExsEsefQSBjU2p47oAheoKz+EOJxSLDdjOaEjdriajfHsMXOmV/EhMvYSg9bAgCUHasuAC+mcUBe/95PfIg==}
|
||||
|
||||
is-alphabetical@2.0.1:
|
||||
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
|
||||
|
||||
@ -2855,6 +2882,16 @@ packages:
|
||||
resolution: {integrity: sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
|
||||
negotiator@1.0.0:
|
||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
next-intl@3.26.5:
|
||||
resolution: {integrity: sha512-EQlCIfY0jOhRldiFxwSXG+ImwkQtDEfQeSOEQp6ieAGSLWGlgjdb/Ck/O7wMfC430ZHGeUKVKax8KGusTPKCgg==}
|
||||
peerDependencies:
|
||||
next: ^10.0.0 || ^11.0.0 || ^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
|
||||
|
||||
next-plausible@3.12.4:
|
||||
resolution: {integrity: sha512-cD3+ixJxf8yBYvsideTxqli3fvrB7R4BXcvsNJz8Sm2X1QN039WfiXjCyNWkub4h5++rRs6fHhchUMnOuJokcg==}
|
||||
peerDependencies:
|
||||
@ -3595,6 +3632,11 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
use-intl@3.26.5:
|
||||
resolution: {integrity: sha512-OdsJnC/znPvHCHLQH/duvQNXnP1w0hPfS+tkSi3mAbfjYBGh4JnyfdwkQBfIVf7t8gs9eSX/CntxUMvtKdG2MQ==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^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'}
|
||||
@ -4183,6 +4225,36 @@ snapshots:
|
||||
|
||||
'@floating-ui/utils@0.2.9': {}
|
||||
|
||||
'@formatjs/ecma402-abstract@2.3.3':
|
||||
dependencies:
|
||||
'@formatjs/fast-memoize': 2.2.6
|
||||
'@formatjs/intl-localematcher': 0.6.0
|
||||
decimal.js: 10.5.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/fast-memoize@2.2.6':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/icu-messageformat-parser@2.11.1':
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.3
|
||||
'@formatjs/icu-skeleton-parser': 1.8.13
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/icu-skeleton-parser@1.8.13':
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.3
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/intl-localematcher@0.5.10':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@formatjs/intl-localematcher@0.6.0':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@hexagon/base64@1.1.28': {}
|
||||
|
||||
'@hookform/resolvers@4.1.0(react-hook-form@7.54.2(react@19.0.0))':
|
||||
@ -5262,6 +5334,8 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
decimal.js@10.5.0: {}
|
||||
|
||||
decode-named-character-reference@1.0.2:
|
||||
dependencies:
|
||||
character-entities: 2.0.2
|
||||
@ -5775,6 +5849,13 @@ snapshots:
|
||||
|
||||
inline-style-parser@0.2.4: {}
|
||||
|
||||
intl-messageformat@10.7.15:
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.3
|
||||
'@formatjs/fast-memoize': 2.2.6
|
||||
'@formatjs/icu-messageformat-parser': 2.11.1
|
||||
tslib: 2.8.1
|
||||
|
||||
is-alphabetical@2.0.1: {}
|
||||
|
||||
is-alphanumerical@2.0.1:
|
||||
@ -6396,6 +6477,16 @@ snapshots:
|
||||
|
||||
nanostores@0.11.4: {}
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
|
||||
next-intl@3.26.5(next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
'@formatjs/intl-localematcher': 0.5.10
|
||||
negotiator: 1.0.0
|
||||
next: 15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
react: 19.0.0
|
||||
use-intl: 3.26.5(react@19.0.0)
|
||||
|
||||
next-plausible@3.12.4(next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
next: 15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
@ -7249,6 +7340,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.9
|
||||
|
||||
use-intl@3.26.5(react@19.0.0):
|
||||
dependencies:
|
||||
'@formatjs/fast-memoize': 2.2.6
|
||||
intl-messageformat: 10.7.15
|
||||
react: 19.0.0
|
||||
|
||||
use-sidecar@1.1.3(@types/react@19.0.9)(react@19.0.0):
|
||||
dependencies:
|
||||
detect-node-es: 1.1.0
|
||||
|
@ -1,94 +0,0 @@
|
||||
// import PromotekitScript from "@/components/affiliate/promotekit-stripe-checkout";
|
||||
// import { HomeCallToAction } from "@/components/home/home-cta";
|
||||
// import { HomeFAQ } from "@/components/home/home-faq";
|
||||
// import HomeFeatures from "@/components/home/home-features";
|
||||
// import { HomeFeaturesHeader } from "@/components/home/home-features-header";
|
||||
// import { HomeFeaturesMore } from "@/components/home/home-features-more";
|
||||
// import HomeHero from "@/components/home/home-hero";
|
||||
// import { HomeHowItWorks } from "@/components/home/home-how-it-works";
|
||||
// import { HomeIntroduction } from "@/components/home/home-introduction";
|
||||
// import HomeMonetization from "@/components/home/home-monetization";
|
||||
// import { HomeNewsletter } from "@/components/home/home-newsletter";
|
||||
// import HomePowered from "@/components/home/home-powered";
|
||||
// import HomePricing from "@/components/home/home-pricing";
|
||||
// import { HomeShowcase } from "@/components/home/home-showcase";
|
||||
// import { HomeTestimonials } from "@/components/home/home-testimonials";
|
||||
// import HomeVideo from "@/components/home/home-video";
|
||||
import CallToAction from "@/components/nsui/call-to-action";
|
||||
import ContentSection from "@/components/nsui/content-2";
|
||||
import FAQs from "@/components/nsui/faqs";
|
||||
import Features from "@/components/nsui/features-2";
|
||||
import FeaturesSection from "@/components/nsui/features-8";
|
||||
import HeroSection from "@/components/nsui/hero-section";
|
||||
import LogoCloud from "@/components/nsui/logo-cloud";
|
||||
import Pricing from "@/components/nsui/pricing";
|
||||
import StatsSection from "@/components/nsui/stats";
|
||||
import WallOfLoveSection from "@/components/nsui/testimonials";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "",
|
||||
canonicalUrl: `${siteConfig.url}/`,
|
||||
});
|
||||
|
||||
export default async function HomePage() {
|
||||
return (
|
||||
<>
|
||||
{/* <PromotekitScript /> */}
|
||||
|
||||
<div className="mt-12 flex flex-col gap-16">
|
||||
|
||||
<HeroSection />
|
||||
|
||||
<LogoCloud />
|
||||
|
||||
{/* <Features /> */}
|
||||
|
||||
<FeaturesSection />
|
||||
|
||||
<ContentSection />
|
||||
|
||||
<Pricing />
|
||||
|
||||
<FAQs />
|
||||
|
||||
<WallOfLoveSection />
|
||||
|
||||
<StatsSection />
|
||||
|
||||
<CallToAction />
|
||||
|
||||
{/* <HomeHero /> */}
|
||||
|
||||
{/* <HomeVideo /> */}
|
||||
|
||||
{/* <HomePowered /> */}
|
||||
|
||||
{/* <HomeMonetization /> */}
|
||||
|
||||
{/* <HomeHowItWorks /> */}
|
||||
|
||||
{/* <HomeFeaturesHeader /> */}
|
||||
|
||||
{/* <HomeFeatures /> */}
|
||||
|
||||
{/* <HomeFeaturesMore /> */}
|
||||
|
||||
{/* <HomePricing /> */}
|
||||
|
||||
{/* <HomeFAQ /> */}
|
||||
|
||||
{/* <HomeIntroduction /> */}
|
||||
|
||||
{/* <HomeTestimonials /> */}
|
||||
|
||||
{/* <HomeCallToAction /> */}
|
||||
|
||||
{/* <HomeShowcase /> */}
|
||||
|
||||
{/* <HomeNewsletter /> */}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
65
src/app/[locale]/(public)/(home)/page.tsx
Normal file
65
src/app/[locale]/(public)/(home)/page.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import CallToAction from "@/components/nsui/call-to-action";
|
||||
import ContentSection from "@/components/nsui/content-2";
|
||||
import FAQs from "@/components/nsui/faqs";
|
||||
import FeaturesSection from "@/components/nsui/features-8";
|
||||
import HeroSection from "@/components/nsui/hero-section";
|
||||
import LogoCloud from "@/components/nsui/logo-cloud";
|
||||
import Pricing from "@/components/nsui/pricing";
|
||||
import StatsSection from "@/components/nsui/stats";
|
||||
import WallOfLoveSection from "@/components/nsui/testimonials";
|
||||
import { siteConfig } from "@/config/site";
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "",
|
||||
canonicalUrl: `${siteConfig.url}/`,
|
||||
});
|
||||
|
||||
interface HomePageProps {
|
||||
params: { locale: string };
|
||||
};
|
||||
|
||||
export default async function HomePage({ params }: HomePageProps) {
|
||||
const { locale } = params;
|
||||
|
||||
// Enable static rendering
|
||||
setRequestLocale(locale);
|
||||
|
||||
// Use getTranslations instead of useTranslations for async server components
|
||||
const t = await getTranslations('HomePage');
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <PromotekitScript /> */}
|
||||
|
||||
<div className="mt-12 flex flex-col gap-16">
|
||||
|
||||
<div>
|
||||
<h1>{t('title')}</h1>
|
||||
{/* <Link href="/about">{t('about')}</Link> */}
|
||||
</div>
|
||||
|
||||
<HeroSection />
|
||||
|
||||
<LogoCloud />
|
||||
|
||||
{/* <Features /> */}
|
||||
|
||||
<FeaturesSection />
|
||||
|
||||
<ContentSection />
|
||||
|
||||
<Pricing />
|
||||
|
||||
<FAQs />
|
||||
|
||||
<WallOfLoveSection />
|
||||
|
||||
<StatsSection />
|
||||
|
||||
<CallToAction />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
20
src/app/[locale]/(public)/about/page.tsx
Normal file
20
src/app/[locale]/(public)/about/page.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
|
||||
interface AboutPageProps {
|
||||
params: { locale: string };
|
||||
};
|
||||
|
||||
export default async function AboutPage({ params }: AboutPageProps) {
|
||||
const { locale } = params;
|
||||
|
||||
// Enable static rendering
|
||||
setRequestLocale(locale);
|
||||
|
||||
const t = await getTranslations('AboutPage');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{t('title')}</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,18 +1,25 @@
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { Navbar } from "@/components/marketing/navbar";
|
||||
import { marketingConfig } from "@/config/marketing";
|
||||
import { setRequestLocale } from "next-intl/server";
|
||||
|
||||
interface MarketingLayoutProps {
|
||||
children: React.ReactNode;
|
||||
params: { locale: string };
|
||||
}
|
||||
|
||||
export default async function MarketingLayout({
|
||||
children,
|
||||
params
|
||||
}: MarketingLayoutProps) {
|
||||
const { locale } = params;
|
||||
|
||||
// Enable static rendering
|
||||
setRequestLocale(locale);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
{/* <Navbar scroll={true} config={marketingConfig} /> */}
|
||||
<Navbar scroll={false} config={marketingConfig} />
|
||||
<Navbar scroll={true} config={marketingConfig} />
|
||||
|
||||
<main className="flex-1">{children}</main>
|
||||
|
5
src/app/[locale]/[...rest]/page.tsx
Normal file
5
src/app/[locale]/[...rest]/page.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
export default function CatchAllPage() {
|
||||
notFound();
|
||||
}
|
82
src/app/[locale]/layout.tsx
Normal file
82
src/app/[locale]/layout.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import "@/app/globals.css";
|
||||
import { fontSourceSans, fontSourceSerif4 } from "@/assets/fonts";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { routing } from '@/i18n/routing';
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { GeistMono } from "geist/font/mono";
|
||||
import { GeistSans } from "geist/font/sans";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getMessages, setRequestLocale } from "next-intl/server";
|
||||
import { notFound } from "next/navigation";
|
||||
import type { ReactNode } from "react";
|
||||
import { Providers } from "./providers";
|
||||
import { TailwindIndicator } from "@/components/tailwind-indicator";
|
||||
|
||||
export const metadata: Metadata = constructMetadata();
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
minimumScale: 1,
|
||||
maximumScale: 1,
|
||||
themeColor: [
|
||||
{ media: '(prefers-color-scheme: light)', color: 'white' },
|
||||
{ media: '(prefers-color-scheme: dark)', color: 'black' }
|
||||
]
|
||||
};
|
||||
|
||||
export function generateStaticParams() {
|
||||
return routing.locales.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
interface LocaleLayoutProps {
|
||||
children: ReactNode;
|
||||
params: { locale: string };
|
||||
};
|
||||
|
||||
/**
|
||||
* https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing#layout
|
||||
*/
|
||||
export default async function LocaleLayout({ children, params }: LocaleLayoutProps) {
|
||||
const { locale } = params;
|
||||
|
||||
// Ensure that the incoming `locale` is valid
|
||||
if (!routing.locales.includes(locale as any)) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Enable static rendering
|
||||
setRequestLocale(locale);
|
||||
|
||||
// Providing all messages to the client
|
||||
// side is the easiest way to get started
|
||||
const messages = await getMessages();
|
||||
|
||||
// TODO: body min-h-screen -> size-full, check if this is correct on mobile
|
||||
return (
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<body
|
||||
className={cn(
|
||||
"size-full antialiased",
|
||||
GeistSans.className,
|
||||
fontSourceSerif4.variable,
|
||||
fontSourceSans.variable,
|
||||
GeistSans.variable,
|
||||
GeistMono.variable,
|
||||
)}
|
||||
>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<Providers>
|
||||
{children}
|
||||
|
||||
<Toaster richColors position="top-right" offset={64} />
|
||||
|
||||
<TailwindIndicator />
|
||||
</Providers>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
22
src/app/[locale]/providers.tsx
Normal file
22
src/app/[locale]/providers.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
|
||||
export function Providers({
|
||||
children
|
||||
}: React.PropsWithChildren) {
|
||||
return (
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<TooltipProvider>
|
||||
{children}
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
@ -1,55 +1,8 @@
|
||||
import "@/app/globals.css";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { constructMetadata } from "@/lib/metadata";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { TailwindIndicator } from "@/components/tailwind-indicator";
|
||||
import { GeistSans } from "geist/font/sans";
|
||||
import { GeistMono } from "geist/font/mono";
|
||||
import { fontSourceSans, fontSourceSerif4 } from "@/assets/fonts";
|
||||
|
||||
export const metadata: Metadata = constructMetadata();
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
minimumScale: 1,
|
||||
maximumScale: 1,
|
||||
themeColor: [
|
||||
{ media: '(prefers-color-scheme: light)', color: 'white' },
|
||||
{ media: '(prefers-color-scheme: dark)', color: 'black' }
|
||||
]
|
||||
};
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
// Since we have a `not-found.tsx` page on the root, a layout file
|
||||
// is required, even if it's just passing children through.
|
||||
export default function RootLayout({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head />
|
||||
<body
|
||||
className={cn(
|
||||
"min-h-screen bg-background antialiased",
|
||||
GeistSans.className,
|
||||
fontSourceSerif4.variable,
|
||||
fontSourceSans.variable,
|
||||
GeistSans.variable,
|
||||
GeistMono.variable,
|
||||
)}
|
||||
>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
|
||||
<Toaster richColors position="top-right" offset={64} />
|
||||
|
||||
{/* <TailwindIndicator /> */}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
// Simply pass children through - the [locale] layout will handle the HTML structure
|
||||
return children;
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import { siteConfig } from '@/config/site';
|
||||
/**
|
||||
* Generates the Web App Manifest for the application
|
||||
*
|
||||
* TODO: https://github.com/amannn/next-intl/blob/main/examples/example-app-router/src/app/manifest.ts
|
||||
*
|
||||
* The manifest.json provides metadata used when the web app is installed on a
|
||||
* user's mobile device or desktop. See https://web.dev/add-manifest/
|
||||
*
|
||||
|
6
src/app/page.tsx
Normal file
6
src/app/page.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
// This page only renders when the app is built statically (output: 'export')
|
||||
export default function RootPage() {
|
||||
redirect('/en');
|
||||
}
|
8
src/i18n/navigation.ts
Normal file
8
src/i18n/navigation.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { createNavigation } from 'next-intl/navigation';
|
||||
import { routing } from '@/i18n/routing';
|
||||
|
||||
/**
|
||||
* https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing#i18n-navigation
|
||||
*/
|
||||
export const { Link, redirect, usePathname, useRouter, getPathname } =
|
||||
createNavigation(routing);
|
22
src/i18n/request.ts
Normal file
22
src/i18n/request.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { getRequestConfig } from 'next-intl/server';
|
||||
import { routing } from './routing';
|
||||
|
||||
export default getRequestConfig(async ({ requestLocale }) => {
|
||||
// This typically corresponds to the `[locale]` segment
|
||||
let locale = await requestLocale;
|
||||
|
||||
// Ensure that the incoming `locale` is valid
|
||||
if (!locale || !routing.locales.includes(locale as any)) {
|
||||
locale = routing.defaultLocale;
|
||||
}
|
||||
|
||||
return {
|
||||
locale,
|
||||
messages: (
|
||||
await (locale === 'en'
|
||||
? // When using Turbopack, this will enable HMR for `en`
|
||||
import('../../messages/en.json')
|
||||
: import(`../../messages/${locale}.json`))
|
||||
).default
|
||||
};
|
||||
});
|
19
src/i18n/routing.ts
Normal file
19
src/i18n/routing.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { defineRouting } from 'next-intl/routing';
|
||||
|
||||
/**
|
||||
* https://next-intl.dev/docs/getting-started/app-router/with-i18n-routing#i18n-routing
|
||||
*/
|
||||
export const routing = defineRouting({
|
||||
// A list of all locales that are supported
|
||||
locales: ['en', 'cn'],
|
||||
|
||||
// Used when no locale matches
|
||||
defaultLocale: 'en',
|
||||
|
||||
pathnames: {
|
||||
'/': '/'
|
||||
}
|
||||
});
|
||||
|
||||
export type Pathnames = keyof typeof routing.pathnames;
|
||||
export type Locale = (typeof routing.locales)[number];
|
19
src/middleware.ts
Normal file
19
src/middleware.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import createMiddleware from 'next-intl/middleware';
|
||||
import { routing } from '@/i18n/routing';
|
||||
|
||||
export default createMiddleware(routing);
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
// Enable a redirect to a matching locale at the root
|
||||
'/',
|
||||
|
||||
// Set a cookie to remember the previous locale for
|
||||
// all requests that have a locale prefix
|
||||
'/(cn|en)/:path*',
|
||||
|
||||
// Enable redirects that add missing locales
|
||||
// (e.g. `/pathnames` -> `/en/pathnames`)
|
||||
'/((?!_next|_vercel|.*\\..*).*)'
|
||||
]
|
||||
};
|
29
src/sitemap.ts
Normal file
29
src/sitemap.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { MetadataRoute } from 'next';
|
||||
import { routing, Locale } from '@/i18n/routing';
|
||||
import { getPathname } from '@/i18n/navigation';
|
||||
import { siteConfig } from './config/site';
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
return [...getEntries('/')];
|
||||
}
|
||||
|
||||
type Href = Parameters<typeof getPathname>[0]['href'];
|
||||
|
||||
/**
|
||||
* https://github.com/amannn/next-intl/blob/main/examples/example-app-router/src/app/sitemap.ts
|
||||
*/
|
||||
function getEntries(href: Href) {
|
||||
return routing.locales.map((locale) => ({
|
||||
url: getUrl(href, locale),
|
||||
alternates: {
|
||||
languages: Object.fromEntries(
|
||||
routing.locales.map((cur) => [cur, getUrl(href, cur)])
|
||||
)
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function getUrl(href: Href, locale: Locale) {
|
||||
const pathname = getPathname({ locale, href });
|
||||
return siteConfig.url + pathname;
|
||||
}
|
Loading…
Reference in New Issue
Block a user