diff --git a/apps/website/i18n/request.tsx b/apps/website/i18n/request.tsx new file mode 100644 index 000000000..714e7404a --- /dev/null +++ b/apps/website/i18n/request.tsx @@ -0,0 +1,12 @@ +import { notFound } from 'next/navigation' +import { getRequestConfig } from 'next-intl/server' +import { routing } from './routing' + +export default getRequestConfig(async ({ locale }) => { + // Validate that the incoming `locale` parameter is valid + if (!routing.locales.includes(locale as any)) notFound() + + return { + messages: (await import(`../locales/${locale}.json`)).default, + } +}) diff --git a/apps/website/i18n/routing.ts b/apps/website/i18n/routing.ts new file mode 100644 index 000000000..b647e4fed --- /dev/null +++ b/apps/website/i18n/routing.ts @@ -0,0 +1,16 @@ +import { defineRouting } from 'next-intl/routing' +import { createSharedPathnamesNavigation } from 'next-intl/navigation' + +export const routing = defineRouting({ + // A list of all locales that are supported + locales: ['en', 'zh-Hans'], + + // Used when no locale matches + defaultLocale: 'en', + localePrefix: 'as-needed', +}) + +// Lightweight wrappers around Next.js' navigation APIs +// that will consider the routing configuration +export const { Link, redirect, usePathname, useRouter } = + createSharedPathnamesNavigation(routing) diff --git a/apps/website/middleware.ts b/apps/website/middleware.ts new file mode 100644 index 000000000..5eb5b6046 --- /dev/null +++ b/apps/website/middleware.ts @@ -0,0 +1,9 @@ +import createMiddleware from 'next-intl/middleware'; +import {routing} from './i18n/routing'; + +export default createMiddleware(routing); + +export const config = { + // Match only internationalized pathnames + matcher: ['/', '/(zh-Hans|en)/:path*'] +}; \ No newline at end of file diff --git a/apps/website/next.config.js b/apps/website/next.config.js index 5ddbf3c97..5792410a3 100644 --- a/apps/website/next.config.js +++ b/apps/website/next.config.js @@ -1,11 +1,15 @@ +const createNextIntlPlugin = require('next-intl/plugin') + +const withNextIntl = createNextIntlPlugin() + /** @type {import('next').NextConfig} */ const nextConfig = { - eslint: { - ignoreDuringBuilds: true, - }, - typescript: { - ignoreBuildErrors: true, - }, -}; + eslint: { + ignoreDuringBuilds: true, + }, + typescript: { + ignoreBuildErrors: true, + }, +} -module.exports = nextConfig; +module.exports = withNextIntl(nextConfig) diff --git a/apps/website/package.json b/apps/website/package.json index 73f742452..3f9e15fbb 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -24,6 +24,7 @@ "framer-motion": "^11.0.24", "lucide-react": "0.364.0", "next": "14.2.2", + "next-intl": "^3.19.0", "react": "18.2.0", "react-dom": "18.2.0", "react-ga4": "^2.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a8e9e303..69d7f53c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -554,6 +554,9 @@ importers: next: specifier: 14.2.2 version: 14.2.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + next-intl: + specifier: ^3.19.0 + version: 3.19.0(next@14.2.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -1662,6 +1665,18 @@ packages: '@floating-ui/utils@0.2.5': resolution: {integrity: sha512-sTcG+QZ6fdEUObICavU+aB3Mp8HY4n14wYHdxK4fXjPmv3PXZZeY5RaguJmGyeH/CJQhX3fqKUtS4qc1LoHwhQ==} + '@formatjs/ecma402-abstract@2.0.0': + resolution: {integrity: sha512-rRqXOqdFmk7RYvj4khklyqzcfQl9vEL/usogncBHRZfZBDOwMGuSRNFl02fu5KGHXdbinju+YXyuR+Nk8xlr/g==} + + '@formatjs/fast-memoize@2.2.0': + resolution: {integrity: sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==} + + '@formatjs/icu-messageformat-parser@2.7.8': + resolution: {integrity: sha512-nBZJYmhpcSX0WeJ5SDYUkZ42AgR3xiyhNCsQweFx3cz/ULJjym8bHAzWKvG5e2+1XO98dBYC0fWeeAECAVSwLA==} + + '@formatjs/icu-skeleton-parser@1.8.2': + resolution: {integrity: sha512-k4ERKgw7aKGWJZgTarIcNEmvyTVD9FYh0mTrrBMHZ1b8hUu6iOJ4SzsZlo3UNAvHYa+PnvntIwRPt1/vy4nA9Q==} + '@formatjs/intl-localematcher@0.5.4': resolution: {integrity: sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==} @@ -5798,6 +5813,9 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + intl-messageformat@10.5.14: + resolution: {integrity: sha512-IjC6sI0X7YRjjyVH9aUgdftcmZK7WXdHeil4KwbjDnRWjnVitKpAx3rr6t6di1joFp5188VqKcobOPA6mCLG/w==} + invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} @@ -6634,6 +6652,12 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + next-intl@3.19.0: + resolution: {integrity: sha512-ciiHYBwR3ztoMdJZgFmt0LII7GYTsLA/MFt3y681q4Lw4fI5EYNCZSYb9XA/BIt3ZX5S1TLUP1uOERy1dIQvMg==} + peerDependencies: + next: ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + next-themes@0.2.1: resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} peerDependencies: @@ -8196,6 +8220,11 @@ packages: '@types/react': optional: true + use-intl@3.19.0: + resolution: {integrity: sha512-JOA73+YdtArxkvFKrneLAhH55m+x+sA9Wj8w8rYkHkHvcW/76w5T3szSmBG063vH3UqLoIKlsDVBBUfpkB7GMg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + use-resize-observer@9.1.0: resolution: {integrity: sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==} peerDependencies: @@ -9680,6 +9709,26 @@ snapshots: '@floating-ui/utils@0.2.5': {} + '@formatjs/ecma402-abstract@2.0.0': + dependencies: + '@formatjs/intl-localematcher': 0.5.4 + tslib: 2.6.3 + + '@formatjs/fast-memoize@2.2.0': + dependencies: + tslib: 2.6.3 + + '@formatjs/icu-messageformat-parser@2.7.8': + dependencies: + '@formatjs/ecma402-abstract': 2.0.0 + '@formatjs/icu-skeleton-parser': 1.8.2 + tslib: 2.6.3 + + '@formatjs/icu-skeleton-parser@1.8.2': + dependencies: + '@formatjs/ecma402-abstract': 2.0.0 + tslib: 2.6.3 + '@formatjs/intl-localematcher@0.5.4': dependencies: tslib: 2.6.3 @@ -14933,6 +14982,13 @@ snapshots: internmap@2.0.3: {} + intl-messageformat@10.5.14: + dependencies: + '@formatjs/ecma402-abstract': 2.0.0 + '@formatjs/fast-memoize': 2.2.0 + '@formatjs/icu-messageformat-parser': 2.7.8 + tslib: 2.6.3 + invariant@2.2.4: dependencies: loose-envify: 1.4.0 @@ -16017,6 +16073,14 @@ snapshots: neo-async@2.6.2: {} + next-intl@3.19.0(next@14.2.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0): + dependencies: + '@formatjs/intl-localematcher': 0.5.4 + negotiator: 0.6.3 + next: 14.2.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + use-intl: 3.19.0(react@18.2.0) + next-themes@0.2.1(next@14.2.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: next: 14.2.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -17797,6 +17861,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + use-intl@3.19.0(react@18.2.0): + dependencies: + '@formatjs/fast-memoize': 2.2.0 + intl-messageformat: 10.5.14 + react: 18.2.0 + use-resize-observer@9.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@juggle/resize-observer': 3.4.0