diff --git a/messages/cn.json b/messages/cn.json
new file mode 100644
index 0000000..b2c1aa7
--- /dev/null
+++ b/messages/cn.json
@@ -0,0 +1,12 @@
+{
+ "HomePage": {
+ "title": "你好,世界!",
+ "about": "去关于页面"
+ },
+ "AboutPage": {
+ "title": "关于"
+ },
+ "BlogPage": {
+ "title": "博客"
+ }
+}
\ No newline at end of file
diff --git a/messages/en.json b/messages/en.json
new file mode 100644
index 0000000..5509ccf
--- /dev/null
+++ b/messages/en.json
@@ -0,0 +1,12 @@
+{
+ "HomePage": {
+ "title": "Hello world!",
+ "about": "Go to the about page"
+ },
+ "AboutPage": {
+ "title": "About"
+ },
+ "BlogPage": {
+ "title": "Blog"
+ }
+}
\ No newline at end of file
diff --git a/next.config.ts b/next.config.ts
index 06bb143..28bee29 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -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));
diff --git a/package.json b/package.json
index bf4fae5..faad8ed 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a8b652b..ed8707b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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
diff --git a/src/app/(public)/(home)/page.tsx b/src/app/(public)/(home)/page.tsx
deleted file mode 100644
index 01d56ee..0000000
--- a/src/app/(public)/(home)/page.tsx
+++ /dev/null
@@ -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 (
- <>
- {/* */}
-
-
-
-
-
-
-
- {/*
*/}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/*
*/}
-
- {/*
*/}
-
- {/*
*/}
-
- {/*
*/}
-
- {/*
*/}
-
- {/*
*/}
-
- {/*
*/}
-
- {/*
*/}
-
- {/*
*/}
-
- {/*
*/}
-
- {/*
*/}
-
- {/*
*/}
-
- {/*
*/}
-
- {/*
*/}
-
- {/*
*/}
-
- >
- );
-}
diff --git a/src/app/(protected)/dashboard/page.tsx b/src/app/[locale]/(protected)/dashboard/page.tsx
similarity index 100%
rename from src/app/(protected)/dashboard/page.tsx
rename to src/app/[locale]/(protected)/dashboard/page.tsx
diff --git a/src/app/[locale]/(public)/(home)/page.tsx b/src/app/[locale]/(public)/(home)/page.tsx
new file mode 100644
index 0000000..c666993
--- /dev/null
+++ b/src/app/[locale]/(public)/(home)/page.tsx
@@ -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 (
+ <>
+ {/* */}
+
+
+
+
+
{t('title')}
+ {/* {t('about')} */}
+
+
+
+
+
+
+ {/*
*/}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/[locale]/(public)/about/page.tsx b/src/app/[locale]/(public)/about/page.tsx
new file mode 100644
index 0000000..1193679
--- /dev/null
+++ b/src/app/[locale]/(public)/about/page.tsx
@@ -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 (
+
+
{t('title')}
+
+ );
+}
diff --git a/src/app/(public)/blog/(blog)/category/[slug]/loading.tsx b/src/app/[locale]/(public)/blog/(blog)/category/[slug]/loading.tsx
similarity index 100%
rename from src/app/(public)/blog/(blog)/category/[slug]/loading.tsx
rename to src/app/[locale]/(public)/blog/(blog)/category/[slug]/loading.tsx
diff --git a/src/app/(public)/blog/(blog)/layout.tsx b/src/app/[locale]/(public)/blog/(blog)/layout.tsx
similarity index 100%
rename from src/app/(public)/blog/(blog)/layout.tsx
rename to src/app/[locale]/(public)/blog/(blog)/layout.tsx
diff --git a/src/app/(public)/blog/(blog)/loading.tsx b/src/app/[locale]/(public)/blog/(blog)/loading.tsx
similarity index 100%
rename from src/app/(public)/blog/(blog)/loading.tsx
rename to src/app/[locale]/(public)/blog/(blog)/loading.tsx
diff --git a/src/app/(public)/blog/(blog)/page.tsx b/src/app/[locale]/(public)/blog/(blog)/page.tsx
similarity index 100%
rename from src/app/(public)/blog/(blog)/page.tsx
rename to src/app/[locale]/(public)/blog/(blog)/page.tsx
diff --git a/src/app/(public)/blog/[...slug]/layout.tsx b/src/app/[locale]/(public)/blog/[...slug]/layout.tsx
similarity index 100%
rename from src/app/(public)/blog/[...slug]/layout.tsx
rename to src/app/[locale]/(public)/blog/[...slug]/layout.tsx
diff --git a/src/app/(public)/blog/[...slug]/loading.tsx b/src/app/[locale]/(public)/blog/[...slug]/loading.tsx
similarity index 100%
rename from src/app/(public)/blog/[...slug]/loading.tsx
rename to src/app/[locale]/(public)/blog/[...slug]/loading.tsx
diff --git a/src/app/(public)/blog/[...slug]/page.tsx b/src/app/[locale]/(public)/blog/[...slug]/page.tsx
similarity index 100%
rename from src/app/(public)/blog/[...slug]/page.tsx
rename to src/app/[locale]/(public)/blog/[...slug]/page.tsx
diff --git a/src/app/(public)/layout.tsx b/src/app/[locale]/(public)/layout.tsx
similarity index 66%
rename from src/app/(public)/layout.tsx
rename to src/app/[locale]/(public)/layout.tsx
index d454882..a5a6e99 100644
--- a/src/app/(public)/layout.tsx
+++ b/src/app/[locale]/(public)/layout.tsx
@@ -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 (
- {/*
*/}
-
+
{children}
diff --git a/src/app/[locale]/[...rest]/page.tsx b/src/app/[locale]/[...rest]/page.tsx
new file mode 100644
index 0000000..cc88c3a
--- /dev/null
+++ b/src/app/[locale]/[...rest]/page.tsx
@@ -0,0 +1,5 @@
+import { notFound } from 'next/navigation';
+
+export default function CatchAllPage() {
+ notFound();
+}
\ No newline at end of file
diff --git a/src/app/auth/error/page.tsx b/src/app/[locale]/auth/error/page.tsx
similarity index 100%
rename from src/app/auth/error/page.tsx
rename to src/app/[locale]/auth/error/page.tsx
diff --git a/src/app/auth/forgot-password/page.tsx b/src/app/[locale]/auth/forgot-password/page.tsx
similarity index 100%
rename from src/app/auth/forgot-password/page.tsx
rename to src/app/[locale]/auth/forgot-password/page.tsx
diff --git a/src/app/auth/layout.tsx b/src/app/[locale]/auth/layout.tsx
similarity index 100%
rename from src/app/auth/layout.tsx
rename to src/app/[locale]/auth/layout.tsx
diff --git a/src/app/auth/loading.tsx b/src/app/[locale]/auth/loading.tsx
similarity index 100%
rename from src/app/auth/loading.tsx
rename to src/app/[locale]/auth/loading.tsx
diff --git a/src/app/auth/login/page.tsx b/src/app/[locale]/auth/login/page.tsx
similarity index 100%
rename from src/app/auth/login/page.tsx
rename to src/app/[locale]/auth/login/page.tsx
diff --git a/src/app/auth/new-password/page.tsx b/src/app/[locale]/auth/new-password/page.tsx
similarity index 100%
rename from src/app/auth/new-password/page.tsx
rename to src/app/[locale]/auth/new-password/page.tsx
diff --git a/src/app/auth/register/page.tsx b/src/app/[locale]/auth/register/page.tsx
similarity index 100%
rename from src/app/auth/register/page.tsx
rename to src/app/[locale]/auth/register/page.tsx
diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx
new file mode 100644
index 0000000..8944279
--- /dev/null
+++ b/src/app/[locale]/layout.tsx
@@ -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 (
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/[locale]/providers.tsx b/src/app/[locale]/providers.tsx
new file mode 100644
index 0000000..91439a2
--- /dev/null
+++ b/src/app/[locale]/providers.tsx
@@ -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 (
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index bfdb3ab..923bc20 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -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 (
-
-
-
-
- {children}
-
-
-
- {/* */}
-
-
-
- );
+ // Simply pass children through - the [locale] layout will handle the HTML structure
+ return children;
}
diff --git a/src/app/manifest.ts b/src/app/manifest.ts
index 712f430..0a82b09 100644
--- a/src/app/manifest.ts
+++ b/src/app/manifest.ts
@@ -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/
*
diff --git a/src/app/page.tsx b/src/app/page.tsx
new file mode 100644
index 0000000..0ad8a7d
--- /dev/null
+++ b/src/app/page.tsx
@@ -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');
+}
\ No newline at end of file
diff --git a/src/i18n/navigation.ts b/src/i18n/navigation.ts
new file mode 100644
index 0000000..57bd1c3
--- /dev/null
+++ b/src/i18n/navigation.ts
@@ -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);
diff --git a/src/i18n/request.ts b/src/i18n/request.ts
new file mode 100644
index 0000000..f323835
--- /dev/null
+++ b/src/i18n/request.ts
@@ -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
+ };
+});
\ No newline at end of file
diff --git a/src/i18n/routing.ts b/src/i18n/routing.ts
new file mode 100644
index 0000000..fbfa2e2
--- /dev/null
+++ b/src/i18n/routing.ts
@@ -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];
diff --git a/src/middleware.ts b/src/middleware.ts
new file mode 100644
index 0000000..8299c5c
--- /dev/null
+++ b/src/middleware.ts
@@ -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|.*\\..*).*)'
+ ]
+};
\ No newline at end of file
diff --git a/src/sitemap.ts b/src/sitemap.ts
new file mode 100644
index 0000000..dc1dc5c
--- /dev/null
+++ b/src/sitemap.ts
@@ -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[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;
+}
\ No newline at end of file