From 255f50080a7226cf55355a3779a9e970fc315323 Mon Sep 17 00:00:00 2001 From: Sonny <24420064+Sonny93@users.noreply.github.com> Date: Sat, 11 Nov 2023 00:07:10 +0100 Subject: [PATCH] Add translation (#9) * feat(wip): translation * fix: some i18n errors + use ssr translation * fix: i18next implementation * fix: tsc errors * feat: i18n middleware * feat: translate link view home page * feat: translate quick actions * feat: translate search modal * feat: translate side menu * feat: native error boundary + translation * feat: translate error pages * feat: translate category forms * feat: translate link forms * refactor: LangSelector is no longer absolute by default --- middleware.ts | 47 ++ next-i18next.config.js | 10 + next.config.js | 3 + package-lock.json | 416 +++++++++++------- package.json | 10 +- public/locales/en/common.json | 33 ++ public/locales/en/home.json | 5 + public/locales/en/login.json | 5 + public/locales/fr/common.json | 33 ++ public/locales/fr/home.json | 5 + public/locales/fr/login.json | 5 + src/components/AppErrorBoundary.tsx | 22 - .../BlockWrapper/block-wrapper.module.scss | 1 + .../ErrorBoundary/ErrorBoundary.jsx | 48 ++ .../ErrorBoundary/error-boundary.module.scss | 29 ++ src/components/FormLayout.tsx | 10 +- src/components/LangSelector.tsx | 31 ++ src/components/Links/Links.tsx | 34 +- src/components/PageTransition.tsx | 11 + src/components/QuickActions/CreateItem.tsx | 9 +- src/components/QuickActions/EditItem.tsx | 9 +- src/components/QuickActions/RemoveItem.tsx | 9 +- src/components/QuickActions/Search.tsx | 1 - src/components/SearchModal/SearchList.tsx | 31 +- src/components/SearchModal/SearchListItem.tsx | 1 + src/components/SearchModal/SearchModal.tsx | 31 +- .../SideMenu/Categories/Categories.tsx | 10 +- .../SideMenu/Categories/CategoryItem.tsx | 4 +- .../SideMenu/Favorites/Favorites.tsx | 6 +- src/components/SideMenu/NavigationLinks.tsx | 20 +- src/components/SideMenu/SideMenu.tsx | 13 +- src/components/SideMenu/UserCard/UserCard.tsx | 14 +- src/hooks/useIsMobile.tsx | 8 + src/i18n/index.ts | 12 + src/i18n/resources.ts | 11 + src/pages/404.tsx | 13 +- src/pages/500.tsx | 13 +- src/pages/_app.tsx | 21 +- src/pages/category/create.tsx | 26 +- src/pages/category/edit/[cid].tsx | 25 +- src/pages/category/remove/[cid].tsx | 36 +- src/pages/index.tsx | 33 +- src/pages/link/create.tsx | 33 +- src/pages/link/edit/[lid].tsx | 33 +- src/pages/link/remove/[lid].tsx | 29 +- src/pages/login.tsx | 42 +- src/styles/error-page.module.scss | 18 +- src/styles/globals.scss | 6 + src/types/i18next.d.ts | 22 + src/{ => types}/types.d.ts | 0 src/utils/array.ts | 5 +- src/utils/link.ts | 8 - tsconfig.json | 5 +- 53 files changed, 896 insertions(+), 419 deletions(-) create mode 100644 middleware.ts create mode 100644 next-i18next.config.js create mode 100644 public/locales/en/common.json create mode 100644 public/locales/en/home.json create mode 100644 public/locales/en/login.json create mode 100644 public/locales/fr/common.json create mode 100644 public/locales/fr/home.json create mode 100644 public/locales/fr/login.json delete mode 100644 src/components/AppErrorBoundary.tsx create mode 100644 src/components/ErrorBoundary/ErrorBoundary.jsx create mode 100644 src/components/ErrorBoundary/error-boundary.module.scss create mode 100644 src/components/LangSelector.tsx create mode 100644 src/hooks/useIsMobile.tsx create mode 100644 src/i18n/index.ts create mode 100644 src/i18n/resources.ts create mode 100644 src/types/i18next.d.ts rename src/{ => types}/types.d.ts (100%) diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..c692534 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,47 @@ +import acceptLanguage from "accept-language"; +import { NextResponse } from "next/server"; +import { i18n } from "./next-i18next.config"; + +acceptLanguage.languages(i18n.locales); + +export const config = { + matcher: ["/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)"], +}; + +const cookieName = "i18next"; + +// Source : https://github.com/i18next/next-app-dir-i18next-example/blob/3d653a46ae33f46abc011b6186f7a4595b84129f/middleware.js +export function middleware(req) { + if ( + req.nextUrl.pathname.indexOf("icon") > -1 || + req.nextUrl.pathname.indexOf("chrome") > -1 + ) + return NextResponse.next(); + let lng; + if (req.cookies.has(cookieName)) + lng = acceptLanguage.get(req.cookies.get(cookieName).value); + if (!lng) lng = acceptLanguage.get(req.headers.get("Accept-Language")); + if (!lng) lng = i18n.defaultLocale; + + // Redirect if lng in path is not supported + if ( + !i18n.locales.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) && + !req.nextUrl.pathname.startsWith("/_next") + ) { + return NextResponse.redirect( + new URL(`/${lng}${req.nextUrl.pathname}`, req.url) + ); + } + + if (req.headers.has("referer")) { + const refererUrl = new URL(req.headers.get("referer")); + const lngInReferer = i18n.locales.find((l) => + refererUrl.pathname.startsWith(`/${l}`) + ); + const response = NextResponse.next(); + if (lngInReferer) response.cookies.set(cookieName, lngInReferer); + return response; + } + + return NextResponse.next(); +} diff --git a/next-i18next.config.js b/next-i18next.config.js new file mode 100644 index 0000000..cbdd0db --- /dev/null +++ b/next-i18next.config.js @@ -0,0 +1,10 @@ +/** @type {import('next-i18next').UserConfig} */ +module.exports = { + // debug: process.env.NODE_ENV === "development", + i18n: { + defaultLocale: "en", + locales: ["en", "fr"], + }, + reloadOnPrerender: process.env.NODE_ENV === "development", + returnNull: false, +}; diff --git a/next.config.js b/next.config.js index b6931cd..85cc5d9 100644 --- a/next.config.js +++ b/next.config.js @@ -1,5 +1,8 @@ +const { i18n } = require("./next-i18next.config"); + /** @type {import('next').NextConfig} */ const config = { + i18n, webpack(config) { config.module.rules.push({ test: /\.svg$/, diff --git a/package-lock.json b/package-lock.json index 9e734a0..93c1cab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,22 +8,24 @@ "dependencies": { "@prisma/client": "^5.5.2", "@svgr/webpack": "^8.1.0", + "accept-language": "^3.0.18", "axios": "^1.6.1", "framer-motion": "^10.16.4", - "next": "^14.0.1", + "i18next": "^23.7.1", + "next": "^14.0.2", "next-auth": "^4.24.4", + "next-i18next": "^15.0.0", "next-seo": "^6.4.0", "node-html-parser": "^6.1.11", "nprogress": "^0.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-error-boundary": "^4.0.11", "react-hotkeys-hook": "^4.4.1", + "react-i18next": "^13.3.1", "react-icons": "^4.11.0", "react-select": "^5.8.0", "sass": "^1.69.5", "sharp": "^0.32.6", - "toastr": "^2.1.4", "yup": "^1.3.2" }, "devDependencies": { @@ -33,7 +35,7 @@ "@typescript-eslint/eslint-plugin": "^6.10.0", "@typescript-eslint/parser": "^6.10.0", "eslint": "^8.53.0", - "eslint-config-next": "14.0.1", + "eslint-config-next": "14.0.2", "prisma": "^5.5.2", "typescript": "5.2.2" } @@ -60,11 +62,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -108,11 +111,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz", - "integrity": "sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dependencies": { - "@babel/types": "^7.21.4", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -241,9 +244,9 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "engines": { "node": ">=6.9.0" } @@ -260,23 +263,23 @@ } }, "node_modules/@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dependencies": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -397,28 +400,28 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } @@ -459,12 +462,12 @@ } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -472,9 +475,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", - "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "bin": { "parser": "bin/babel-parser.js" }, @@ -1659,42 +1662,42 @@ } }, "node_modules/@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.4.tgz", - "integrity": "sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dependencies": { - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.4", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.4", - "@babel/types": "^7.21.4", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1703,12 +1706,12 @@ } }, "node_modules/@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -2012,23 +2015,23 @@ } }, "node_modules/@next/env": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.1.tgz", - "integrity": "sha512-Ms8ZswqY65/YfcjrlcIwMPD7Rg/dVjdLapMcSHG26W6O67EJDF435ShW4H4LXi1xKO1oRc97tLXUpx8jpLe86A==" + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.2.tgz", + "integrity": "sha512-HAW1sljizEaduEOes/m84oUqeIDAUYBR1CDwu2tobNlNDFP3cSm9d6QsOsGeNlIppU1p/p1+bWbYCbvwjFiceA==" }, "node_modules/@next/eslint-plugin-next": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.0.1.tgz", - "integrity": "sha512-bLjJMwXdzvhnQOnxvHoTTUh/+PYk6FF/DCgHi4BXwXCINer+o1ZYfL9aVeezj/oI7wqGJOqwGIXrlBvPbAId3w==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.0.2.tgz", + "integrity": "sha512-APrYFsXfAhnysycqxHcpg6Y4i7Ukp30GzVSZQRKT3OczbzkqGjt33vNhScmgoOXYBU1CfkwgtXmNxdiwv1jKmg==", "dev": true, "dependencies": { "glob": "7.1.7" } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.1.tgz", - "integrity": "sha512-JyxnGCS4qT67hdOKQ0CkgFTp+PXub5W1wsGvIq98TNbF3YEIN7iDekYhYsZzc8Ov0pWEsghQt+tANdidITCLaw==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.2.tgz", + "integrity": "sha512-i+jQY0fOb8L5gvGvojWyZMfQoQtDVB2kYe7fufOEiST6sicvzI2W5/EXo4lX5bLUjapHKe+nFxuVv7BA+Pd7LQ==", "cpu": [ "arm64" ], @@ -2041,9 +2044,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.1.tgz", - "integrity": "sha512-625Z7bb5AyIzswF9hvfZWa+HTwFZw+Jn3lOBNZB87lUS0iuCYDHqk3ujuHCkiyPtSC0xFBtYDLcrZ11mF/ap3w==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.2.tgz", + "integrity": "sha512-zRCAO0d2hW6gBEa4wJaLn+gY8qtIqD3gYd9NjruuN98OCI6YyelmhWVVLlREjS7RYrm9OUQIp/iVJFeB6kP1hg==", "cpu": [ "x64" ], @@ -2056,9 +2059,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.1.tgz", - "integrity": "sha512-iVpn3KG3DprFXzVHM09kvb//4CNNXBQ9NB/pTm8LO+vnnnaObnzFdS5KM+w1okwa32xH0g8EvZIhoB3fI3mS1g==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.2.tgz", + "integrity": "sha512-tSJmiaon8YaKsVhi7GgRizZoV0N1Sx5+i+hFTrCKKQN7s3tuqW0Rov+RYdPhAv/pJl4qiG+XfSX4eJXqpNg3dA==", "cpu": [ "arm64" ], @@ -2071,9 +2074,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.1.tgz", - "integrity": "sha512-mVsGyMxTLWZXyD5sen6kGOTYVOO67lZjLApIj/JsTEEohDDt1im2nkspzfV5MvhfS7diDw6Rp/xvAQaWZTv1Ww==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.2.tgz", + "integrity": "sha512-dXJLMSEOwqJKcag1BeX1C+ekdPPJ9yXbWIt3nAadhbLx5CjACoB2NQj9Xcqu2tmdr5L6m34fR+fjGPs+ZVPLzA==", "cpu": [ "arm64" ], @@ -2086,9 +2089,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.1.tgz", - "integrity": "sha512-wMqf90uDWN001NqCM/auRl3+qVVeKfjJdT9XW+RMIOf+rhUzadmYJu++tp2y+hUbb6GTRhT+VjQzcgg/QTD9NQ==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.2.tgz", + "integrity": "sha512-WC9KAPSowj6as76P3vf1J3mf2QTm3Wv3FBzQi7UJ+dxWjK3MhHVWsWUo24AnmHx9qDcEtHM58okgZkXVqeLB+Q==", "cpu": [ "x64" ], @@ -2101,9 +2104,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.1.tgz", - "integrity": "sha512-ol1X1e24w4j4QwdeNjfX0f+Nza25n+ymY0T2frTyalVczUmzkVD7QGgPTZMHfR1aLrO69hBs0G3QBYaj22J5GQ==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.2.tgz", + "integrity": "sha512-KSSAwvUcjtdZY4zJFa2f5VNJIwuEVnOSlqYqbQIawREJA+gUI6egeiRu290pXioQXnQHYYdXmnVNZ4M+VMB7KQ==", "cpu": [ "x64" ], @@ -2116,9 +2119,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.1.tgz", - "integrity": "sha512-WEmTEeWs6yRUEnUlahTgvZteh5RJc4sEjCQIodJlZZ5/VJwVP8p2L7l6VhzQhT4h7KvLx/Ed4UViBdne6zpIsw==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.2.tgz", + "integrity": "sha512-2/O0F1SqJ0bD3zqNuYge0ok7OEWCQwk55RPheDYD0va5ij7kYwrFkq5ycCRN0TLjLfxSF6xI5NM6nC5ux7svEQ==", "cpu": [ "arm64" ], @@ -2131,9 +2134,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.1.tgz", - "integrity": "sha512-oFpHphN4ygAgZUKjzga7SoH2VGbEJXZa/KL8bHCAwCjDWle6R1SpiGOdUdA8EJ9YsG1TYWpzY6FTbUA+iAJeww==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.2.tgz", + "integrity": "sha512-vJI/x70Id0oN4Bq/R6byBqV1/NS5Dl31zC+lowO8SDu1fHmUxoAdILZR5X/sKbiJpuvKcCrwbYgJU8FF/Gh50Q==", "cpu": [ "ia32" ], @@ -2146,9 +2149,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.1.tgz", - "integrity": "sha512-FFp3nOJ/5qSpeWT0BZQ+YE1pSMk4IMpkME/1DwKBwhg4mJLB9L+6EXuJi4JEwaJdl5iN+UUlmUD3IsR1kx5fAg==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.2.tgz", + "integrity": "sha512-Ut4LXIUvC5m8pHTe2j0vq/YDnTEyq6RSR9vHYPqnELrDapPhLNz9Od/L5Ow3J8RNDWpEnfCiQXuVdfjlNEJ7ug==", "cpu": [ "x64" ], @@ -2570,6 +2573,15 @@ "node": ">=10.13.0" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2876,6 +2888,15 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/accept-language": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/accept-language/-/accept-language-3.0.18.tgz", + "integrity": "sha512-sUofgqBPzgfcF20sPoBYGQ1IhQLt2LSkxTnlQSuLF3n5gPEqd5AimbvOvHEi0T1kLMiGVqPWzI5a9OteBRth3A==", + "dependencies": { + "bcp47": "^1.1.2", + "stable": "^0.1.6" + } + }, "node_modules/acorn": { "version": "8.11.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", @@ -3229,6 +3250,14 @@ } ] }, + "node_modules/bcp47": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/bcp47/-/bcp47-1.1.2.tgz", + "integrity": "sha512-JnkkL4GUpOvvanH9AZPX38CxhiLsXMBicBY2IAtqiVN8YulGDQybUydWA4W6yAMtw6iShtw+8HEF6cfrTHU+UQ==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -3537,6 +3566,16 @@ "node": ">= 0.6" } }, + "node_modules/core-js": { + "version": "3.33.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.2.tgz", + "integrity": "sha512-XeBzWI6QL3nJQiHmdzbAOiMYqjrb7hwU7A39Qhvd/POSa/t9E1AeZyEZx3fNvp/vtM8zXwhoL0FsiS0hD0pruQ==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-js-compat": { "version": "3.27.2", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.27.2.tgz", @@ -4105,12 +4144,12 @@ } }, "node_modules/eslint-config-next": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.0.1.tgz", - "integrity": "sha512-QfIFK2WD39H4WOespjgf6PLv9Bpsd7KGGelCtmq4l67nGvnlsGpuvj0hIT+aIy6p5gKH+lAChYILsyDlxP52yg==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.0.2.tgz", + "integrity": "sha512-CasWThlsyIcg/a+clU6KVOMTieuDhTztsrqvniP6AsRki9v7FnojTa7vKQOYM8QSOsQdZ/aElLD1Y2Oc8/PsIg==", "dev": true, "dependencies": { - "@next/eslint-plugin-next": "14.0.1", + "@next/eslint-plugin-next": "14.0.2", "@rushstack/eslint-patch": "^1.3.3", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0", "eslint-import-resolver-node": "^0.3.6", @@ -5114,6 +5153,41 @@ "react-is": "^16.7.0" } }, + "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/i18next": { + "version": "23.7.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.7.1.tgz", + "integrity": "sha512-lD2lZkdhb9jnIGGc2ja8ER6cGStgJ+jFVL336Sa1C37//2Q8odC617ek2oafYbbs0/a+BbUqKe5JPST2r88UEQ==", + "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-fs-backend": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.2.0.tgz", + "integrity": "sha512-VOPHhdDX0M/csRqEw+9Ectpf6wvTIg1MZDfAHxc3JKnAlJz7fcZSAKAeyDohOq0xuLx57esYpJopIvBaRb0Bag==" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -5591,11 +5665,6 @@ "url": "https://github.com/sponsors/panva" } }, - "node_modules/jquery": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.1.tgz", - "integrity": "sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw==" - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5873,11 +5942,11 @@ "dev": true }, "node_modules/next": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/next/-/next-14.0.1.tgz", - "integrity": "sha512-s4YaLpE4b0gmb3ggtmpmV+wt+lPRuGtANzojMQ2+gmBpgX9w5fTbjsy6dXByBuENsdCX5pukZH/GxdFgO62+pA==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/next/-/next-14.0.2.tgz", + "integrity": "sha512-jsAU2CkYS40GaQYOiLl9m93RTv2DA/tTJ0NRlmZIBIL87YwQ/xR8k796z7IqgM3jydI8G25dXvyYMC9VDIevIg==", "dependencies": { - "@next/env": "14.0.1", + "@next/env": "14.0.2", "@swc/helpers": "0.5.2", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", @@ -5892,15 +5961,15 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.0.1", - "@next/swc-darwin-x64": "14.0.1", - "@next/swc-linux-arm64-gnu": "14.0.1", - "@next/swc-linux-arm64-musl": "14.0.1", - "@next/swc-linux-x64-gnu": "14.0.1", - "@next/swc-linux-x64-musl": "14.0.1", - "@next/swc-win32-arm64-msvc": "14.0.1", - "@next/swc-win32-ia32-msvc": "14.0.1", - "@next/swc-win32-x64-msvc": "14.0.1" + "@next/swc-darwin-arm64": "14.0.2", + "@next/swc-darwin-x64": "14.0.2", + "@next/swc-linux-arm64-gnu": "14.0.2", + "@next/swc-linux-arm64-musl": "14.0.2", + "@next/swc-linux-x64-gnu": "14.0.2", + "@next/swc-linux-x64-musl": "14.0.2", + "@next/swc-win32-arm64-msvc": "14.0.2", + "@next/swc-win32-ia32-msvc": "14.0.2", + "@next/swc-win32-x64-msvc": "14.0.2" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -5944,6 +6013,41 @@ } } }, + "node_modules/next-i18next": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-15.0.0.tgz", + "integrity": "sha512-9iGEU4dt1YCC5CXh6H8YHmDpmeWKjxES6XfoABxy9mmfaLLJcqS92F56ZKmVuZUPXEOLtgY/JtsnxsHYom9J4g==", + "funding": [ + { + "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" + }, + { + "type": "individual", + "url": "https://locize.com" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2", + "@types/hoist-non-react-statics": "^3.3.4", + "core-js": "^3", + "hoist-non-react-statics": "^3.3.2", + "i18next-fs-backend": "^2.2.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "i18next": "^23.6.0", + "next": ">= 12.0.0", + "react": ">= 17.0.2", + "react-i18next": "^13.3.1" + } + }, "node_modules/next-seo": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/next-seo/-/next-seo-6.4.0.tgz", @@ -5975,9 +6079,9 @@ } }, "node_modules/node-abi/node_modules/semver": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -6549,17 +6653,6 @@ "react": "^18.2.0" } }, - "node_modules/react-error-boundary": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.11.tgz", - "integrity": "sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw==", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "peerDependencies": { - "react": ">=16.13.1" - } - }, "node_modules/react-hotkeys-hook": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.4.1.tgz", @@ -6569,6 +6662,27 @@ "react-dom": ">=16.8.1" } }, + "node_modules/react-i18next": { + "version": "13.3.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-13.3.1.tgz", + "integrity": "sha512-JAtYREK879JXaN9GdzfBI4yJeo/XyLeXWUsRABvYXiFUakhZJ40l+kaTo+i+A/3cKIED41kS/HAbZ5BzFtq/Og==", + "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-icons": { "version": "4.11.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.11.0.tgz", @@ -6678,9 +6792,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, "node_modules/regenerator-transform": { "version": "0.15.1", @@ -7098,6 +7212,12 @@ "node": ">=0.10.0" } }, + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility" + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -7414,14 +7534,6 @@ "node": ">=8.0" } }, - "node_modules/toastr": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/toastr/-/toastr-2.1.4.tgz", - "integrity": "sha512-LIy77F5n+sz4tefMmFOntcJ6HL0Fv3k1TDnNmFZ0bU/GcvIIfy6eG2v7zQmMiYgaalAiUv75ttFrPn5s0gyqlA==", - "dependencies": { - "jquery": ">=1.12.0" - } - }, "node_modules/toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", @@ -7698,6 +7810,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", diff --git a/package.json b/package.json index aff0e77..3588a5a 100644 --- a/package.json +++ b/package.json @@ -10,22 +10,24 @@ "dependencies": { "@prisma/client": "^5.5.2", "@svgr/webpack": "^8.1.0", + "accept-language": "^3.0.18", "axios": "^1.6.1", "framer-motion": "^10.16.4", - "next": "^14.0.1", + "i18next": "^23.7.1", + "next": "^14.0.2", "next-auth": "^4.24.4", + "next-i18next": "^15.0.0", "next-seo": "^6.4.0", "node-html-parser": "^6.1.11", "nprogress": "^0.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-error-boundary": "^4.0.11", "react-hotkeys-hook": "^4.4.1", + "react-i18next": "^13.3.1", "react-icons": "^4.11.0", "react-select": "^5.8.0", "sass": "^1.69.5", "sharp": "^0.32.6", - "toastr": "^2.1.4", "yup": "^1.3.2" }, "devDependencies": { @@ -35,7 +37,7 @@ "@typescript-eslint/eslint-plugin": "^6.10.0", "@typescript-eslint/parser": "^6.10.0", "eslint": "^8.53.0", - "eslint-config-next": "14.0.1", + "eslint-config-next": "14.0.2", "prisma": "^5.5.2", "typescript": "5.2.2" } diff --git a/public/locales/en/common.json b/public/locales/en/common.json new file mode 100644 index 0000000..c4e6fc3 --- /dev/null +++ b/public/locales/en/common.json @@ -0,0 +1,33 @@ +{ + "confirm": "Confirm", + "cancel": "Cancel", + "back-home": "← Back to home page", + "logout": "Logout", + "login": "Login", + "link": { + "links": "Links", + "link": "Link", + "name": "Link name", + "create": "Create a link", + "edit": "Edit a link", + "remove": "Delete a link", + "remove-confirm": "Confirm deletion?" + }, + "category": { + "categories": "Categories", + "category": "Category", + "name": "Category name", + "create": "Create a category", + "edit": "Edit a category", + "remove": "Delete a category", + "remove-confirm": "Confirm deletion?", + "remove-description": "You must delete all links in this category before you can delete this category." + }, + "favorite": "Favorite", + "no-item-found": "No item found", + "search": "Search", + "avatar": "{{name}}'s avatar", + "generic-error": "Something went wrong", + "generic-error-description": "An error has occurred, if this happens again please create an issue with as much detail as possible.", + "retry": "Retry" +} diff --git a/public/locales/en/home.json b/public/locales/en/home.json new file mode 100644 index 0000000..db70819 --- /dev/null +++ b/public/locales/en/home.json @@ -0,0 +1,5 @@ +{ + "select-categorie": "Please select a category", + "or-create-one": "or create one", + "no-link": "No link for {{name}}" +} diff --git a/public/locales/en/login.json b/public/locales/en/login.json new file mode 100644 index 0000000..16fe994 --- /dev/null +++ b/public/locales/en/login.json @@ -0,0 +1,5 @@ +{ + "title": "Authentication", + "informative-text": "Authentication required to use MyLinks", + "continue-with": "Continue with {{provider}}" +} diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json new file mode 100644 index 0000000..cbd50f7 --- /dev/null +++ b/public/locales/fr/common.json @@ -0,0 +1,33 @@ +{ + "confirm": "Confirmer", + "cancel": "Annuler", + "back-home": "← Revenir à l'accueil", + "logout": "Déconnexion", + "login": "Connexion", + "link": { + "links": "Liens", + "link": "Lien", + "name": "Nom du lien", + "create": "Créer un lien", + "edit": "Modifier un lien", + "remove": "Supprimer un lien", + "remove-confirm": "Confirmer la suppression ?" + }, + "category": { + "categories": "Catégories", + "category": "Catégorie", + "name": "Nom de la catégorie", + "create": "Créer une catégorie", + "edit": "Modifier une catégorie", + "remove": "Supprimer une categorie", + "remove-confirm": "Confirmer la suppression ?", + "remove-description": "Vous devez supprimer tous les liens de cette catégorie avant de pouvoir supprimer cette catégorie" + }, + "favorite": "Favoris", + "no-item-found": "Aucun élément trouvé", + "search": "Rechercher", + "avatar": "Avatar de {{name}}", + "generic-error": "Une erreur est survenue", + "generic-error-description": "Une erreur est survenue, si cela se reproduit merci de créer une issue avec le maximum de détails.", + "retry": "Recommencer" +} diff --git a/public/locales/fr/home.json b/public/locales/fr/home.json new file mode 100644 index 0000000..bef936e --- /dev/null +++ b/public/locales/fr/home.json @@ -0,0 +1,5 @@ +{ + "select-categorie": "Veuillez séléctionner une categorie", + "or-create-one": "ou en créer une", + "no-link": "Aucun lien pour {{name}}" +} diff --git a/public/locales/fr/login.json b/public/locales/fr/login.json new file mode 100644 index 0000000..9ff969f --- /dev/null +++ b/public/locales/fr/login.json @@ -0,0 +1,5 @@ +{ + "title": "Authentification", + "informative-text": "Authentification requise pour utiliser ce service", + "continue-with": "Continuer avec {{provider}}" +} diff --git a/src/components/AppErrorBoundary.tsx b/src/components/AppErrorBoundary.tsx deleted file mode 100644 index e005078..0000000 --- a/src/components/AppErrorBoundary.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { ReactNode } from "react"; -import { ErrorBoundary } from "react-error-boundary"; - -function fallbackRender({ error, resetErrorBoundary }) { - return ( -
-

Something went wrong:

-
{error.message}
- -
- ); -} - -export default function AppErrorBoundary({ - children, -}: { - children: ReactNode; -}) { - return ( - {children} - ); -} diff --git a/src/components/BlockWrapper/block-wrapper.module.scss b/src/components/BlockWrapper/block-wrapper.module.scss index da8b4c5..db81f2c 100644 --- a/src/components/BlockWrapper/block-wrapper.module.scss +++ b/src/components/BlockWrapper/block-wrapper.module.scss @@ -15,6 +15,7 @@ } & ul { + flex: 1; animation: fadein 0.3s both; } diff --git a/src/components/ErrorBoundary/ErrorBoundary.jsx b/src/components/ErrorBoundary/ErrorBoundary.jsx new file mode 100644 index 0000000..b30474c --- /dev/null +++ b/src/components/ErrorBoundary/ErrorBoundary.jsx @@ -0,0 +1,48 @@ +import { withTranslation } from 'next-i18next'; +import React from 'react'; +import LangSelector from '../LangSelector'; +import styles from './error-boundary.module.scss'; + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { error: false, errorInfo: null }; + } + + componentDidCatch(error, errorInfo) { + // Catch errors in any components below and re-render with error message + this.setState({ + error: error, + errorInfo: errorInfo + }); + } + + render() { + if (!this.state.errorInfo) return this.props.children; + + return ( +
+
+

{this.props.t('common:generic-error')}

+

+ +

+ {this.state.error && this.state.error.toString()} + {this.state.errorInfo.componentStack} +
+
+ +
+
+
+ ); + } +} + +export default withTranslation()(ErrorBoundary); diff --git a/src/components/ErrorBoundary/error-boundary.module.scss b/src/components/ErrorBoundary/error-boundary.module.scss new file mode 100644 index 0000000..9dbae9f --- /dev/null +++ b/src/components/ErrorBoundary/error-boundary.module.scss @@ -0,0 +1,29 @@ +.error-boundary { + padding: 3em 2em; + display: flex; + justify-content: center; + + & .boundary-content { + height: 100%; + width: 600px; + gap: 2em; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + } + + & h1 { + margin: 0; + } + + & button { + width: 175px; + } +} + +@media (max-width: 600px) { + .error-boundary .boundary-content { + width: 100%; + } +} diff --git a/src/components/FormLayout.tsx b/src/components/FormLayout.tsx index 2b599fe..c39cad4 100644 --- a/src/components/FormLayout.tsx +++ b/src/components/FormLayout.tsx @@ -1,8 +1,8 @@ +import MessageManager from "components/MessageManager/MessageManager"; +import { i18n, useTranslation } from "next-i18next"; import { NextSeo } from "next-seo"; import Link from "next/link"; -import MessageManager from "components/MessageManager/MessageManager"; - interface FormProps { title: string; @@ -29,11 +29,13 @@ export default function Form({ infoMessage, canSubmit, handleSubmit, - textBtnConfirm = "Valider", + textBtnConfirm = i18n.t("common:confirm"), classBtnConfirm = "", children, disableHomeLink = false, }: FormProps) { + const { t } = useTranslation(); + return ( <> @@ -46,7 +48,7 @@ export default function Form({ {!disableHomeLink && ( - ← Revenir à l'accueil + {t("common:back-home")} )} { + const { pathname, asPath, query } = router; + i18n.changeLanguage(newLocale); + router.push({ pathname, query }, asPath, { locale: newLocale }); + }; + const languages = ["en", "fr"]; + + return ( + + ); +} diff --git a/src/components/Links/Links.tsx b/src/components/Links/Links.tsx index f87d1cb..d3cab3e 100644 --- a/src/components/Links/Links.tsx +++ b/src/components/Links/Links.tsx @@ -1,16 +1,15 @@ -import { motion } from "framer-motion"; -import LinkTag from "next/link"; - -import { Category, Link } from "types"; - -import EditItem from "components/QuickActions/EditItem"; -import RemoveItem from "components/QuickActions/RemoveItem"; -import LinkItem from "./LinkItem"; - import ButtonLink from "components/ButtonLink"; import CreateItem from "components/QuickActions/CreateItem"; +import EditItem from "components/QuickActions/EditItem"; +import RemoveItem from "components/QuickActions/RemoveItem"; import QuickActionSearch from "components/QuickActions/Search"; +import { motion } from "framer-motion"; +import { useTranslation } from "next-i18next"; +import LinkTag from "next/link"; import { RxHamburgerMenu } from "react-icons/rx"; +import { Category, Link } from "types"; +import { TFunctionParam } from "types/i18next"; +import LinkItem from "./LinkItem"; import styles from "./links.module.scss"; export default function Links({ @@ -26,11 +25,13 @@ export default function Links({ openMobileModal: () => void; openSearchModal: () => void; }) { + const { t } = useTranslation("home"); + if (category === null) { return (
-

Veuillez séléctionner une categorié

- ou en créer une +

{t("home:select-categorie")}

+ {t("home:or-create-one")}
); } @@ -85,11 +86,14 @@ export default function Links({ damping: 20, duration: 0.01, }} - > - Aucun lien pour {name} - + dangerouslySetInnerHTML={{ + __html: t("home:no-link", { name } as TFunctionParam, { + interpolation: { escapeValue: false }, + }), + }} + /> - Créer un lien + {t("common:link.create")} )} diff --git a/src/components/PageTransition.tsx b/src/components/PageTransition.tsx index e12ebfd..e0669bb 100644 --- a/src/components/PageTransition.tsx +++ b/src/components/PageTransition.tsx @@ -1,15 +1,21 @@ import { motion } from "framer-motion"; +import useIsMobile from "hooks/useIsMobile"; import { CSSProperties, ReactNode } from "react"; +import LangSelector from "./LangSelector"; export default function PageTransition({ className, children, style = {}, + hideLangageSelector = false, }: { className?: string; children: ReactNode; style?: CSSProperties; + hideLangageSelector?: boolean; }) { + const isMobile = useIsMobile(); + return ( {children} + {!hideLangageSelector && !isMobile && ( +
+ +
+ )}
); } diff --git a/src/components/QuickActions/CreateItem.tsx b/src/components/QuickActions/CreateItem.tsx index 657eb73..6e67e38 100644 --- a/src/components/QuickActions/CreateItem.tsx +++ b/src/components/QuickActions/CreateItem.tsx @@ -1,8 +1,7 @@ +import { useTranslation } from "next-i18next"; import LinkTag from "next/link"; import { IoAddOutline } from "react-icons/io5"; - import { Category } from "types"; - import styles from "./quickactions.module.scss"; export default function CreateItem({ @@ -12,12 +11,14 @@ export default function CreateItem({ }: { type: "category" | "link"; categoryId?: Category["id"]; - onClick?: (event: any) => void; // FIXME: find good event type + onClick?: (event: any) => void; }) { + const { t } = useTranslation("home"); + return ( diff --git a/src/components/QuickActions/EditItem.tsx b/src/components/QuickActions/EditItem.tsx index bdc7a92..9a1ceea 100644 --- a/src/components/QuickActions/EditItem.tsx +++ b/src/components/QuickActions/EditItem.tsx @@ -1,8 +1,7 @@ import LinkTag from "next/link"; +import { useTranslation } from "next-i18next"; import { AiOutlineEdit } from "react-icons/ai"; - import { Category, Link } from "types"; - import styles from "./quickactions.module.scss"; export default function EditItem({ @@ -13,13 +12,15 @@ export default function EditItem({ }: { type: "category" | "link"; id: Link["id"] | Category["id"]; - onClick?: (event: any) => void; // FIXME: find good event type + onClick?: (event: any) => void; className?: string; }) { + const { t } = useTranslation("home"); + return ( diff --git a/src/components/QuickActions/RemoveItem.tsx b/src/components/QuickActions/RemoveItem.tsx index 960d7dc..be44c69 100644 --- a/src/components/QuickActions/RemoveItem.tsx +++ b/src/components/QuickActions/RemoveItem.tsx @@ -1,8 +1,7 @@ +import { useTranslation } from "next-i18next"; import LinkTag from "next/link"; import { CgTrashEmpty } from "react-icons/cg"; - import { Category, Link } from "types"; - import styles from "./quickactions.module.scss"; export default function RemoveItem({ @@ -12,12 +11,14 @@ export default function RemoveItem({ }: { type: "category" | "link"; id: Link["id"] | Category["id"]; - onClick?: (event: any) => void; // FIXME: find good event type + onClick?: (event: any) => void; }) { + const { t } = useTranslation("home"); + return ( diff --git a/src/components/QuickActions/Search.tsx b/src/components/QuickActions/Search.tsx index a41121e..d0f2003 100644 --- a/src/components/QuickActions/Search.tsx +++ b/src/components/QuickActions/Search.tsx @@ -1,6 +1,5 @@ import ButtonLink from "components/ButtonLink"; import { BiSearchAlt } from "react-icons/bi"; - import styles from "./quickactions.module.scss"; export default function QuickActionSearch({ diff --git a/src/components/SearchModal/SearchList.tsx b/src/components/SearchModal/SearchList.tsx index 8be3fca..cc9cc0c 100644 --- a/src/components/SearchModal/SearchList.tsx +++ b/src/components/SearchModal/SearchList.tsx @@ -1,12 +1,10 @@ +import * as Keys from "constants/keys"; +import { useTranslation } from "next-i18next"; import { ReactNode, useEffect, useMemo } from "react"; import { useHotkeys } from "react-hotkeys-hook"; - import { SearchItem } from "types"; import { groupItemBy } from "utils/array"; import SearchListItem from "./SearchListItem"; - -import * as Keys from "constants/keys"; - import styles from "./search.module.scss"; const isActiveItem = (item: SearchItem, otherItem: SearchItem) => @@ -65,16 +63,18 @@ export default function SearchList({
    {groupedItems.length > 0 ? ( groupedItems.map(([key, items]) => ( -
  • -
  • {typeof key === "undefined" ? "-" : key}
  • - {items.map((item) => ( - - ))} +
  • + {typeof key === "undefined" ? "-" : key} +
      + {items.map((item) => ( + + ))} +
  • )) ) : noItem ? ( @@ -87,5 +87,6 @@ export default function SearchList({ } function LabelNoItem() { - return Aucun élément trouvé; + const { t } = useTranslation("home"); + return {t("common:no-item-found")}; } diff --git a/src/components/SearchModal/SearchListItem.tsx b/src/components/SearchModal/SearchListItem.tsx index 614d861..a2a7e45 100644 --- a/src/components/SearchModal/SearchListItem.tsx +++ b/src/components/SearchModal/SearchListItem.tsx @@ -34,6 +34,7 @@ export default function SearchListItem({ } ref={ref} key={id} + title={name} > )} @@ -150,6 +148,8 @@ function SearchFilter({ canSearchCategory: boolean; setCanSearchCategory: (value: boolean) => void; }) { + const { t } = useTranslation(); + return (
    - {/* à remplacer par des Chips Checkbox */}
    setCanSearchLink(target.checked)} checked={canSearchLink} /> - +
    setCanSearchCategory(target.checked)} checked={canSearchCategory} /> - +
    ); diff --git a/src/components/SideMenu/Categories/Categories.tsx b/src/components/SideMenu/Categories/Categories.tsx index 989af52..adf0ad4 100644 --- a/src/components/SideMenu/Categories/Categories.tsx +++ b/src/components/SideMenu/Categories/Categories.tsx @@ -1,7 +1,7 @@ +import { useTranslation } from "next-i18next"; +import { useMemo } from "react"; import { Category } from "types"; import CategoryItem from "./CategoryItem"; - -import { useMemo } from "react"; import styles from "./categories.module.scss"; interface CategoriesProps { @@ -14,13 +14,17 @@ export default function Categories({ categoryActive, handleSelectCategory, }: CategoriesProps) { + const { t } = useTranslation(); const linksCount = useMemo( () => categories.reduce((acc, current) => (acc += current.links.length), 0), [categories] ); + return (
    -

    Catégories • {linksCount}

    +

    + {t("common:category.categories")} • {linksCount} +

      {categories.map((category, index) => ( -

      Favoris

      +

      {t("common:favorite")}

        {favorites.map((link) => ( diff --git a/src/components/SideMenu/NavigationLinks.tsx b/src/components/SideMenu/NavigationLinks.tsx index e2e2bb4..55fb49a 100644 --- a/src/components/SideMenu/NavigationLinks.tsx +++ b/src/components/SideMenu/NavigationLinks.tsx @@ -1,7 +1,7 @@ -import PATHS from "constants/paths"; -import { SideMenuProps } from "./SideMenu"; - import ButtonLink from "components/ButtonLink"; +import PATHS from "constants/paths"; +import { useTranslation } from "next-i18next"; +import { SideMenuProps } from "./SideMenu"; import styles from "./sidemenu.module.scss"; export default function NavigationLinks({ @@ -11,25 +11,25 @@ export default function NavigationLinks({ categoryActive: SideMenuProps["categoryActive"]; openSearchModal: SideMenuProps["openSearchModal"]; }) { - const handleOpenSearchModal = (event) => { - event.preventDefault(); - openSearchModal(); - }; + const { t } = useTranslation(); + return (
        - Rechercher + {t("common:search")} S
        - Créer categorie + + {t("common:category.create")} + C
        - Créer lien + {t("common:link.create")} L
        diff --git a/src/components/SideMenu/SideMenu.tsx b/src/components/SideMenu/SideMenu.tsx index abe882d..e5633f2 100644 --- a/src/components/SideMenu/SideMenu.tsx +++ b/src/components/SideMenu/SideMenu.tsx @@ -1,14 +1,11 @@ -import { useHotkeys } from "react-hotkeys-hook"; - import BlockWrapper from "components/BlockWrapper/BlockWrapper"; -import Categories from "./Categories/Categories"; -import NavigationLinks from "./NavigationLinks"; -import Favorites from "./Favorites/Favorites"; -import UserCard from "./UserCard/UserCard"; - import * as Keys from "constants/keys"; +import { useHotkeys } from "react-hotkeys-hook"; import { Category, Link } from "types"; - +import Categories from "./Categories/Categories"; +import Favorites from "./Favorites/Favorites"; +import NavigationLinks from "./NavigationLinks"; +import UserCard from "./UserCard/UserCard"; import styles from "./sidemenu.module.scss"; export interface SideMenuProps { diff --git a/src/components/SideMenu/UserCard/UserCard.tsx b/src/components/SideMenu/UserCard/UserCard.tsx index d3941ae..59d78d4 100644 --- a/src/components/SideMenu/UserCard/UserCard.tsx +++ b/src/components/SideMenu/UserCard/UserCard.tsx @@ -1,12 +1,18 @@ +import PATHS from "constants/paths"; import { signOut, useSession } from "next-auth/react"; +import { useTranslation } from "next-i18next"; import Image from "next/image"; import { FiLogOut } from "react-icons/fi"; - -import PATHS from "constants/paths"; +import { TFunctionParam } from "types/i18next"; import styles from "./user-card.module.scss"; export default function UserCard() { const { data } = useSession({ required: true }); + const { t } = useTranslation(); + + const avatarLabel = t("common:avatar", { + name: data.user.name, + } as TFunctionParam); return (
        @@ -14,13 +20,15 @@ export default function UserCard() { src={data.user.image} width={28} height={28} - alt={`${data.user.name}'s avatar`} + alt={avatarLabel} + title={avatarLabel} /> {data.user.name}
        diff --git a/src/hooks/useIsMobile.tsx b/src/hooks/useIsMobile.tsx new file mode 100644 index 0000000..6bf7d8b --- /dev/null +++ b/src/hooks/useIsMobile.tsx @@ -0,0 +1,8 @@ +const MOBILE_SCREEN_SIZE = 768; + +export default function useIsMobile() { + return ( + typeof window !== "undefined" && + window.matchMedia(`screen and (max-width: ${MOBILE_SCREEN_SIZE}px)`).matches + ); +} diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..f833e04 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,12 @@ +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; +import nextI18NextConfig from "../../next-i18next.config"; + +async function getServerSideTranslation(locale: string = "en") { + return await serverSideTranslations( + locale, + ["common", "login", "home"], + nextI18NextConfig + ); +} + +export { getServerSideTranslation }; diff --git a/src/i18n/resources.ts b/src/i18n/resources.ts new file mode 100644 index 0000000..21ae67f --- /dev/null +++ b/src/i18n/resources.ts @@ -0,0 +1,11 @@ +import common from "../../public/locales/en/common.json"; +import home from "../../public/locales/en/home.json"; +import login from "../../public/locales/en/login.json"; + +const resources = { + common, + login, + home, +} as const; + +export default resources; diff --git a/src/pages/404.tsx b/src/pages/404.tsx index 7a8aafd..881d1d1 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -1,15 +1,18 @@ +import PageTransition from "components/PageTransition"; +import PATHS from "constants/paths"; import { NextSeo } from "next-seo"; - +import Link from "next/link"; import styles from "styles/error-page.module.scss"; export default function Custom404() { return ( - <> - + +

        404

        -

        Cette page est introuvable.

        +

        Page not found

        - + ← Back to home page +
        ); } diff --git a/src/pages/500.tsx b/src/pages/500.tsx index 7db1237..358f5af 100644 --- a/src/pages/500.tsx +++ b/src/pages/500.tsx @@ -1,15 +1,18 @@ +import PageTransition from "components/PageTransition"; +import PATHS from "constants/paths"; import { NextSeo } from "next-seo"; - +import Link from "next/link"; import styles from "styles/error-page.module.scss"; export default function Custom500() { return ( - <> - + +

        500

        -

        Une erreur côté serveur est survenue.

        +

        An internal server error has occurred

        - + ← Back to home page +
        ); } diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 030e022..c75ac92 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,17 +1,16 @@ +import ErrorBoundary from "components/ErrorBoundary/ErrorBoundary"; +import * as Keys from "constants/keys"; +import PATHS from "constants/paths"; import { SessionProvider } from "next-auth/react"; +import { appWithTranslation } from "next-i18next"; import { DefaultSeo } from "next-seo"; import { useRouter } from "next/router"; import nProgress from "nprogress"; +import "nprogress/nprogress.css"; import { useEffect } from "react"; import { useHotkeys } from "react-hotkeys-hook"; - -import AppErrorBoundary from "components/AppErrorBoundary"; - -import * as Keys from "constants/keys"; -import PATHS from "constants/paths"; - -import "nprogress/nprogress.css"; import "styles/globals.scss"; +import nextI18nextConfig from "../../next-i18next.config"; function MyApp({ Component, pageProps: { session, ...pageProps } }) { const router = useRouter(); @@ -22,7 +21,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }) { }); useEffect(() => { - // Chargement pages + // Page loading events router.events.on("routeChangeStart", nProgress.start); router.events.on("routeChangeComplete", nProgress.done); router.events.on("routeChangeError", nProgress.done); @@ -37,11 +36,11 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }) { return ( - + - + ); } -export default MyApp; +export default appWithTranslation(MyApp, nextI18nextConfig); diff --git a/src/pages/category/create.tsx b/src/pages/category/create.tsx index 63f2760..4e2ab29 100644 --- a/src/pages/category/create.tsx +++ b/src/pages/category/create.tsx @@ -1,28 +1,28 @@ import axios from "axios"; -import { useRouter } from "next/router"; -import nProgress from "nprogress"; -import { useMemo, useState } from "react"; - import FormLayout from "components/FormLayout"; import PageTransition from "components/PageTransition"; import TextBox from "components/TextBox"; - import PATHS from "constants/paths"; import useAutoFocus from "hooks/useAutoFocus"; +import { getServerSideTranslation } from "i18n"; import getUserCategoriesCount from "lib/category/getUserCategoriesCount"; +import { useTranslation } from "next-i18next"; +import { useRouter } from "next/router"; +import nProgress from "nprogress"; +import { useMemo, useState } from "react"; +import styles from "styles/form.module.scss"; import { redirectWithoutClientCache } from "utils/client"; import { HandleAxiosError } from "utils/front"; import { withAuthentication } from "utils/session"; -import styles from "styles/form.module.scss"; - export default function PageCreateCategory({ categoriesCount, }: { categoriesCount: number; }) { - const autoFocusRef = useAutoFocus(); + const { t } = useTranslation(); const router = useRouter(); + const autoFocusRef = useAutoFocus(); const info = useRouter().query?.info as string; const [name, setName] = useState(""); @@ -57,7 +57,7 @@ export default function PageCreateCategory({ return ( setName(value)} value={name} fieldClass={styles["input-field"]} - placeholder="Nom..." + placeholder={t("common:category.name")} innerRef={autoFocusRef} /> @@ -79,12 +79,14 @@ export default function PageCreateCategory({ } export const getServerSideProps = withAuthentication( - async ({ session, user }) => { + async ({ session, user, locale }) => { const categoriesCount = await getUserCategoriesCount(user); + return { props: { session, categoriesCount, + ...(await getServerSideTranslation(locale)), }, }; } diff --git a/src/pages/category/edit/[cid].tsx b/src/pages/category/edit/[cid].tsx index 80796da..6e8b19e 100644 --- a/src/pages/category/edit/[cid].tsx +++ b/src/pages/category/edit/[cid].tsx @@ -1,24 +1,24 @@ import axios from "axios"; -import { useRouter } from "next/router"; -import nProgress from "nprogress"; -import { useMemo, useState } from "react"; - import FormLayout from "components/FormLayout"; import PageTransition from "components/PageTransition"; import TextBox from "components/TextBox"; - import PATHS from "constants/paths"; import useAutoFocus from "hooks/useAutoFocus"; +import { getServerSideTranslation } from "i18n"; import getUserCategory from "lib/category/getUserCategory"; +import { useTranslation } from "next-i18next"; +import { useRouter } from "next/router"; +import nProgress from "nprogress"; +import { useMemo, useState } from "react"; +import styles from "styles/form.module.scss"; import { Category } from "types"; import { HandleAxiosError } from "utils/front"; import { withAuthentication } from "utils/session"; -import styles from "styles/form.module.scss"; - export default function PageEditCategory({ category }: { category: Category }) { - const autoFocusRef = useAutoFocus(); + const { t } = useTranslation(); const router = useRouter(); + const autoFocusRef = useAutoFocus(); const [name, setName] = useState(category.name); @@ -53,18 +53,18 @@ export default function PageEditCategory({ category }: { category: Category }) { return ( setName(value)} value={name} fieldClass={styles["input-field"]} - placeholder={`Nom original : ${category.name}`} + placeholder={`${t("common:category.name")} : ${category.name}`} innerRef={autoFocusRef} /> @@ -73,7 +73,7 @@ export default function PageEditCategory({ category }: { category: Category }) { } export const getServerSideProps = withAuthentication( - async ({ query, session, user }) => { + async ({ query, session, user, locale }) => { const { cid } = query; const category = await getUserCategory(user, Number(cid)); @@ -89,6 +89,7 @@ export const getServerSideProps = withAuthentication( props: { session, category: JSON.parse(JSON.stringify(category)), + ...(await getServerSideTranslation(locale)), }, }; } diff --git a/src/pages/category/remove/[cid].tsx b/src/pages/category/remove/[cid].tsx index b978a25..6e07bb6 100644 --- a/src/pages/category/remove/[cid].tsx +++ b/src/pages/category/remove/[cid].tsx @@ -1,32 +1,29 @@ import axios from "axios"; -import { useRouter } from "next/router"; -import nProgress from "nprogress"; -import { useMemo, useState } from "react"; - import Checkbox from "components/Checkbox"; import FormLayout from "components/FormLayout"; import PageTransition from "components/PageTransition"; import TextBox from "components/TextBox"; - import PATHS from "constants/paths"; +import { getServerSideTranslation } from "i18n"; import getUserCategory from "lib/category/getUserCategory"; +import { useTranslation } from "next-i18next"; +import { useRouter } from "next/router"; +import nProgress from "nprogress"; +import { useEffect, useMemo, useState } from "react"; +import styles from "styles/form.module.scss"; import { Category } from "types"; import { HandleAxiosError } from "utils/front"; import { withAuthentication } from "utils/session"; -import styles from "styles/form.module.scss"; - export default function PageRemoveCategory({ category, }: { category: Category; }) { + const { t, i18n } = useTranslation(); const router = useRouter(); - const [error, setError] = useState( - category.links.length > 0 - ? "Vous devez supprimer tous les liens de cette catégorie avant de pouvoir supprimer cette catégorie" - : null - ); + + const [error, setError] = useState(null); const [confirmDelete, setConfirmDelete] = useState(false); const [submitted, setSubmitted] = useState(false); @@ -53,10 +50,16 @@ export default function PageRemoveCategory({ } }; + useEffect(() => { + setError( + category.links.length > 0 ? t("common:category.remove-description") : null + ); + }, [category.links.length, i18n.language, t]); + return ( setConfirmDelete(checked)} @@ -84,7 +87,7 @@ export default function PageRemoveCategory({ } export const getServerSideProps = withAuthentication( - async ({ query, session, user }) => { + async ({ query, session, user, locale }) => { const { cid } = query; const category = await getUserCategory(user, Number(cid)); @@ -100,6 +103,7 @@ export const getServerSideProps = withAuthentication( props: { session, category: JSON.parse(JSON.stringify(category)), + ...(await getServerSideTranslation(locale)), }, }; } diff --git a/src/pages/index.tsx b/src/pages/index.tsx index f1f5a81..b8a65dc 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,10 +1,6 @@ -import { AnimatePresence } from "framer-motion"; -import { useRouter } from "next/router"; -import { useCallback, useMemo, useState } from "react"; -import { useHotkeys } from "react-hotkeys-hook"; - import BlockWrapper from "components/BlockWrapper/BlockWrapper"; import ButtonLink from "components/ButtonLink"; +import LangSelector from "components/LangSelector"; import Links from "components/Links/Links"; import Modal from "components/Modal/Modal"; import PageTransition from "components/PageTransition"; @@ -12,14 +8,18 @@ import SearchModal from "components/SearchModal/SearchModal"; import Categories from "components/SideMenu/Categories/Categories"; import SideMenu from "components/SideMenu/SideMenu"; import UserCard from "components/SideMenu/UserCard/UserCard"; - import * as Keys from "constants/keys"; import PATHS from "constants/paths"; +import { AnimatePresence } from "framer-motion"; import { useMediaQuery } from "hooks/useMediaQuery"; import useModal from "hooks/useModal"; +import { getServerSideTranslation } from "i18n"; import getUserCategories from "lib/category/getUserCategories"; +import { useRouter } from "next/router"; +import { useCallback, useMemo, useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; +import { useTranslation } from "react-i18next"; import { Category, Link, SearchItem } from "types"; -import { pushStateVanilla } from "utils/link"; import { withAuthentication } from "utils/session"; interface HomePageProps { @@ -30,6 +30,7 @@ interface HomePageProps { export default function HomePage(props: HomePageProps) { const router = useRouter(); const searchModal = useModal(); + const { t } = useTranslation(); const isMobile = useMediaQuery("(max-width: 768px)"); const mobileModal = useModal(); @@ -105,7 +106,7 @@ export default function HomePage(props: HomePageProps) { const handleSelectCategory = (category: Category) => { setCategoryActive(category); - pushStateVanilla(`${PATHS.HOME}?categoryId=${category.id}`); + router.push(`${PATHS.HOME}?categoryId=${category.id}`); mobileModal.close(); }; @@ -142,13 +143,22 @@ export default function HomePage(props: HomePageProps) { {isMobile ? ( <> - + + + + {mobileModal.isShowing && ( - Créer categorie + {t("common:category.create")} { + async ({ query, session, user, locale }) => { const queryCategoryId = (query?.categoryId as string) || ""; const categories = await getUserCategories(user); @@ -215,6 +225,7 @@ export const getServerSideProps = withAuthentication( currentCategory: currentCategory ? JSON.parse(JSON.stringify(currentCategory)) : null, + ...(await getServerSideTranslation(locale)), }, }; } diff --git a/src/pages/link/create.tsx b/src/pages/link/create.tsx index 861c1e6..d9764ba 100644 --- a/src/pages/link/create.tsx +++ b/src/pages/link/create.tsx @@ -1,30 +1,30 @@ import axios from "axios"; -import { useRouter } from "next/router"; -import nProgress from "nprogress"; -import { useMemo, useState } from "react"; - import Checkbox from "components/Checkbox"; import FormLayout from "components/FormLayout"; import PageTransition from "components/PageTransition"; import Selector from "components/Selector"; import TextBox from "components/TextBox"; - import PATHS from "constants/paths"; import useAutoFocus from "hooks/useAutoFocus"; +import { getServerSideTranslation } from "i18n"; import getUserCategories from "lib/category/getUserCategories"; +import { useTranslation } from "next-i18next"; +import { useRouter } from "next/router"; +import nProgress from "nprogress"; +import { useMemo, useState } from "react"; +import styles from "styles/form.module.scss"; import { Category, Link } from "types"; import { HandleAxiosError, IsValidURL } from "utils/front"; import { withAuthentication } from "utils/session"; -import styles from "styles/form.module.scss"; - export default function PageCreateLink({ categories, }: { categories: Category[]; }) { - const autoFocusRef = useAutoFocus(); + const { t } = useTranslation(); const router = useRouter(); + const autoFocusRef = useAutoFocus(); const categoryIdQuery = router.query?.categoryId as string; const [name, setName] = useState(""); @@ -69,7 +69,7 @@ export default function PageCreateLink({ return ( setName(value)} value={name} fieldClass={styles["input-field"]} - placeholder="Nom du lien" + placeholder={t("common:link.name")} innerRef={autoFocusRef} /> setUrl(value)} value={url} fieldClass={styles["input-field"]} - placeholder="https://www.example.org/" + placeholder="https://www.example.com/" /> setCategoryId(value)} options={categories.map(({ id, name }) => ({ @@ -106,7 +106,7 @@ export default function PageCreateLink({ name="favorite" isChecked={favorite} onChangeCallback={(value) => setFavorite(value)} - label="Favoris" + label={t("common:favorite")} /> @@ -114,7 +114,7 @@ export default function PageCreateLink({ } export const getServerSideProps = withAuthentication( - async ({ session, user }) => { + async ({ session, user, locale }) => { const categories = await getUserCategories(user); if (categories.length === 0) { return { @@ -128,6 +128,7 @@ export const getServerSideProps = withAuthentication( props: { session, categories: JSON.parse(JSON.stringify(categories)), + ...(await getServerSideTranslation(locale)), }, }; } diff --git a/src/pages/link/edit/[lid].tsx b/src/pages/link/edit/[lid].tsx index c4f80d9..7e225bf 100644 --- a/src/pages/link/edit/[lid].tsx +++ b/src/pages/link/edit/[lid].tsx @@ -1,24 +1,23 @@ import axios from "axios"; -import { useRouter } from "next/router"; -import nProgress from "nprogress"; -import { useMemo, useState } from "react"; - import Checkbox from "components/Checkbox"; import FormLayout from "components/FormLayout"; import PageTransition from "components/PageTransition"; import Selector from "components/Selector"; import TextBox from "components/TextBox"; - import PATHS from "constants/paths"; import useAutoFocus from "hooks/useAutoFocus"; +import { getServerSideTranslation } from "i18n"; import getUserCategories from "lib/category/getUserCategories"; import getUserLink from "lib/link/getUserLink"; +import { useTranslation } from "next-i18next"; +import { useRouter } from "next/router"; +import nProgress from "nprogress"; +import { useMemo, useState } from "react"; +import styles from "styles/form.module.scss"; import { Category, Link } from "types"; import { HandleAxiosError, IsValidURL } from "utils/front"; import { withAuthentication } from "utils/session"; -import styles from "styles/form.module.scss"; - export default function PageEditLink({ link, categories, @@ -26,8 +25,9 @@ export default function PageEditLink({ link: Link; categories: Category[]; }) { - const autoFocusRef = useAutoFocus(); + const { t } = useTranslation(); const router = useRouter(); + const autoFocusRef = useAutoFocus(); const [name, setName] = useState(link.name); const [url, setUrl] = useState(link.url); @@ -85,31 +85,31 @@ export default function PageEditLink({ return ( setName(value)} value={name} fieldClass={styles["input-field"]} - placeholder={`Nom original : ${link.name}`} + placeholder={`${t("common:link.name")} : ${link.name}`} innerRef={autoFocusRef} /> setUrl(value)} value={url} fieldClass={styles["input-field"]} - placeholder={`URL original : ${link.url}`} + placeholder="https://example.com/" /> setCategoryId(value)} options={categories.map(({ id, name }) => ({ @@ -121,7 +121,7 @@ export default function PageEditLink({ name="favorite" isChecked={favorite} onChangeCallback={(value) => setFavorite(value)} - label="Favoris" + label={t("common:favorite")} /> @@ -129,7 +129,7 @@ export default function PageEditLink({ } export const getServerSideProps = withAuthentication( - async ({ query, session, user }) => { + async ({ query, session, user, locale }) => { const { lid } = query; const categories = await getUserCategories(user); @@ -147,6 +147,7 @@ export const getServerSideProps = withAuthentication( session, link: JSON.parse(JSON.stringify(link)), categories: JSON.parse(JSON.stringify(categories)), + ...(await getServerSideTranslation(locale)), }, }; } diff --git a/src/pages/link/remove/[lid].tsx b/src/pages/link/remove/[lid].tsx index 64b8fe6..b8982fa 100644 --- a/src/pages/link/remove/[lid].tsx +++ b/src/pages/link/remove/[lid].tsx @@ -1,22 +1,22 @@ import axios from "axios"; -import { useRouter } from "next/router"; -import nProgress from "nprogress"; -import { useMemo, useState } from "react"; - import Checkbox from "components/Checkbox"; import FormLayout from "components/FormLayout"; import PageTransition from "components/PageTransition"; import TextBox from "components/TextBox"; - import PATHS from "constants/paths"; +import { getServerSideTranslation } from "i18n"; import getUserLink from "lib/link/getUserLink"; +import { useTranslation } from "next-i18next"; +import { useRouter } from "next/router"; +import nProgress from "nprogress"; +import { useMemo, useState } from "react"; +import styles from "styles/form.module.scss"; import { Link } from "types"; import { HandleAxiosError } from "utils/front"; import { withAuthentication } from "utils/session"; -import styles from "styles/form.module.scss"; - export default function PageRemoveLink({ link }: { link: Link }) { + const { t } = useTranslation(); const router = useRouter(); const [error, setError] = useState(null); @@ -47,7 +47,7 @@ export default function PageRemoveLink({ link }: { link: Link }) { return ( setConfirmDelete(checked)} /> @@ -94,7 +94,7 @@ export default function PageRemoveLink({ link }: { link: Link }) { } export const getServerSideProps = withAuthentication( - async ({ query, session, user }) => { + async ({ query, session, user, locale }) => { const { lid } = query; const link = await getUserLink(user, Number(lid)); @@ -110,6 +110,7 @@ export const getServerSideProps = withAuthentication( props: { session, link: JSON.parse(JSON.stringify(link)), + ...(await getServerSideTranslation(locale)), }, }; } diff --git a/src/pages/login.tsx b/src/pages/login.tsx index 00076e8..a158e4d 100644 --- a/src/pages/login.tsx +++ b/src/pages/login.tsx @@ -1,27 +1,29 @@ +import ButtonLink from "components/ButtonLink"; +import LangSelector from "components/LangSelector"; +import MessageManager from "components/MessageManager/MessageManager"; +import PageTransition from "components/PageTransition"; +import PATHS from "constants/paths"; +import { getServerSideTranslation } from "i18n/index"; +import getUser from "lib/user/getUser"; import { Provider } from "next-auth/providers"; import { getProviders, signIn } from "next-auth/react"; +import { useTranslation } from "next-i18next"; import { NextSeo } from "next-seo"; import Image from "next/image"; import { FcGoogle } from "react-icons/fc"; - -import ButtonLink from "components/ButtonLink"; -import MessageManager from "components/MessageManager/MessageManager"; -import PageTransition from "components/PageTransition"; - -import PATHS from "constants/paths"; -import getUser from "lib/user/getUser"; -import { getSession } from "utils/session"; - import styles from "styles/login.module.scss"; +import { getSession } from "utils/session"; interface SignInProps { providers: Provider[]; } export default function SignIn({ providers }: SignInProps) { + const { t } = useTranslation("login"); + return (
        - - + +
        -

        Authentification

        - +

        {t("login:title")}

        + {Object.values(providers).map(({ name, id }) => ( signIn(id, { callbackUrl: PATHS.HOME })} className={styles["login-button"]} key={id} > - Continuer avec {name} + {" "} + {t("login:continue-with", { provider: name } as undefined)} ))}
        +
        + +
        ); } -export async function getServerSideProps({ req, res }) { +export async function getServerSideProps({ req, res, locale }) { const session = await getSession(req, res); const user = await getUser(session); if (user) { @@ -61,6 +67,10 @@ export async function getServerSideProps({ req, res }) { const providers = await getProviders(); return { - props: { session, providers }, + props: { + session, + providers, + ...(await getServerSideTranslation(locale)), + }, }; } diff --git a/src/styles/error-page.module.scss b/src/styles/error-page.module.scss index ac75196..0588418 100644 --- a/src/styles/error-page.module.scss +++ b/src/styles/error-page.module.scss @@ -1,25 +1,25 @@ @import "keyframes.scss"; .App { - height: 100%; + margin-top: 10em; + margin-bottom: 3em; display: flex; align-items: center; justify-content: center; animation: fadein 250ms both; & h1 { - display: inline-block; - border-right: 1px solid rgba(0, 0, 0, 0.3); - margin: 0; - margin-right: 20px; - padding: 10px 23px 10px 0; - font-size: 24px; + font-size: 1.75em; font-weight: 500; - vertical-align: top; + margin: 0; + margin-right: 1em; + border-right: 1px solid rgba(0, 0, 0, 0.3); + padding: 10px 23px 10px 0; + display: inline-block; } & h2 { - font-size: 14px; + font-size: 1em; font-weight: normal; line-height: inherit; margin: 0; diff --git a/src/styles/globals.scss b/src/styles/globals.scss index d90b23d..a978df6 100644 --- a/src/styles/globals.scss +++ b/src/styles/globals.scss @@ -193,6 +193,12 @@ kbd { display: inline-block; } +.lang-selector { + position: absolute; + bottom: 4em; + right: 2em; +} + @media (max-width: 1280px) { .App { width: 100%; diff --git a/src/types/i18next.d.ts b/src/types/i18next.d.ts new file mode 100644 index 0000000..21a1dd6 --- /dev/null +++ b/src/types/i18next.d.ts @@ -0,0 +1,22 @@ +/** + * If you want to enable locale keys typechecking and enhance IDE experience. + * + * Requires `resolveJsonModule:true` in your tsconfig.json. + * + * @link https://www.i18next.com/overview/typescript + */ +import "i18next"; + +// resources.ts file is generated with `npm run toc` +import resources from "../i18n/resources"; + +declare module "i18next" { + interface CustomTypeOptions { + defaultNS: "common"; + resources: typeof resources; + returnNull: false; + } +} + +// Ugly hack because of the above declaration, i cant use "t" function params +type TFunctionParam = undefined; diff --git a/src/types.d.ts b/src/types/types.d.ts similarity index 100% rename from src/types.d.ts rename to src/types/types.d.ts diff --git a/src/utils/array.ts b/src/utils/array.ts index 1097729..793c8e3 100644 --- a/src/utils/array.ts +++ b/src/utils/array.ts @@ -1,10 +1,13 @@ +import { i18n } from "next-i18next"; + export function groupItemBy(array: any[], property: string) { const hash = {}; const props = property.split("."); for (const item of array) { const key = props.reduce((acc, prop) => acc && acc[prop], item); - const hashKey = key !== undefined ? key : "catégories"; + const hashKey = + key !== undefined ? key : i18n.t("common:category.categories"); if (!hash[hashKey]) { hash[hashKey] = []; diff --git a/src/utils/link.ts b/src/utils/link.ts index 675225a..e502eff 100644 --- a/src/utils/link.ts +++ b/src/utils/link.ts @@ -1,11 +1,3 @@ export function faviconLinkBuilder(origin: string) { return `http://localhost:3000/api/favicon?url=${origin}`; } - -export function pushStateVanilla(newUrl: string) { - window.history.replaceState( - { ...window.history.state, as: newUrl, url: newUrl }, - "", - newUrl - ); -} diff --git a/tsconfig.json b/tsconfig.json index 7c6cb34..eef6dbc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,8 +14,9 @@ "isolatedModules": true, "jsx": "preserve", "incremental": true, - "baseUrl": "./src" + "baseUrl": "./src", + "typeRoots": ["./src/types"] }, - "include": ["next-env.d.ts", "@/**.*", "**/*.ts", "**/*.tsx"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] }