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 (
);
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"]
}