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 type { NextConfig } from "next";
|
||||||
|
import createNextIntlPlugin from 'next-intl/plugin';
|
||||||
import { withContentCollections } from "@content-collections/next";
|
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 = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
|
|
||||||
@ -13,11 +22,13 @@ const nextConfig: NextConfig = {
|
|||||||
images: {
|
images: {
|
||||||
// https://vercel.com/docs/image-optimization/managing-image-optimization-costs#minimizing-image-optimization-costs
|
// https://vercel.com/docs/image-optimization/managing-image-optimization-costs#minimizing-image-optimization-costs
|
||||||
// vercel has limits on image optimization, 1000 images per month
|
// 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
|
// 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"
|
// 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
|
// but dangerouslyAllowSVG is disabled
|
||||||
dangerouslyAllowSVG: true,
|
// dangerouslyAllowSVG: true,
|
||||||
|
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
@ -31,18 +42,13 @@ const nextConfig: NextConfig = {
|
|||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: "randomuser.me",
|
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
|
* withContentCollections must be the outermost plugin
|
||||||
export default withContentCollections(nextConfig);
|
*
|
||||||
|
* https://www.content-collections.dev/docs/quickstart/next
|
||||||
|
*/
|
||||||
|
export default withContentCollections(withNextIntl(nextConfig));
|
||||||
|
@ -42,6 +42,7 @@
|
|||||||
"mdast-util-toc": "^7.1.0",
|
"mdast-util-toc": "^7.1.0",
|
||||||
"motion": "^12.4.3",
|
"motion": "^12.4.3",
|
||||||
"next": "15.1.7",
|
"next": "15.1.7",
|
||||||
|
"next-intl": "^3.26.5",
|
||||||
"next-plausible": "^3.12.4",
|
"next-plausible": "^3.12.4",
|
||||||
"next-safe-action": "^7.10.4",
|
"next-safe-action": "^7.10.4",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
|
97
pnpm-lock.yaml
generated
97
pnpm-lock.yaml
generated
@ -107,6 +107,9 @@ importers:
|
|||||||
next:
|
next:
|
||||||
specifier: 15.1.7
|
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)
|
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:
|
next-plausible:
|
||||||
specifier: ^3.12.4
|
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)
|
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':
|
'@floating-ui/utils@0.2.9':
|
||||||
resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
|
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':
|
'@hexagon/base64@1.1.28':
|
||||||
resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
|
resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
|
||||||
|
|
||||||
@ -2063,6 +2084,9 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
decimal.js@10.5.0:
|
||||||
|
resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==}
|
||||||
|
|
||||||
decode-named-character-reference@1.0.2:
|
decode-named-character-reference@1.0.2:
|
||||||
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
|
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
|
||||||
|
|
||||||
@ -2494,6 +2518,9 @@ packages:
|
|||||||
inline-style-parser@0.2.4:
|
inline-style-parser@0.2.4:
|
||||||
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
|
resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==}
|
||||||
|
|
||||||
|
intl-messageformat@10.7.15:
|
||||||
|
resolution: {integrity: sha512-LRyExsEsefQSBjU2p47oAheoKz+EOJxSLDdjOaEjdriajfHsMXOmV/EhMvYSg9bAgCUHasuAC+mcUBe/95PfIg==}
|
||||||
|
|
||||||
is-alphabetical@2.0.1:
|
is-alphabetical@2.0.1:
|
||||||
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
|
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
|
||||||
|
|
||||||
@ -2855,6 +2882,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ==}
|
resolution: {integrity: sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ==}
|
||||||
engines: {node: ^18.0.0 || >=20.0.0}
|
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:
|
next-plausible@3.12.4:
|
||||||
resolution: {integrity: sha512-cD3+ixJxf8yBYvsideTxqli3fvrB7R4BXcvsNJz8Sm2X1QN039WfiXjCyNWkub4h5++rRs6fHhchUMnOuJokcg==}
|
resolution: {integrity: sha512-cD3+ixJxf8yBYvsideTxqli3fvrB7R4BXcvsNJz8Sm2X1QN039WfiXjCyNWkub4h5++rRs6fHhchUMnOuJokcg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -3595,6 +3632,11 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
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:
|
use-sidecar@1.1.3:
|
||||||
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
|
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -4183,6 +4225,36 @@ snapshots:
|
|||||||
|
|
||||||
'@floating-ui/utils@0.2.9': {}
|
'@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': {}
|
'@hexagon/base64@1.1.28': {}
|
||||||
|
|
||||||
'@hookform/resolvers@4.1.0(react-hook-form@7.54.2(react@19.0.0))':
|
'@hookform/resolvers@4.1.0(react-hook-form@7.54.2(react@19.0.0))':
|
||||||
@ -5262,6 +5334,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
decimal.js@10.5.0: {}
|
||||||
|
|
||||||
decode-named-character-reference@1.0.2:
|
decode-named-character-reference@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
character-entities: 2.0.2
|
character-entities: 2.0.2
|
||||||
@ -5775,6 +5849,13 @@ snapshots:
|
|||||||
|
|
||||||
inline-style-parser@0.2.4: {}
|
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-alphabetical@2.0.1: {}
|
||||||
|
|
||||||
is-alphanumerical@2.0.1:
|
is-alphanumerical@2.0.1:
|
||||||
@ -6396,6 +6477,16 @@ snapshots:
|
|||||||
|
|
||||||
nanostores@0.11.4: {}
|
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):
|
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:
|
dependencies:
|
||||||
next: 15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
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:
|
optionalDependencies:
|
||||||
'@types/react': 19.0.9
|
'@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):
|
use-sidecar@1.1.3(@types/react@19.0.9)(react@19.0.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
detect-node-es: 1.1.0
|
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 { Footer } from "@/components/layout/footer";
|
||||||
import { Navbar } from "@/components/marketing/navbar";
|
import { Navbar } from "@/components/marketing/navbar";
|
||||||
import { marketingConfig } from "@/config/marketing";
|
import { marketingConfig } from "@/config/marketing";
|
||||||
|
import { setRequestLocale } from "next-intl/server";
|
||||||
|
|
||||||
interface MarketingLayoutProps {
|
interface MarketingLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
params: { locale: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function MarketingLayout({
|
export default async function MarketingLayout({
|
||||||
children,
|
children,
|
||||||
|
params
|
||||||
}: MarketingLayoutProps) {
|
}: MarketingLayoutProps) {
|
||||||
|
const { locale } = params;
|
||||||
|
|
||||||
|
// Enable static rendering
|
||||||
|
setRequestLocale(locale);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-h-screen">
|
<div className="flex flex-col min-h-screen">
|
||||||
{/* <Navbar scroll={true} config={marketingConfig} /> */}
|
<Navbar scroll={true} config={marketingConfig} />
|
||||||
<Navbar scroll={false} config={marketingConfig} />
|
|
||||||
|
|
||||||
<main className="flex-1">{children}</main>
|
<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 { PropsWithChildren } from 'react';
|
||||||
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' }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// 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) {
|
export default function RootLayout({ children }: PropsWithChildren) {
|
||||||
return (
|
// Simply pass children through - the [locale] layout will handle the HTML structure
|
||||||
<html lang="en" suppressHydrationWarning>
|
return children;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ import { siteConfig } from '@/config/site';
|
|||||||
/**
|
/**
|
||||||
* Generates the Web App Manifest for the application
|
* 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
|
* 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/
|
* 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