| @@ -2,7 +2,9 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next | |||
| ## Setting Up the Environment | |||
| It is recommended to use the same node and npm versions for development. An easy way to do so would be to use `nvm`. After installing `nvm`, run: | |||
| It is recommended to use the same node and npm versions for development. An easy way to do so would be to use `nvm` ([Linux/MacOS](https://github.com/nvm-sh/nvm), [Windows](https://github.com/coreybutler/nvm-windows)). | |||
| After installing `nvm`, run: | |||
| ```bash | |||
| nvm use | |||
| @@ -21,21 +23,10 @@ npm run dev | |||
| Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. | |||
| You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. | |||
| This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. | |||
| ## Learn More | |||
| To learn more about Next.js, take a look at the following resources: | |||
| - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. | |||
| - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. | |||
| You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! | |||
| ## Deploy on Vercel | |||
| The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. | |||
| ## References | |||
| Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. | |||
| This project uses the following libraries: | |||
| - [NextJS](https://nextjs.org/docs) | |||
| - [Next-Auth](https://next-auth.js.org/getting-started/example) | |||
| - [Material UI](https://mui.com/material-ui/getting-started/) | |||
| - [i18next](https://www.i18next.com/overview/getting-started) | |||
| @@ -16,11 +16,15 @@ | |||
| "@mui/icons-material": "^5.15.0", | |||
| "@mui/material": "^5.15.0", | |||
| "@mui/material-nextjs": "^5.15.0", | |||
| "@unly/universal-language-detector": "^2.0.3", | |||
| "i18next": "^23.7.11", | |||
| "i18next-resources-to-backend": "^1.2.0", | |||
| "next": "14.0.4", | |||
| "next-auth": "^4.24.5", | |||
| "react": "^18", | |||
| "react-dom": "^18", | |||
| "react-hook-form": "^7.49.2" | |||
| "react-hook-form": "^7.49.2", | |||
| "react-i18next": "^13.5.0" | |||
| }, | |||
| "devDependencies": { | |||
| "@types/node": "^20", | |||
| @@ -1068,6 +1072,43 @@ | |||
| "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", | |||
| "dev": true | |||
| }, | |||
| "node_modules/@unly/iso3166-1": { | |||
| "version": "1.0.2", | |||
| "resolved": "https://registry.npmjs.org/@unly/iso3166-1/-/iso3166-1-1.0.2.tgz", | |||
| "integrity": "sha512-aL/7cmcfjpwOaKFr9XHcWP/Z7lQjKLm5NMcjncT96aeSJxfblmPLnH/8lnX0GrWWFA2/CseCxnA73u5eiZcClA==" | |||
| }, | |||
| "node_modules/@unly/universal-language-detector": { | |||
| "version": "2.0.3", | |||
| "resolved": "https://registry.npmjs.org/@unly/universal-language-detector/-/universal-language-detector-2.0.3.tgz", | |||
| "integrity": "sha512-UMmZIQS2XfJko8/GOrJc9ojO4UIF9HJM59NyQgcH+Ncrb0WBHL2L7EZ/7iZ/YcdoRnioonRECUQmmUELWt9z5Q==", | |||
| "dependencies": { | |||
| "@unly/iso3166-1": "1.0.2", | |||
| "@unly/utils": "1.0.3", | |||
| "accept-language-parser": "1.5.0", | |||
| "i18next": "19.0.2", | |||
| "i18next-browser-languagedetector": "4.0.1", | |||
| "lodash.get": "4.4.2", | |||
| "lodash.includes": "4.3.0" | |||
| } | |||
| }, | |||
| "node_modules/@unly/universal-language-detector/node_modules/i18next": { | |||
| "version": "19.0.2", | |||
| "resolved": "https://registry.npmjs.org/i18next/-/i18next-19.0.2.tgz", | |||
| "integrity": "sha512-fBa43Ann2udP1CQAz3IQpOZ1dGAkmi3mMfzisOhH17igneSRbvZ7P2RNbL+L1iRYKMufBmVwnC7G3gqcyviZ9g==", | |||
| "dependencies": { | |||
| "@babel/runtime": "^7.3.1" | |||
| } | |||
| }, | |||
| "node_modules/@unly/utils": { | |||
| "version": "1.0.3", | |||
| "resolved": "https://registry.npmjs.org/@unly/utils/-/utils-1.0.3.tgz", | |||
| "integrity": "sha512-QTRknIDX56FvzGcIpBum5D/oRSlX3dkZ+l1op1jsFlYCTd925OGUb991V7zsFv3ePcqFfvfqfR5cNVv+w4JAOw==" | |||
| }, | |||
| "node_modules/accept-language-parser": { | |||
| "version": "1.5.0", | |||
| "resolved": "https://registry.npmjs.org/accept-language-parser/-/accept-language-parser-1.5.0.tgz", | |||
| "integrity": "sha512-QhyTbMLYo0BBGg1aWbeMG4ekWtds/31BrEU+DONOg/7ax23vxpL03Pb7/zBmha2v7vdD3AyzZVWBVGEZxKOXWw==" | |||
| }, | |||
| "node_modules/acorn": { | |||
| "version": "8.11.2", | |||
| "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", | |||
| @@ -3003,6 +3044,14 @@ | |||
| "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", | |||
| "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" | |||
| }, | |||
| "node_modules/html-parse-stringify": { | |||
| "version": "3.0.1", | |||
| "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", | |||
| "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", | |||
| "dependencies": { | |||
| "void-elements": "3.1.0" | |||
| } | |||
| }, | |||
| "node_modules/human-signals": { | |||
| "version": "4.3.1", | |||
| "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", | |||
| @@ -3012,6 +3061,44 @@ | |||
| "node": ">=14.18.0" | |||
| } | |||
| }, | |||
| "node_modules/i18next": { | |||
| "version": "23.7.11", | |||
| "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.7.11.tgz", | |||
| "integrity": "sha512-A/vOkw8vY99YHU9A1Td3I1dcTiYaPnwBWzrpVzfXUXSYgogK3cmBcmop/0cnXPc6QpUWIyqaugKNxRUEZVk9Nw==", | |||
| "funding": [ | |||
| { | |||
| "type": "individual", | |||
| "url": "https://locize.com" | |||
| }, | |||
| { | |||
| "type": "individual", | |||
| "url": "https://locize.com/i18next.html" | |||
| }, | |||
| { | |||
| "type": "individual", | |||
| "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" | |||
| } | |||
| ], | |||
| "dependencies": { | |||
| "@babel/runtime": "^7.23.2" | |||
| } | |||
| }, | |||
| "node_modules/i18next-browser-languagedetector": { | |||
| "version": "4.0.1", | |||
| "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-4.0.1.tgz", | |||
| "integrity": "sha512-RxSoX6mB8cab0CTIQ+klCS764vYRj+Jk621cnFVsINvcdlb/cdi3vQFyrPwmnowB7ReUadjHovgZX+RPIzHVQQ==", | |||
| "dependencies": { | |||
| "@babel/runtime": "^7.5.5" | |||
| } | |||
| }, | |||
| "node_modules/i18next-resources-to-backend": { | |||
| "version": "1.2.0", | |||
| "resolved": "https://registry.npmjs.org/i18next-resources-to-backend/-/i18next-resources-to-backend-1.2.0.tgz", | |||
| "integrity": "sha512-8f1l03s+QxDmCfpSXCh9V+AFcxAwIp0UaroWuyOx+hmmv8484GcELHs+lnu54FrNij8cDBEXvEwhzZoXsKcVpg==", | |||
| "dependencies": { | |||
| "@babel/runtime": "^7.23.2" | |||
| } | |||
| }, | |||
| "node_modules/ignore": { | |||
| "version": "5.3.0", | |||
| "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", | |||
| @@ -3610,6 +3697,16 @@ | |||
| "url": "https://github.com/sponsors/sindresorhus" | |||
| } | |||
| }, | |||
| "node_modules/lodash.get": { | |||
| "version": "4.4.2", | |||
| "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", | |||
| "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" | |||
| }, | |||
| "node_modules/lodash.includes": { | |||
| "version": "4.3.0", | |||
| "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", | |||
| "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" | |||
| }, | |||
| "node_modules/lodash.merge": { | |||
| "version": "4.6.2", | |||
| "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", | |||
| @@ -4422,6 +4519,27 @@ | |||
| "react": "^16.8.0 || ^17 || ^18" | |||
| } | |||
| }, | |||
| "node_modules/react-i18next": { | |||
| "version": "13.5.0", | |||
| "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-13.5.0.tgz", | |||
| "integrity": "sha512-CFJ5NDGJ2MUyBohEHxljOq/39NQ972rh1ajnadG9BjTk+UXbHLq4z5DKEbEQBDoIhUmmbuS/fIMJKo6VOax1HA==", | |||
| "dependencies": { | |||
| "@babel/runtime": "^7.22.5", | |||
| "html-parse-stringify": "^3.0.1" | |||
| }, | |||
| "peerDependencies": { | |||
| "i18next": ">= 23.2.3", | |||
| "react": ">= 16.8.0" | |||
| }, | |||
| "peerDependenciesMeta": { | |||
| "react-dom": { | |||
| "optional": true | |||
| }, | |||
| "react-native": { | |||
| "optional": true | |||
| } | |||
| } | |||
| }, | |||
| "node_modules/react-is": { | |||
| "version": "18.2.0", | |||
| "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", | |||
| @@ -5087,6 +5205,14 @@ | |||
| "uuid": "dist/bin/uuid" | |||
| } | |||
| }, | |||
| "node_modules/void-elements": { | |||
| "version": "3.1.0", | |||
| "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", | |||
| "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", | |||
| "engines": { | |||
| "node": ">=0.10.0" | |||
| } | |||
| }, | |||
| "node_modules/watchpack": { | |||
| "version": "2.4.0", | |||
| "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", | |||
| @@ -17,11 +17,15 @@ | |||
| "@mui/icons-material": "^5.15.0", | |||
| "@mui/material": "^5.15.0", | |||
| "@mui/material-nextjs": "^5.15.0", | |||
| "@unly/universal-language-detector": "^2.0.3", | |||
| "i18next": "^23.7.11", | |||
| "i18next-resources-to-backend": "^1.2.0", | |||
| "next": "14.0.4", | |||
| "next-auth": "^4.24.5", | |||
| "react": "^18", | |||
| "react-dom": "^18", | |||
| "react-hook-form": "^7.49.2" | |||
| "react-hook-form": "^7.49.2", | |||
| "react-i18next": "^13.5.0" | |||
| }, | |||
| "devDependencies": { | |||
| "@types/node": "^20", | |||
| @@ -1,4 +1,4 @@ | |||
| const Dashboard: React.FC = () => { | |||
| const Dashboard: React.FC = async () => { | |||
| return "Dashboard (protected)"; | |||
| }; | |||
| @@ -1,5 +1,5 @@ | |||
| import type { Metadata } from "next"; | |||
| import { detectLanguage } from "@/i18n"; | |||
| import ThemeRegistry from "@/theme/ThemeRegistry"; | |||
| export const metadata: Metadata = { | |||
| @@ -7,13 +7,15 @@ export const metadata: Metadata = { | |||
| description: "Generated by create next app", | |||
| }; | |||
| export default function RootLayout({ | |||
| export default async function RootLayout({ | |||
| children, | |||
| }: { | |||
| children: React.ReactNode; | |||
| }) { | |||
| const lang = await detectLanguage(); | |||
| return ( | |||
| <html lang="en"> | |||
| <html lang={lang}> | |||
| <body> | |||
| <ThemeRegistry>{children}</ThemeRegistry> | |||
| </body> | |||
| @@ -1,9 +1,8 @@ | |||
| import Grid from "@mui/material/Grid"; | |||
| import Paper from "@mui/material/Paper"; | |||
| import LoginForm from "./LoginForm"; | |||
| import { getServerSession } from "next-auth"; | |||
| import { redirect } from "next/navigation"; | |||
| import { authOptions } from "@/config/authConfig"; | |||
| import { I18nProvider } from "@/i18n"; | |||
| import LoginPage from "@/components/LoginPage/LoginPage"; | |||
| const Login: React.FC = async () => { | |||
| const session = await getServerSession(authOptions); | |||
| @@ -12,16 +11,9 @@ const Login: React.FC = async () => { | |||
| } | |||
| return ( | |||
| <Grid container height="100vh"> | |||
| <Grid item sm> | |||
| Hero | |||
| </Grid> | |||
| <Grid item xs={12} sm={8} lg={5}> | |||
| <Paper square sx={{ height: "100%" }}> | |||
| <LoginForm /> | |||
| </Paper> | |||
| </Grid> | |||
| </Grid> | |||
| <I18nProvider namespaces={["login"]}> | |||
| <LoginPage /> | |||
| </I18nProvider> | |||
| ); | |||
| }; | |||
| @@ -5,10 +5,12 @@ import Button from "@mui/material/Button"; | |||
| import Stack from "@mui/material/Stack"; | |||
| import TextField from "@mui/material/TextField"; | |||
| import Typography from "@mui/material/Typography"; | |||
| import { TFunction } from "i18next"; | |||
| import { signIn } from "next-auth/react"; | |||
| import { useRouter } from "next/navigation"; | |||
| import { useState } from "react"; | |||
| import { SubmitHandler, useForm } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| type LoginFields = { | |||
| username: string; | |||
| @@ -16,17 +18,21 @@ type LoginFields = { | |||
| }; | |||
| // Error codes in https://next-auth.js.org/configuration/pages#sign-in-page | |||
| const getHumanFriendlyErrorMessage = (serverError: string): string => { | |||
| const getHumanFriendlyErrorMessage = ( | |||
| t: TFunction, | |||
| serverError: string, | |||
| ): string => { | |||
| switch (serverError) { | |||
| case "CredentialsSignin": | |||
| return "Invalid username or password."; | |||
| return t("Invalid username or password."); | |||
| case "Default": | |||
| default: | |||
| return "Something went wrong. Please try again later."; | |||
| return t("Something went wrong. Please try again later."); | |||
| } | |||
| }; | |||
| const LoginForm: React.FC = () => { | |||
| const { t } = useTranslation("login"); | |||
| const { | |||
| register, | |||
| handleSubmit, | |||
| @@ -60,23 +66,23 @@ const LoginForm: React.FC = () => { | |||
| component="form" | |||
| onSubmit={handleSubmit(onSubmit)} | |||
| > | |||
| <Typography variant="h1">Sign In</Typography> | |||
| <Typography variant="h1">{t("Sign In")}</Typography> | |||
| <TextField | |||
| label="Username" | |||
| {...register("username", { required: "Please enter a username" })} | |||
| label={t("Username")} | |||
| {...register("username", { required: t("Please enter a username") })} | |||
| error={Boolean(errors.username)} | |||
| helperText={errors.username?.message} | |||
| /> | |||
| <TextField | |||
| label="Password" | |||
| label={t("Password")} | |||
| type="password" | |||
| {...register("password", { required: "Please enter a password" })} | |||
| {...register("password", { required: t("Please enter a password") })} | |||
| error={Boolean(errors.password)} | |||
| helperText={errors.password?.message} | |||
| /> | |||
| {serverError && ( | |||
| <FormHelperText error> | |||
| {getHumanFriendlyErrorMessage(serverError)} | |||
| {getHumanFriendlyErrorMessage(t, serverError)} | |||
| </FormHelperText> | |||
| )} | |||
| <Button | |||
| @@ -85,7 +91,7 @@ const LoginForm: React.FC = () => { | |||
| type="submit" | |||
| disabled={isSubmitting} | |||
| > | |||
| Login | |||
| {t("Login")} | |||
| </Button> | |||
| </Stack> | |||
| ); | |||
| @@ -0,0 +1,20 @@ | |||
| import Grid from "@mui/material/Grid"; | |||
| import Paper from "@mui/material/Paper"; | |||
| import LoginForm from "./LoginForm"; | |||
| const LoginPage = () => { | |||
| return ( | |||
| <Grid container height="100vh"> | |||
| <Grid item sm> | |||
| test | |||
| </Grid> | |||
| <Grid item xs={12} sm={8} lg={5}> | |||
| <Paper square sx={{ height: "100%" }}> | |||
| <LoginForm /> | |||
| </Paper> | |||
| </Grid> | |||
| </Grid> | |||
| ); | |||
| }; | |||
| export default LoginPage; | |||
| @@ -0,0 +1 @@ | |||
| export { default } from "./LoginPage"; | |||
| @@ -1,7 +1,12 @@ | |||
| import { AuthOptions } from "next-auth"; | |||
| import { AuthOptions, Session } from "next-auth"; | |||
| import CredentialsProvider from "next-auth/providers/credentials"; | |||
| import { LOGIN_API_PATH } from "./api"; | |||
| interface SessionWithTokens extends Session { | |||
| accessToken?: string; | |||
| refreshToken?: string; | |||
| } | |||
| export const authOptions: AuthOptions = { | |||
| debug: process.env.NODE_ENV === "development", | |||
| providers: [ | |||
| @@ -33,10 +38,21 @@ export const authOptions: AuthOptions = { | |||
| }, | |||
| callbacks: { | |||
| jwt(params) { | |||
| return params.token; | |||
| // Add the data from user to the token | |||
| const { token, user } = params; | |||
| const newToken = { ...token, ...user }; | |||
| return newToken; | |||
| }, | |||
| session(params) { | |||
| return params.session; | |||
| session({ session, token }) { | |||
| const sessionWithToken: SessionWithTokens = { | |||
| ...session, | |||
| // Add the data from the token to the session | |||
| accessToken: token.accessToken as string | undefined, | |||
| refreshToken: token.refreshToken as string | undefined, | |||
| }; | |||
| return sessionWithToken; | |||
| }, | |||
| }, | |||
| }; | |||
| @@ -0,0 +1,43 @@ | |||
| "use client"; | |||
| import { createInstance, i18n } from "i18next"; | |||
| import React, { useMemo } from "react"; | |||
| import { I18nextProvider } from "react-i18next"; | |||
| interface Props { | |||
| children: React.ReactNode; | |||
| language: string; | |||
| resources: { [ns: string]: any }; | |||
| namespaces: string[]; | |||
| } | |||
| export const I18nContext = React.createContext(null); | |||
| const I18nProvider: React.FC<Props> = ({ | |||
| children, | |||
| language, | |||
| resources, | |||
| namespaces, | |||
| }) => { | |||
| const i18n = useMemo<i18n>(() => { | |||
| const instance = createInstance(); | |||
| instance.init({ | |||
| resources: { | |||
| [language]: resources, | |||
| }, | |||
| fallbackLng: language, | |||
| interpolation: { | |||
| escapeValue: false, | |||
| }, | |||
| ns: namespaces, | |||
| }); | |||
| return instance as i18n; | |||
| // No need to check dependencies since this | |||
| // should only be created once from the server | |||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||
| }, []); | |||
| return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>; | |||
| }; | |||
| export default I18nProvider; | |||
| @@ -0,0 +1 @@ | |||
| {} | |||
| @@ -0,0 +1,3 @@ | |||
| { | |||
| "test": "abc" | |||
| } | |||
| @@ -0,0 +1,92 @@ | |||
| import { cookies, headers } from "next/headers"; | |||
| import { createInstance, i18n, LanguageDetectorAsyncModule } from "i18next"; | |||
| import resourcesToBackend from "i18next-resources-to-backend"; | |||
| import { getServerSession } from "next-auth"; | |||
| import { authOptions } from "@/config/authConfig"; | |||
| import I18nClientProvider from "./I18nClientProvider"; | |||
| import universalLanguageDetect from "@unly/universal-language-detector"; | |||
| const FALLBACK_LANG = "en"; | |||
| const SUPPORTED_LANGUAGES = ["en", "zh"]; | |||
| export const detectLanguage = async (): Promise<string> => { | |||
| // Logic to get language preference from cookies/headers/session | |||
| const cookiesList = cookies(); | |||
| const cookiesObj = cookiesList | |||
| .getAll() | |||
| .reduce<{ [name: string]: string }>( | |||
| (acc, cookie) => ({ ...acc, [cookie.name]: cookie.value }), | |||
| {}, | |||
| ); | |||
| const headersList = headers(); | |||
| const session = await getServerSession(authOptions); | |||
| const lang = universalLanguageDetect({ | |||
| supportedLanguages: SUPPORTED_LANGUAGES, | |||
| fallbackLanguage: FALLBACK_LANG, | |||
| acceptLanguageHeader: headersList.get("accept-language") || undefined, | |||
| serverCookies: cookiesObj, | |||
| }); | |||
| return lang; | |||
| }; | |||
| const languageDetector: LanguageDetectorAsyncModule = { | |||
| type: "languageDetector", | |||
| detect: detectLanguage, | |||
| async: true, | |||
| }; | |||
| const initI18next = async (namespaces: string[]): Promise<i18n> => { | |||
| const i18nInstance = createInstance(); | |||
| await i18nInstance | |||
| .use(languageDetector) | |||
| .use( | |||
| resourcesToBackend((language: string, namespace: string) => { | |||
| return import(`./${language}/${namespace}.json`); | |||
| }), | |||
| ) | |||
| .init({ | |||
| fallbackLng: "en", | |||
| interpolation: { | |||
| escapeValue: false, | |||
| }, | |||
| ns: namespaces, | |||
| }); | |||
| return i18nInstance as i18n; | |||
| }; | |||
| export const getServerI18n = async (...namespaces: string[]) => { | |||
| return initI18next(namespaces); | |||
| }; | |||
| interface Props { | |||
| children: React.ReactNode; | |||
| namespaces: string[]; | |||
| } | |||
| // Provides the resources for the client | |||
| export const I18nProvider: React.FC<Props> = async ({ | |||
| children, | |||
| namespaces, | |||
| }) => { | |||
| const i18n = await getServerI18n(...namespaces); | |||
| const language = i18n.language; | |||
| const resources = namespaces.reduce<{ [ns: string]: any }>( | |||
| (acc, ns) => ({ | |||
| ...acc, | |||
| [ns]: i18n.getResourceBundle(language, ns), | |||
| }), | |||
| {}, | |||
| ); | |||
| return ( | |||
| <I18nClientProvider | |||
| language={language} | |||
| resources={resources} | |||
| namespaces={namespaces} | |||
| > | |||
| {children} | |||
| </I18nClientProvider> | |||
| ); | |||
| }; | |||
| @@ -0,0 +1,10 @@ | |||
| { | |||
| "Invalid username or password.": "帳號或密碼錯誤。", | |||
| "Something went wrong. Please try again later.": "出了些問題。請稍後再試。", | |||
| "Username": "帳號", | |||
| "Password": "密碼", | |||
| "Please enter a username": "請輸入帳號", | |||
| "Please enter a password": "請輸入密碼", | |||
| "Login": "登入", | |||
| "Sign In": "登入" | |||
| } | |||
| @@ -0,0 +1,3 @@ | |||
| { | |||
| "test": "def" | |||
| } | |||
| @@ -1,8 +1,30 @@ | |||
| import { withAuth } from "next-auth/middleware"; | |||
| import { NextRequestWithAuth, withAuth } from "next-auth/middleware"; | |||
| import { authOptions } from "@/config/authConfig"; | |||
| import { NextFetchEvent, NextResponse } from "next/server"; | |||
| export default withAuth({ | |||
| const PUBLIC_ROUTES = ["/login"]; | |||
| const LANG_QUERY_PARAM = "lang"; | |||
| const authMiddleware = withAuth({ | |||
| pages: authOptions.pages, | |||
| }); | |||
| export const config = { matcher: ["/dashboard"] }; | |||
| export default async function middleware( | |||
| req: NextRequestWithAuth, | |||
| event: NextFetchEvent, | |||
| ) { | |||
| const langPref = req.nextUrl.searchParams.get(LANG_QUERY_PARAM); | |||
| if (langPref) { | |||
| // Redirect to same url without the lang query param + set cookies | |||
| const newUrl = new URL(req.nextUrl); | |||
| newUrl.searchParams.delete(LANG_QUERY_PARAM); | |||
| const response = NextResponse.redirect(newUrl); | |||
| response.cookies.set("i18next", langPref); | |||
| return response; | |||
| } | |||
| // Matcher for using the auth middleware | |||
| return PUBLIC_ROUTES.some((route) => req.nextUrl.pathname.startsWith(route)) | |||
| ? NextResponse.next() // Return normal response | |||
| : await authMiddleware(req, event); // Let auth middleware handle response | |||
| } | |||