From 416a17c5b72e5327c9c5e6f42c67f5912b32b9f4 Mon Sep 17 00:00:00 2001 From: Luke Vella Date: Thu, 21 Jul 2022 12:12:35 +0100 Subject: [PATCH] Add locale support (#228) --- README.md | 4 + declarations/i18next.d.ts | 2 + next-i18next.config.js | 2 +- package.json | 5 +- public/locales/de/app.json | 129 +++++ public/locales/de/common.json | 13 + public/locales/de/homepage.json | 44 ++ public/locales/en/app.json | 8 +- public/locales/en/common.json | 12 +- public/locales/en/homepage.json | 4 +- src/components/create-poll.tsx | 2 +- .../forms/poll-options-form/week-calendar.tsx | 8 +- src/components/home/bonus.tsx | 2 +- src/components/home/hero.tsx | 4 +- src/components/icons/check.svg | 4 +- src/components/icons/discord.svg | 3 + src/components/icons/information-circle.svg | 3 + src/components/icons/star.svg | 4 +- src/components/icons/translate.svg | 3 + src/components/name-input.tsx | 4 +- src/components/page-layout.tsx | 11 +- src/components/page-layout/footer.tsx | 168 +++--- src/components/poll.tsx | 7 - src/components/poll/desktop-poll.tsx | 110 +++- .../desktop-poll/participant-row-form.tsx | 50 +- .../poll/desktop-poll/participant-row.tsx | 23 +- src/components/poll/language-selector.tsx | 27 + src/components/poll/manage-poll.tsx | 22 +- .../poll/mobile-poll/poll-option.tsx | 1 + src/components/poll/mutations.ts | 4 + src/components/poll/vote-selector.tsx | 30 +- src/components/preferences.tsx | 36 +- .../preferences/preferences-provider.tsx | 11 +- src/components/standard-layout.tsx | 44 +- src/middleware.ts | 22 + src/pages/demo.tsx | 12 +- src/pages/home.tsx | 14 +- src/pages/new.tsx | 12 +- src/pages/poll.tsx | 15 +- src/pages/privacy-policy.tsx | 2 +- src/pages/profile.tsx | 11 +- src/utils/api-utils.ts | 6 - src/utils/auth.ts | 8 +- src/utils/with-page-translations.ts | 15 + style.css | 2 +- tests/vote-and-comment.spec.ts | 4 +- yarn.lock | 529 +++++++++++------- 47 files changed, 978 insertions(+), 478 deletions(-) create mode 100644 public/locales/de/app.json create mode 100644 public/locales/de/common.json create mode 100644 public/locales/de/homepage.json create mode 100644 src/components/icons/discord.svg create mode 100644 src/components/icons/information-circle.svg create mode 100644 src/components/icons/translate.svg create mode 100644 src/components/poll/language-selector.tsx create mode 100644 src/middleware.ts create mode 100644 src/utils/with-page-translations.ts diff --git a/README.md b/README.md index bf717809169..6fa6b76161b 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,10 @@ yarn start If you would like to contribute to the development of the project please reach out first before spending significant time on it. +### Translators 🇫🇷 🇩🇪 🇮🇹 🇪🇸 + +If you'd like to volunteer to translate Rallly to another language, check out our [guide for translators](https://github.com/lukevella/rallly/wiki/Guide-for-translators). + ## 👮‍♂️ License Rallly is open-source under the GNU Affero General Public License Version 3 (AGPLv3) or any later version. See [LICENSE](LICENSE) for more detail. diff --git a/declarations/i18next.d.ts b/declarations/i18next.d.ts index f832414cc13..8fdf4fd0ee1 100644 --- a/declarations/i18next.d.ts +++ b/declarations/i18next.d.ts @@ -1,11 +1,13 @@ import "react-i18next"; import app from "~/public/locales/en/app.json"; +import common from "~/public/locales/en/common.json"; import homepage from "~/public/locales/en/homepage.json"; declare module "next-i18next" { interface Resources { homepage: typeof homepage; app: typeof app; + common: typeof common; } } diff --git a/next-i18next.config.js b/next-i18next.config.js index 30e3f8b7f98..613b0ccb264 100644 --- a/next-i18next.config.js +++ b/next-i18next.config.js @@ -3,7 +3,7 @@ const path = require("path"); module.exports = { i18n: { defaultLocale: "en", - locales: ["en"], + locales: ["en", "de"], localePath: path.resolve("./public/locales"), reloadOnPrerender: process.env.NODE_ENV === "development", }, diff --git a/package.json b/package.json index 6988c6d50f8..cc23b24246a 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "js-cookie": "^3.0.1", "lodash": "^4.17.21", "nanoid": "^3.1.30", - "next": "^12.1.4", + "next": "^12.2.2", "next-i18next": "^10.5.0", "next-plausible": "^3.1.9", "nodemailer": "^6.7.2", @@ -43,7 +43,6 @@ "react": "17.0.2", "react-big-calendar": "^0.38.9", "react-dom": "17.0.2", - "react-github-btn": "^1.2.2", "react-hook-form": "^7.31.3", "react-hot-toast": "^2.2.0", "react-i18next": "^11.16.9", @@ -70,7 +69,7 @@ "@typescript-eslint/parser": "^5.21.0", "autoprefixer": "^10.4.2", "eslint": "^7.26.0", - "eslint-config-next": "12.1.0", + "eslint-config-next": "^12.2.2", "eslint-import-resolver-typescript": "^2.7.0", "eslint-plugin-import": "^2.22.1", "eslint-plugin-react": "^7.23.2", diff --git a/public/locales/de/app.json b/public/locales/de/app.json new file mode 100644 index 00000000000..3824f615e1c --- /dev/null +++ b/public/locales/de/app.json @@ -0,0 +1,129 @@ +{ + "12h": "12 Stunden", + "24h": "24 Stunden", + "addTimeOption": "Uhrzeit hinzufügen", + "applyToAllDates": "Auf alle Termine anwenden", + "areYouSure": "Bist du sicher?", + "back": "Zurück", + "blog": "Blog", + "calendarHelp": "Du kannst keine Umfrage ohne Optionen erstellen. Füge mindestens eine Option hinzu, um fortzufahren.", + "calendarHelpTitle": "Irgendwas vergessen?", + "cancel": "Abbrechen", + "comment": "Kommentieren", + "commentPlaceholder": "Kommentar zu dieser Umfrage hinterlassen (für jeden sichtbar)", + "comments": "Kommentare", + "continue": "Weiter", + "copied": "Kopiert", + "copyLink": "Link kopieren", + "createdBy": "von {{name}}", + "createPoll": "Umfrage erstellen", + "creatingDemo": "Demo-Umfrage wird erstellt…", + "delete": "Löschen", + "deleteComment": "Kommentar löschen", + "deleteDate": "Tag löschen", + "deletedPoll": "Umfrage gelöscht", + "deletedPollInfo": "Diese Umfrage existiert nicht mehr.", + "deletePoll": "Umfrage löschen", + "deletePollDescription": "Alle Daten zu dieser Umfrage werden gelöscht. Zur Bestätigung geben Sie bitte “{{confirmText}}” in das folgende Feld ein:", + "deletingOptionsWarning": "Du löschst Optionen, für die bereits Teilnehmer gestimmt haben. Ihre Stimmen werden ebenfalls gelöscht.", + "demoPollNotice": "Demo-Umfragen werden automatisch nach einem Tag gelöscht", + "description": "Beschreibung", + "descriptionPlaceholder": "Hallo ihr, bitte wählt alle Termine aus, die für euch passen!", + "discussions": "Diskussion", + "donate": "Spenden", + "editDetails": "Details bearbeiten", + "editOptions": "Optionen bearbeiten", + "email": "E-Mail", + "emailPlaceholder": "Max.Mustermann@mail.de", + "endingGuestSessionNotice": "Sobald eine Gastsitzung beendet ist, kann sie nicht fortgesetzt werden. Du kannst weder Auswahl noch Kommentare bearbeiten, die Du mit dieser Sitzung gemacht hast.", + "endSession": "Sitzung beenden", + "errorCreate": "Oh oh! Es gab ein Problem beim Erstellen deiner Umfrage. Der Fehler wurde protokolliert und wir werden versuchen, ihn zu beheben.", + "exportToCsv": "CSV exportieren", + "finish": "Fertigstellen", + "forgetMe": "Vergiss mich", + "goToAdmin": "Zur Admin Oberfläche", + "guest": "Gast", + "guestSessionNotice": "Du benutzt eine Gastsitzung, so können wir dich erkennen, wenn du später wiederkommst, damit du deine Angaben bearbeiten kannst.", + "guestSessionReadMore": "Mehr über Gastsitzungen.", + "hide": "Ausblenden", + "ifNeedBe": "Falls erforderlich", + "linkHasExpired": "Der Link ist abgelaufen oder ungültig", + "loading": "Wird geladen…", + "loadingParticipants": "Teilnehmerliste wird geladen…", + "location": "Ort", + "locationPlaceholder": "Joe's Café", + "lockPoll": "Umfragen sperren", + "login": "Login", + "loginCheckInbox": "Bitte überprüfe deinen Posteingang.", + "loginMagicLinkSent": "Ein magischer Link wurde geschickt an:", + "loginSendMagicLink": "Schicke mir einen magischen Link", + "loginViaMagicLink": "Anmeldung über magischen Link", + "loginViaMagicLinkDescription": "Wir senden dir eine E-Mail mit einem magischen Link, mit dem du dich anmelden kannst.", + "loginWithValidEmail": "Bitte gib eine gültige E-Mail-Adresse ein", + "logout": "Logout", + "manage": "Verwalten", + "menu": "Menü", + "mixedOptionsDescription": "Du kannst keine Zeit- und Datumsoptionen in der gleichen Umfrage haben. Welche möchtest Du beibehalten?", + "mixedOptionsKeepDates": "Datumsoptionen beibehalten", + "mixedOptionsKeepTimes": "Zeitoptionen beibehalten", + "mixedOptionsTitle": "Warte einen Moment…🤔", + "monday": "Montag", + "monthView": "Monatsansicht", + "name": "Name", + "namePlaceholder": "Max Mustermann", + "newPoll": "Neue Umfrage", + "next": "Weiter", + "nextMonth": "Nächster Monat", + "no": "Nein", + "noDatesSelected": "Kein Datum ausgewählt", + "notificationsDisabled": "Benachrichtigungen wurden deaktiviert", + "notificationsOff": "Benachrichtigungen sind deaktiviert", + "notificationsOn": "Benachrichtigungen sind aktiv", + "notificationsOnDescription": "Wenn diese Umfrage bearbeitet wird, wird eine E-Mail an {{email}} gesendet.", + "notificationsVerifyEmail": "Du musst deine E-Mail-Adresse bestätigen, um Benachrichtigungen zu aktivieren", + "ok": "Ok", + "options": "Optionen", + "participant": "Teilnehmer", + "addParticipant": "Add participant", + "alreadyVoted": "You have already voted", + "participantCount_other": "{{count}} Teilnehmer", + "participantCount": "{{count}} Teilnehmer", + "pollHasBeenLocked": "Diese Umfrage wurde gesperrt", + "pollHasBeenVerified": "Deine Umfrage wurde verifiziert", + "pollOwnerNotice": "Hallo {{name}}, sieht so aus, als ob du der Besitzer dieser Umfrage bist.", + "pollsEmpty": "Keine Umfragen erstellt", + "possibleAnswers": "Mögliche Antworten", + "preferences": "Einstellungen", + "previousMonth": "Vorheriger Monat", + "profileLogin": "Profil - Login", + "profileUser": "Profil - {{username}}", + "requiredNameError": "Bitte gib einen Namen an", + "save": "Speichern", + "saveInstruction": "Select your availability and click {{save}}", + "share": "Teilen", + "shareDescription": "Gib diesen Link deinen Teilnehmern damit sie an deiner Umfrage teilnehmen können.", + "shareLink": "Über Link teilen", + "specifyTimes": "Uhrzeiten angeben", + "specifyTimesDescription": "Start- und Endzeit für jede Option angeben", + "stepSummary": "Schritt {{current}} von {{total}}", + "sunday": "Sonntag", + "support": "Hilfe", + "timeAndDate": "Datum & Uhrzeit", + "timeFormat": "Uhrzeitformat:", + "timeZone": "Zeitzone:", + "title": "Titel", + "titlePlaceholder": "Monatliches Meeting", + "unlockPoll": "Umfrage entsperren", + "unverifiedMessage": "Ein Link zur Bestätigung der E-Mail-Adresse wurde an {{email}} versendet.", + "user": "Benutzer", + "vote": "Abstimmen", + "voteCount_other": "{{count}} Stimmen", + "voteCount": "{{count}} Stimme", + "weekStartsOn": "Woche beginnt am", + "weekView": "Wochenansicht", + "whatsThis": "Was ist das?", + "yes": "Ja", + "yourDetails": "Persönliche Angaben", + "yourName": "Your name…", + "yourPolls": "Deine Umfragen" +} diff --git a/public/locales/de/common.json b/public/locales/de/common.json new file mode 100644 index 00000000000..3f23dc4d567 --- /dev/null +++ b/public/locales/de/common.json @@ -0,0 +1,13 @@ +{ + "language": "Sprache", + "english": "Englisch", + "german": "Deutsch", + "home": "Home", + "blog": "Blog", + "support": "Support", + "donate": "Donate", + "volunteerTranslator": "Help translate this site", + "starOnGithub": "Star us on Github", + "footerCredit": "Made by @imlukevella", + "footerSponsor": "This project is user-funded. Please consider supporting it by donating." +} diff --git a/public/locales/de/homepage.json b/public/locales/de/homepage.json new file mode 100644 index 00000000000..09395ce58be --- /dev/null +++ b/public/locales/de/homepage.json @@ -0,0 +1,44 @@ +{ + "3Ls": "Ja – mit 3 Ls", + "adFree": "Ohne Werbung", + "adFreeDescription": "Gönn deinem Adblocker eine Pause - du brauchst ihn hier nicht.", + "blog": "Blog", + "comments": "Kommentare", + "commentsDescription": "Teilnehmer können deine Umfrage kommentieren, die Kommentare sind für alle sichtbar.", + "discussions": "Diskussion", + "features": "Funktionen", + "featuresSubheading": "Terminfindung leicht gemacht", + "follow": "Folgen", + "getStarted": "Los geht's", + "heroSubText": "Finde ohne Hin-und Her den richtigen Termin", + "heroText": "Plane
Besprechungen
ganz einfach", + "links": "Links", + "liveDemo": "Live Demo", + "metaDescription": "Erstelle Umfragen und stimme ab, um den besten Tag oder die beste Zeit zu finden. Eine kostenlose Alternative zu Doodle.", + "metaTitle": "Rallly - Gruppenmeetings planen", + "mobileFriendly": "Für Mobilgeräte optimiert", + "mobileFriendlyDescription": "Funktioniert hervorragend auf mobilen Geräten, so dass Teilnehmer auf Umfragen antworten können, wo immer sie sich befinden.", + "new": "Neu", + "noLoginRequired": "Keine Anmeldung benötigt", + "noLoginRequiredDescription": "Du musst dich nicht einloggen, um eine Umfrage zu erstellen oder an ihr teilzunehmen", + "notifications": "Benachrichtigungen", + "notificationsDescription": "Behalte den Überblick darüber, wer geantwortet hat. Werde benachrichtigt, wenn Teilnehmer abstimmen oder deine Umfrage kommentieren.", + "openSource": "Open Source", + "openSourceDescription": "Die Codebase ist vollständig Open-Source und auf GitHub verfügbar.", + "participant": "Teilnehmer", + "participantCount_other": "{{count}} Teilnehmer", + "social": "Social", + "participantCount": "{{count}} Teilnehmer", + "perfect": "Perfekt!", + "poweredBy": "Unterstützt von", + "principles": "Grundsätze", + "principlesSubheading": "Wir sind nicht wie die anderen", + "privacyPolicy": "Datenschutzrichtlinie", + "selfHostable": "Selfhosting möglich", + "selfHostableDescription": "Betreibe es auf deinem eigenen Server, um die volle Kontrolle über deine Daten zu haben", + "sponsorThisProject": "Dieses Projekt unterstützen", + "star": "Star", + "support": "Hilfe", + "timeSlots": "Zeitfenster", + "timeSlotsDescription": "Wähle individuelle Start- und Endzeiten für jede Option in deiner Umfrage. Die Zeiten können automatisch an die Zeitzone jedes Teilnehmers angepasst werden oder so eingestellt werden, dass Zeitzonen komplett ignoriert werden." +} diff --git a/public/locales/en/app.json b/public/locales/en/app.json index 9602c47c8c7..9808ede591b 100644 --- a/public/locales/en/app.json +++ b/public/locales/en/app.json @@ -2,7 +2,6 @@ "12h": "12-hour", "24h": "24-hour", "addTimeOption": "Add time option", - "admin": "Admin", "applyToAllDates": "Apply to all dates", "areYouSure": "Are you sure?", "back": "Back", @@ -23,7 +22,7 @@ "deleteComment": "Delete comment", "deleteDate": "Delete date", "deletedPoll": "Deleted poll", - "deletedPollInfo": "this poll doesn't exist anymore.", + "deletedPollInfo": "This poll doesn't exist anymore.", "deletePoll": "Delete poll", "deletePollDescription": "All data related to this poll will be deleted. To confirm, please type “{{confirmText}}” in to the input below:", "deletingOptionsWarning": "You are deleting options that participants have voted for. Their votes will be also be deleted.", @@ -85,6 +84,8 @@ "ok": "Ok", "options": "Options", "participant": "Participant", + "addParticipant": "Add participant", + "alreadyVoted": "You have already voted", "participantCount_other": "{{count}} participants", "participantCount": "{{count}} participant", "pollHasBeenLocked": "This poll has been locked", @@ -96,9 +97,9 @@ "previousMonth": "Previous month", "profileLogin": "Profile - Login", "profileUser": "Profile - {{username}}", - "remove": "Remove", "requiredNameError": "Name is required", "save": "Save", + "saveInstruction": "Select your availability and click {{save}}", "share": "Share", "shareDescription": "Give this link to your participants to allow them to vote on your poll.", "shareLink": "Share via link", @@ -123,5 +124,6 @@ "whatsThis": "What's this?", "yes": "Yes", "yourDetails": "Your details", + "yourName": "Your name…", "yourPolls": "Your polls" } diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 8d801badc35..e7654f0abc4 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -1,3 +1,13 @@ { - "appName": "Rallly" + "language": "Language", + "english": "English", + "german": "German", + "home": "Home", + "blog": "Blog", + "support": "Support", + "donate": "Donate", + "volunteerTranslator": "Help translate this site", + "starOnGithub": "Star us on Github", + "footerCredit": "Made by @imlukevella", + "footerSponsor": "This project is user-funded. Please consider supporting it by donating." } diff --git a/public/locales/en/homepage.json b/public/locales/en/homepage.json index 17013ea97b6..d34988a078f 100644 --- a/public/locales/en/homepage.json +++ b/public/locales/en/homepage.json @@ -9,7 +9,6 @@ "features": "Features", "featuresSubheading": "Scheduling, the smart way", "follow": "Follow", - "footerCredit": "Self-funded and built by @imlukevella", "getStarted": "Get started", "heroSubText": "Find the right date without the back and forth", "heroText": "Schedule
group meetings
with ease", @@ -28,6 +27,7 @@ "openSourceDescription": "The codebase is fully open-source and available on GitHub.", "participant": "Participant", "participantCount_other": "{{count}} participants", + "social": "Social", "participantCount": "{{count}} participant", "perfect": "Perfect!", "poweredBy": "Powered by", @@ -38,7 +38,7 @@ "selfHostableDescription": "Run it on your own server to take full control of your data", "sponsorThisProject": "Sponsor this project", "star": "Star", - "support": "support", + "support": "Support", "timeSlots": "Time slots", "timeSlotsDescription": "Set individual start and end times for each option in your poll. Times can be automatically adjusted to each participant's timezone or they can be set to ignore timezones completely." } diff --git a/src/components/create-poll.tsx b/src/components/create-poll.tsx index 650b647b52f..6517dfcde8c 100644 --- a/src/components/create-poll.tsx +++ b/src/components/create-poll.tsx @@ -156,7 +156,7 @@ const Page: NextPage = ({
-

New Poll

+

{t("newPoll")}

diff --git a/src/components/forms/poll-options-form/week-calendar.tsx b/src/components/forms/poll-options-form/week-calendar.tsx index a6f36ea2976..2c3c4d855fd 100644 --- a/src/components/forms/poll-options-form/week-calendar.tsx +++ b/src/components/forms/poll-options-form/week-calendar.tsx @@ -66,7 +66,7 @@ const WeekCalendar: React.VoidFunctionComponent = ({ ); }} components={{ - toolbar: (props) => { + toolbar: function Toolbar(props) { return ( = ({ /> ); }, - eventWrapper: (props) => { + eventWrapper: function EventWraper(props) { const start = dayjs(props.event.start); const end = dayjs(props.event.end); return ( @@ -105,7 +105,7 @@ const WeekCalendar: React.VoidFunctionComponent = ({ }, week: { // eslint-disable-next-line @typescript-eslint/no-explicit-any - header: ({ date }: any) => { + header: function Header({ date }: any) { const dateString = formatDateWithoutTime(date); const selectedOption = options.find((option) => { return option.type === "date" && option.date === dateString; @@ -143,7 +143,7 @@ const WeekCalendar: React.VoidFunctionComponent = ({ ); }, }, - timeSlotWrapper: ({ children }) => { + timeSlotWrapper: function TimeSlotWrapper({ children }) { return
{children}
; }, }} diff --git a/src/components/home/bonus.tsx b/src/components/home/bonus.tsx index 76589708bce..e1d01daa9e3 100644 --- a/src/components/home/bonus.tsx +++ b/src/components/home/bonus.tsx @@ -10,7 +10,7 @@ import Ban from "./ban-ads.svg"; const Bonus: React.VoidFunctionComponent = () => { const { t } = useTranslation("homepage"); return ( -
+

{t("principles")}

{t("principlesSubheading")}

diff --git a/src/components/home/hero.tsx b/src/components/home/hero.tsx index d43ecd08cf1..51311a8fdb9 100644 --- a/src/components/home/hero.tsx +++ b/src/components/home/hero.tsx @@ -26,12 +26,12 @@ const Hero: React.VoidFunctionComponent = () => {
{t("heroSubText")}
- + {t("getStarted")} - + - + + \ No newline at end of file diff --git a/src/components/icons/discord.svg b/src/components/icons/discord.svg new file mode 100644 index 00000000000..d42cf2750e8 --- /dev/null +++ b/src/components/icons/discord.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/icons/information-circle.svg b/src/components/icons/information-circle.svg new file mode 100644 index 00000000000..d1bd06a19bb --- /dev/null +++ b/src/components/icons/information-circle.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/icons/star.svg b/src/components/icons/star.svg index f4e707ae054..792709df1cc 100644 --- a/src/components/icons/star.svg +++ b/src/components/icons/star.svg @@ -1,3 +1,3 @@ - - + + \ No newline at end of file diff --git a/src/components/icons/translate.svg b/src/components/icons/translate.svg new file mode 100644 index 00000000000..5c06fec5445 --- /dev/null +++ b/src/components/icons/translate.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/name-input.tsx b/src/components/name-input.tsx index 05f6930c812..2d81c3b6a2d 100644 --- a/src/components/name-input.tsx +++ b/src/components/name-input.tsx @@ -1,4 +1,5 @@ import clsx from "clsx"; +import { useTranslation } from "next-i18next"; import * as React from "react"; import UserAvatar from "./poll/user-avatar"; @@ -16,6 +17,7 @@ const NameInput: React.ForwardRefRenderFunction< HTMLInputElement, NameInputProps > = ({ value, defaultValue, className, ...forwardProps }, ref) => { + const { t } = useTranslation("app"); return (
diff --git a/src/components/page-layout.tsx b/src/components/page-layout.tsx index 7d0096349c2..1c97d7c4923 100644 --- a/src/components/page-layout.tsx +++ b/src/components/page-layout.tsx @@ -1,6 +1,5 @@ import clsx from "clsx"; import dynamic from "next/dynamic"; -import Head from "next/head"; import Link from "next/link"; import { useRouter } from "next/router"; import { Trans, useTranslation } from "next-i18next"; @@ -24,6 +23,7 @@ const Menu: React.VoidFunctionComponent<{ className: string }> = ({ className, }) => { const { pathname } = useRouter(); + const { t } = useTranslation("common"); return (