From 82e7e51fca8debc361319bf86f8ff712e1628d77 Mon Sep 17 00:00:00 2001 From: Mihai C <34626017+mihaic195@users.noreply.github.com> Date: Thu, 23 Sep 2021 11:49:17 +0300 Subject: [PATCH] Setup i18n and locale detection (#712) * feat: setup translations * feat: i18n setup * Update pages/settings/profile.tsx Co-authored-by: Alex Johansson * fix: abstract locale hook * fix: set default locale if preferred locale is not supported * Revert "fix: set default locale if preferred locale is not supported" This reverts commit e2a3d81371ee02a033520058a1d7d61cffeffc94. * fix: set default locale if preferred locale is not supported * fix: use 1 namespace and remove unnecessary logs * fix: yarn.lock * fix: linting errors Co-authored-by: Peer Richelsen Co-authored-by: Alex Johansson --- .babelrc | 2 +- .github/ISSUE_TEMPLATE/bug_report.md | 16 +- .github/ISSUE_TEMPLATE/feature_request.md | 13 +- calendso.yaml | 124 +- lib/CalEventParser.ts | 6 +- lib/calendarClient.ts | 4 +- lib/core/i18n/i18n.utils.ts | 71 + lib/hooks/useLocale.ts | 20 + next-i18next.config.js | 10 + next.config.js | 6 +- package.json | 2 + pages/[user]/[type].tsx | 5 + pages/[user]/book.tsx | 6 + pages/_app.tsx | 3 +- pages/api/user/profile.ts | 1 + pages/event-types/[type].tsx | 6 + pages/event-types/index.tsx | 33 +- pages/index.tsx | 24 +- pages/settings/profile.tsx | 35 +- .../migration.sql | 2 + prisma/schema.prisma | 1 + public/robots.txt | 5 + public/static/locales/en/common.json | 3 + public/static/locales/ro/common.json | 3 + tsconfig.json | 29 +- yarn.lock | 2056 ++++------------- 26 files changed, 801 insertions(+), 1685 deletions(-) create mode 100644 lib/core/i18n/i18n.utils.ts create mode 100644 lib/hooks/useLocale.ts create mode 100644 next-i18next.config.js create mode 100644 prisma/migrations/20210919174415_add_user_locale/migration.sql create mode 100644 public/robots.txt create mode 100644 public/static/locales/en/common.json create mode 100644 public/static/locales/ro/common.json diff --git a/.babelrc b/.babelrc index e49a7e6569..1ff94f7ed2 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,3 @@ { "presets": ["next/babel"] -} \ No newline at end of file +} diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 0e7bd19c49..50ea970837 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,18 +1,19 @@ --- name: Bug report about: Report any issues with the platform -title: '' +title: "" labels: bug -assignees: '' - +assignees: "" --- Found a bug? Please fill out the sections below. 👍 ### Issue Summary -A summary of the issue. This needs to be a clear detailed-rich summary. + +A summary of the issue. This needs to be a clear detailed-rich summary. ### Steps to Reproduce + 1. (for example) Went to ... 2. Clicked on... 3. ... @@ -20,6 +21,7 @@ A summary of the issue. This needs to be a clear detailed-rich summary. Any other relevant information. For example, why do you consider this a bug and what did you expect to happen instead? ### Technical details -* Browser version: You can use https://www.whatsmybrowser.org/ to find this out. -* Node.js version -* Anything else that you think could be an issue. + +- Browser version: You can use https://www.whatsmybrowser.org/ to find this out. +- Node.js version +- Anything else that you think could be an issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 263a4d38dc..a68c42d112 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,36 +1,43 @@ --- name: Feature request about: Suggest a feature or idea -title: '' +title: "" labels: enhancement -assignees: '' - +assignees: "" --- > Please check if your Feature Request has not been already raised in the [Discussions Tab](https://github.com/calendso/calendso/discussions), as we would like to reduce duplicates. If it has been already raised, simply upvote it 🔼. ### Is your proposal related to a problem? + + (Write your answer here.) ### Describe the solution you'd like + + (Describe your proposed solution here.) ### Describe alternatives you've considered + + (Write your answer here.) ### Additional context + + (Write your answer here.) diff --git a/calendso.yaml b/calendso.yaml index 1fa3cd05bf..e26c679925 100644 --- a/calendso.yaml +++ b/calendso.yaml @@ -7,20 +7,20 @@ info: email: support@cal.com license: name: MIT License - url: 'https://opensource.org/licenses/MIT' + url: "https://opensource.org/licenses/MIT" version: 1.0.0 - termsOfService: 'https://cal.com/terms' + termsOfService: "https://cal.com/terms" server: - url: 'http://localhost:{port}' + url: "http://localhost:{port}" description: Local Development Server variables: port: - default: '3000' + default: "3000" tags: - name: Authentication - description: 'Auth routes, powered by Next-Auth.js' + description: "Auth routes, powered by Next-Auth.js" externalDocs: - url: 'http://next-auth.js.org/' + url: "http://next-auth.js.org/" - name: Availability description: Checking and setting user availability - name: Booking @@ -38,15 +38,15 @@ paths: summary: Displays the sign in page tags: - Authentication - '/api/auth/signin/:provider': + "/api/auth/signin/:provider": post: description: Starts an OAuth signin flow for the specified provider. The POST submission requires CSRF token from /api/auth/csrf. summary: Starts an OAuth signin flow for the specified provider tags: - Authentication - '/api/auth/callback/:provider': + "/api/auth/callback/:provider": get: - description: 'Handles returning requests from OAuth services during sign in. For OAuth 2.0 providers that support the state option, the value of the state parameter is checked against the one that was generated when the sign in flow was started - this uses a hash of the CSRF token which MUST match for both the POST and GET calls during sign in.' + description: "Handles returning requests from OAuth services during sign in. For OAuth 2.0 providers that support the state option, the value of the state parameter is checked against the one that was generated when the sign in flow was started - this uses a hash of the CSRF token which MUST match for both the POST and GET calls during sign in." summary: Handles returning requests from OAuth services tags: - Authentication @@ -103,26 +103,26 @@ paths: summary: Reset a user's password tags: - Authentication - '/api/availability/{user}?dateFrom={dateFrom}&dateTo={dateTo}': + "/api/availability/{user}?dateFrom={dateFrom}&dateTo={dateTo}": get: - description: 'Gets the busy times for a particular user, by username.' + description: "Gets the busy times for a particular user, by username." summary: Gets the busy times for a user tags: - Availability responses: - '200': + "200": description: OK content: application/json: schema: type: array - description: '' + description: "" minItems: 1 uniqueItems: true x-examples: example-1: - - start: 'Fri, 03 Sep 2021 17:00:00 GMT' - end: 'Fri, 03 Sep 2021 17:40:00 GMT' + - start: "Fri, 03 Sep 2021 17:00:00 GMT" + end: "Fri, 03 Sep 2021 17:40:00 GMT" items: type: object properties: @@ -135,7 +135,7 @@ paths: required: - start - end - '500': + "500": description: Internal Server Error parameters: - schema: @@ -163,13 +163,13 @@ paths: tags: - Availability responses: - '200': + "200": description: OK content: application/json: schema: type: array - description: '' + description: "" minItems: 1 uniqueItems: true items: @@ -221,7 +221,7 @@ paths: externalId: c_feunmui1m8el5o1oo885fu48k8@group.calendar.google.com integration: google_calendar name: 1.0 Launch - '500': + "500": description: Internal Server Error post: description: Adds a selected calendar for the user. @@ -229,7 +229,7 @@ paths: tags: - Availability responses: - '200': + "200": description: OK content: application/json: @@ -238,7 +238,7 @@ paths: properties: message: type: string - '500': + "500": description: Internal Server Error requestBody: content: @@ -256,7 +256,7 @@ paths: tags: - Availability responses: - '200': + "200": description: OK content: application/json: @@ -265,7 +265,7 @@ paths: properties: message: type: string - '500': + "500": description: Internal Server Error requestBody: content: @@ -284,7 +284,7 @@ paths: tags: - Availability responses: - '200': + "200": description: OK content: application/json: @@ -305,7 +305,7 @@ paths: type: string bufferMins: type: string - description: '' + description: "" /api/availability/eventtype: post: description: Adds a new event type for the user. @@ -339,7 +339,7 @@ paths: type: array items: {} responses: - '200': + "200": description: OK content: application/json: @@ -369,7 +369,7 @@ paths: customInputs: type: array items: {} - '500': + "500": description: Internal Server Error patch: description: Updates an event type for the user. @@ -403,7 +403,7 @@ paths: type: array items: {} responses: - '200': + "200": description: OK content: application/json: @@ -433,7 +433,7 @@ paths: customInputs: type: array items: {} - '500': + "500": description: Internal Server Error delete: description: Deletes an event type for the user. @@ -441,16 +441,16 @@ paths: tags: - Availability responses: - '200': + "200": description: OK content: application/json: schema: type: object properties: {} - '500': + "500": description: Internal Server Error - '/api/book/event': + "/api/book/event": post: description: Creates a booking in the user's calendar. summary: Creates a booking for a user @@ -492,7 +492,7 @@ paths: paymentUid: type: string responses: - '204': + "204": description: No Content content: application/json: @@ -501,7 +501,7 @@ paths: properties: message: type: string - '500': + "500": description: Internal Server Error content: application/json: @@ -535,7 +535,7 @@ paths: confirmed: type: string responses: - '204': + "204": description: No Content content: application/json: @@ -544,7 +544,7 @@ paths: properties: message: type: string - '500': + "500": description: Internal Server Error /api/integrations: get: @@ -553,12 +553,12 @@ paths: tags: - Integrations responses: - '200': + "200": description: OK content: application/json: schema: - description: '' + description: "" type: object x-examples: example-1: @@ -569,7 +569,7 @@ paths: id: 83 type: google_calendar key: - scope: 'https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar.events' + scope: "https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar.events" token_type: Bearer expiry_date: 1630838974808 access_token: ya29.a0ARrdaM89R686rUyBBluTuD69oQ6WIIjjMa2xjJ0qe_5u-9ShDL09KNN1mCYoks3NP54FUMzYKmqTzb8nzCJX9jlNKP7X7-gukO4--HUyfOUbFHlHbfQ2Ei05F8AQn_xS0E_awhDgyn2anvrvEw72U3_65Zi4v6Y @@ -667,7 +667,7 @@ paths: - description required: - pageProps - '500': + "500": description: Internal Server Error content: application/json: @@ -690,7 +690,7 @@ paths: id: type: number responses: - '200': + "200": description: OK content: application/json: @@ -699,7 +699,7 @@ paths: properties: message: type: string - '401': + "401": description: Unauthorized content: application/json: @@ -708,7 +708,7 @@ paths: properties: message: type: string - '500': + "500": description: Internal Server Error content: application/json: @@ -780,7 +780,7 @@ paths: theme: type: string responses: - '200': + "200": description: OK content: application/json: @@ -789,7 +789,7 @@ paths: properties: message: type: string - '401': + "401": description: Unauthorized content: application/json: @@ -798,7 +798,7 @@ paths: properties: message: type: string - '500': + "500": description: Internal Server Error content: application/json: @@ -819,7 +819,7 @@ paths: schema: type: object responses: - '200': + "200": description: OK content: application/json: @@ -828,7 +828,7 @@ paths: properties: message: type: string - '401': + "401": description: Unauthorized content: application/json: @@ -837,7 +837,7 @@ paths: properties: message: type: string - '500': + "500": description: Internal Server Error content: application/json: @@ -872,7 +872,7 @@ paths: properties: teamId: type: string - '/api/{team}': + "/api/{team}": delete: description: Deletes a team summary: Deletes a team @@ -880,9 +880,9 @@ paths: - Teams parameters: [] responses: - '204': + "204": description: No Content - '401': + "401": description: Unauthorized content: application/json: @@ -891,7 +891,7 @@ paths: properties: message: type: string - '500': + "500": description: Internal Server Error content: application/json: @@ -907,7 +907,7 @@ paths: in: path required: true description: The team which you wish to modify - '/api/{team}/invite': + "/api/{team}/invite": post: description: Invites someone to a team. summary: Invites someone to a team @@ -933,7 +933,7 @@ paths: in: path required: true description: The team which you wish to send the invite for - '/api/{team}/membership': + "/api/{team}/membership": get: description: Lists the members of a team. summary: Lists members of a team @@ -941,7 +941,7 @@ paths: - Teams parameters: [] responses: - '200': + "200": description: OK content: application/json: @@ -951,7 +951,7 @@ paths: members: type: array items: {} - '401': + "401": description: Unauthorized content: application/json: @@ -960,7 +960,7 @@ paths: properties: message: type: string - '500': + "500": description: Internal Server Error content: application/json: @@ -983,14 +983,14 @@ paths: userId: type: number responses: - '200': + "200": description: OK content: application/json: schema: type: object properties: {} - '401': + "401": description: Unauthorized content: application/json: @@ -999,7 +999,7 @@ paths: properties: message: type: string - '500': + "500": description: Internal Server Error content: application/json: @@ -1016,7 +1016,7 @@ paths: required: true description: The team which you wish to list members of servers: - - url: 'https://app.cal.com' + - url: "https://app.cal.com" description: Production components: securitySchemes: {} diff --git a/lib/CalEventParser.ts b/lib/CalEventParser.ts index 5f8e57adfe..54e9eb8386 100644 --- a/lib/CalEventParser.ts +++ b/lib/CalEventParser.ts @@ -1,9 +1,11 @@ import short from "short-uuid"; import { v5 as uuidv5 } from "uuid"; + +import { getIntegrationName } from "@lib/integrations"; +import { VideoCallData } from "@lib/videoClient"; + import { CalendarEvent } from "./calendarClient"; import { stripHtml } from "./emails/helpers"; -import { VideoCallData } from "@lib/videoClient"; -import { getIntegrationName } from "@lib/integrations"; const translator = short(); diff --git a/lib/calendarClient.ts b/lib/calendarClient.ts index ef6dc19a22..4778e13de2 100644 --- a/lib/calendarClient.ts +++ b/lib/calendarClient.ts @@ -1,13 +1,15 @@ import { Prisma, Credential } from "@prisma/client"; + import { EventResult } from "@lib/events/EventManager"; import logger from "@lib/logger"; +import { VideoCallData } from "@lib/videoClient"; + import CalEventParser from "./CalEventParser"; import EventOrganizerMail from "./emails/EventOrganizerMail"; import EventOrganizerRescheduledMail from "./emails/EventOrganizerRescheduledMail"; import { AppleCalendar } from "./integrations/Apple/AppleCalendarAdapter"; import { CalDavCalendar } from "./integrations/CalDav/CalDavCalendarAdapter"; import prisma from "./prisma"; -import { VideoCallData } from "@lib/videoClient"; const log = logger.getChildLogger({ prefix: ["[lib] calendarClient"] }); diff --git a/lib/core/i18n/i18n.utils.ts b/lib/core/i18n/i18n.utils.ts new file mode 100644 index 0000000000..936aebd666 --- /dev/null +++ b/lib/core/i18n/i18n.utils.ts @@ -0,0 +1,71 @@ +import parser from "accept-language-parser"; +import { IncomingMessage } from "http"; + +import { getSession } from "@lib/auth"; +import prisma from "@lib/prisma"; + +import { i18n } from "../../../next-i18next.config"; + +export const extractLocaleInfo = async (req: IncomingMessage) => { + const session = await getSession({ req: req }); + const preferredLocale = parser.pick(i18n.locales, req.headers["accept-language"]); + + if (session?.user?.id) { + const user = await prisma.user.findUnique({ + where: { + id: session.user.id, + }, + select: { + locale: true, + }, + }); + + if (user?.locale) { + return user.locale; + } + + if (preferredLocale) { + await prisma.user.update({ + where: { + id: session.user.id, + }, + data: { + locale: preferredLocale, + }, + }); + } else { + await prisma.user.update({ + where: { + id: session.user.id, + }, + data: { + locale: i18n.defaultLocale, + }, + }); + } + } + + if (preferredLocale) { + return preferredLocale; + } + + return i18n.defaultLocale; +}; + +interface localeType { + [locale: string]: string; +} + +export const localeLabels: localeType = { + en: "English", + ro: "Romanian", +}; + +export type OptionType = { + value: string; + label: string; +}; + +export const localeOptions: OptionType[] = i18n.locales.map((locale) => { + return { value: locale, label: localeLabels[locale] }; +}); diff --git a/lib/hooks/useLocale.ts b/lib/hooks/useLocale.ts new file mode 100644 index 0000000000..0022c5275e --- /dev/null +++ b/lib/hooks/useLocale.ts @@ -0,0 +1,20 @@ +import { useTranslation } from "next-i18next"; +import { useEffect } from "react"; + +type LocaleProps = { + localeProp: string; +}; + +export const useLocale = (props: LocaleProps) => { + const { i18n, t } = useTranslation("common"); + + useEffect(() => { + (async () => await i18n.changeLanguage(props.localeProp))(); + }, [i18n, props.localeProp]); + + return { + i18n, + locale: props.localeProp, + t, + }; +}; diff --git a/next-i18next.config.js b/next-i18next.config.js new file mode 100644 index 0000000000..7ef0876147 --- /dev/null +++ b/next-i18next.config.js @@ -0,0 +1,10 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const path = require("path"); + +module.exports = { + i18n: { + defaultLocale: "en", + locales: ["en", "ro"], + }, + localePath: path.resolve("./public/static/locales"), +}; diff --git a/next.config.js b/next.config.js index 0b1871947f..07a33433e0 100644 --- a/next.config.js +++ b/next.config.js @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-var-requires */ const withTM = require("next-transpile-modules")(["react-timezone-select"]); +const { i18n } = require("./next-i18next.config"); // So we can test deploy previews preview if (process.env.VERCEL_URL && !process.env.BASE_URL) { @@ -41,10 +42,7 @@ if (process.env.GOOGLE_API_CREDENTIALS && !validJson(process.env.GOOGLE_API_CRED } module.exports = withTM({ - i18n: { - locales: ["en"], - defaultLocale: "en", - }, + i18n, eslint: { // This allows production builds to successfully complete even if the project has ESLint errors. ignoreDuringBuilds: true, diff --git a/package.json b/package.json index 7bc5ec23f9..872c47b128 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@stripe/stripe-js": "^1.16.0", "@tailwindcss/forms": "^0.3.3", "@types/stripe": "^8.0.417", + "accept-language-parser": "^1.5.0", "async": "^3.2.1", "bcryptjs": "^2.4.3", "classnames": "^2.3.1", @@ -52,6 +53,7 @@ "micro": "^9.3.4", "next": "^11.1.1", "next-auth": "^3.28.0", + "next-i18next": "^8.8.0", "next-seo": "^4.26.0", "next-transpile-modules": "^8.0.0", "nodemailer": "^6.6.3", diff --git a/pages/[user]/[type].tsx b/pages/[user]/[type].tsx index 505f140729..2827bc3129 100644 --- a/pages/[user]/[type].tsx +++ b/pages/[user]/[type].tsx @@ -1,7 +1,9 @@ import { User } from "@prisma/client"; import { GetServerSidePropsContext } from "next"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { asStringOrNull } from "@lib/asStringOrNull"; +import { extractLocaleInfo } from "@lib/core/i18n/i18n.utils"; import prisma from "@lib/prisma"; import { inferSSRProps } from "@lib/types/inferSSRProps"; @@ -12,6 +14,7 @@ export default function Type(props: inferSSRProps) { } export const getServerSideProps = async (context: GetServerSidePropsContext) => { + const locale = await extractLocaleInfo(context.req); // get query params and typecast them to string // (would be even better to assert them instead of typecasting) const userParam = asStringOrNull(context.query.user); @@ -177,6 +180,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => return { props: { + localeProp: locale, profile: { name: user.name, image: user.avatar, @@ -186,6 +190,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => date: dateParam, eventType: eventTypeObject, workingHours, + ...(await serverSideTranslations(locale, ["common"])), }, }; }; diff --git a/pages/[user]/book.tsx b/pages/[user]/book.tsx index e9709e32e9..556b8119bd 100644 --- a/pages/[user]/book.tsx +++ b/pages/[user]/book.tsx @@ -2,8 +2,10 @@ import dayjs from "dayjs"; import timezone from "dayjs/plugin/timezone"; import utc from "dayjs/plugin/utc"; import { GetServerSidePropsContext } from "next"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { asStringOrThrow } from "@lib/asStringOrNull"; +import { extractLocaleInfo } from "@lib/core/i18n/i18n.utils"; import prisma from "@lib/prisma"; import { inferSSRProps } from "@lib/types/inferSSRProps"; @@ -19,6 +21,8 @@ export default function Book(props: BookPageProps) { } export async function getServerSideProps(context: GetServerSidePropsContext) { + const locale = await extractLocaleInfo(context.req); + const user = await prisma.user.findUnique({ where: { username: asStringOrThrow(context.query.user), @@ -99,6 +103,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { return { props: { + localeProp: locale, profile: { slug: user.username, name: user.name, @@ -107,6 +112,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }, eventType: eventTypeObject, booking, + ...(await serverSideTranslations(locale, ["common"])), }, }; } diff --git a/pages/_app.tsx b/pages/_app.tsx index bf47216d88..911897bc4c 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,3 +1,4 @@ +import { appWithTranslation } from "next-i18next"; import { DefaultSeo } from "next-seo"; import type { AppProps as NextAppProps } from "next/app"; @@ -21,4 +22,4 @@ function MyApp({ Component, pageProps, err }: AppProps) { ); } -export default MyApp; +export default appWithTranslation(MyApp); diff --git a/pages/api/user/profile.ts b/pages/api/user/profile.ts index 55eeb7a486..bf906733c5 100644 --- a/pages/api/user/profile.ts +++ b/pages/api/user/profile.ts @@ -27,6 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) "hideBranding", "theme", "completedOnboarding", + "locale", ]), bio: req.body.description, }, diff --git a/pages/event-types/[type].tsx b/pages/event-types/[type].tsx index a0ad3fbdf6..5dc6c66fb2 100644 --- a/pages/event-types/[type].tsx +++ b/pages/event-types/[type].tsx @@ -19,6 +19,7 @@ import timezone from "dayjs/plugin/timezone"; import utc from "dayjs/plugin/utc"; import throttle from "lodash.throttle"; import { GetServerSidePropsContext } from "next"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { useRouter } from "next/router"; import React, { useEffect, useRef, useState } from "react"; import { DateRangePicker, OrientationShape, toMomentObject } from "react-dates"; @@ -33,6 +34,7 @@ import { asStringOrThrow } from "@lib/asStringOrNull"; import { getSession } from "@lib/auth"; import classNames from "@lib/classNames"; import { HttpError } from "@lib/core/http/error"; +import { extractLocaleInfo } from "@lib/core/i18n/i18n.utils"; import getIntegrations, { hasIntegration } from "@lib/integrations/getIntegrations"; import { LocationType } from "@lib/location"; import deleteEventType from "@lib/mutations/event-types/delete-event-type"; @@ -1185,6 +1187,8 @@ const EventTypePage = (props: inferSSRProps) => { export const getServerSideProps = async (context: GetServerSidePropsContext) => { const { req, query } = context; const session = await getSession({ req }); + const locale = await extractLocaleInfo(context.req); + const typeParam = parseInt(asStringOrThrow(query.type)); if (!session?.user?.id) { @@ -1345,6 +1349,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => return { props: { + localeProp: locale, eventType: eventTypeObject, locationOptions, availability, @@ -1352,6 +1357,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => teamMembers, hasPaymentIntegration, currency, + ...(await serverSideTranslations(locale, ["common"])), }, }; }; diff --git a/pages/event-types/index.tsx b/pages/event-types/index.tsx index 14e4155693..74b0365209 100644 --- a/pages/event-types/index.tsx +++ b/pages/event-types/index.tsx @@ -11,6 +11,7 @@ import { import { SchedulingType } from "@prisma/client"; import { Prisma } from "@prisma/client"; import dayjs from "dayjs"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import Head from "next/head"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -21,7 +22,9 @@ import { asStringOrNull } from "@lib/asStringOrNull"; import { getSession } from "@lib/auth"; import classNames from "@lib/classNames"; import { HttpError } from "@lib/core/http/error"; +import { extractLocaleInfo } from "@lib/core/i18n/i18n.utils"; import { ONBOARDING_INTRODUCED_AT } from "@lib/getting-started"; +import { useLocale } from "@lib/hooks/useLocale"; import { useToggleQuery } from "@lib/hooks/useToggleQuery"; import createEventType from "@lib/mutations/event-types/create-event-type"; import showToast from "@lib/notification"; @@ -53,6 +56,11 @@ type Profile = PageProps["profiles"][number]; type MembershipCount = EventType["metadata"]["membershipCount"]; const EventTypesPage = (props: PageProps) => { + const { locale } = useLocale({ + localeProp: props.localeProp, + namespaces: "event-types-page", + }); + const CreateFirstEventTypeView = () => (
@@ -62,7 +70,11 @@ const EventTypesPage = (props: PageProps) => { Event types enable you to share links that show available times on your calendar and allow people to make bookings with you.

- +
); @@ -316,10 +328,19 @@ const EventTypesPage = (props: PageProps) => { ); }; -const CreateNewEventDialog = ({ profiles, canAddEvents }: { profiles: Profile[]; canAddEvents: boolean }) => { +const CreateNewEventDialog = ({ + profiles, + canAddEvents, + localeProp, +}: { + profiles: Profile[]; + canAddEvents: boolean; + localeProp: string; +}) => { const router = useRouter(); const teamId: number | null = Number(router.query.teamId) || null; const modalOpen = useToggleQuery("new"); + const { t } = useLocale({ localeProp }); const createMutation = useMutation(createEventType, { onSuccess: async ({ eventType }) => { @@ -351,13 +372,13 @@ const CreateNewEventDialog = ({ profiles, canAddEvents }: { profiles: Profile[]; disabled: true, })} StartIcon={PlusIcon}> - New event type + {t("new-event-type-btn")} )} {profiles.filter((profile) => profile.teamId).length > 0 && ( - + Create an event type under your name or a team. @@ -534,6 +555,8 @@ const CreateNewEventDialog = ({ profiles, canAddEvents }: { profiles: Profile[]; export async function getServerSideProps(context) { const session = await getSession(context); + const locale = await extractLocaleInfo(context.req); + if (!session?.user?.id) { return { redirect: { permanent: false, destination: "/auth/login" } }; } @@ -700,6 +723,7 @@ export async function getServerSideProps(context) { return { props: { + localeProp: locale, canAddEvents, user: userObj, // don't display event teams without event types, @@ -710,6 +734,7 @@ export async function getServerSideProps(context) { ...group.profile, ...group.metadata, })), + ...(await serverSideTranslations(locale, ["common"])), }, }; } diff --git a/pages/index.tsx b/pages/index.tsx index b1e0708e57..b80cf67bc1 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,22 +1,16 @@ -import { useRouter } from "next/router"; - -import Loader from "@components/Loader"; +import { getSession } from "@lib/auth"; function RedirectPage() { - const router = useRouter(); - if (typeof window !== "undefined") { - router.push("/event-types"); - return; - } - return ; + return; } -RedirectPage.getInitialProps = (ctx) => { - if (ctx.res) { - ctx.res.writeHead(302, { Location: "/event-types" }); - ctx.res.end(); +export async function getServerSideProps(context) { + const session = await getSession(context); + if (!session?.user?.id) { + return { redirect: { permanent: false, destination: "/auth/login" } }; } - return {}; -}; + + return { redirect: { permanent: false, destination: "/event-types" } }; +} export default RedirectPage; diff --git a/pages/settings/profile.tsx b/pages/settings/profile.tsx index 18607396cf..2815d7326a 100644 --- a/pages/settings/profile.tsx +++ b/pages/settings/profile.tsx @@ -1,10 +1,13 @@ import crypto from "crypto"; import { GetServerSidePropsContext } from "next"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { RefObject, useEffect, useRef, useState } from "react"; import Select from "react-select"; import TimezoneSelect from "react-timezone-select"; import { getSession } from "@lib/auth"; +import { extractLocaleInfo, localeLabels, localeOptions, OptionType } from "@lib/core/i18n/i18n.utils"; +import { useLocale } from "@lib/hooks/useLocale"; import { isBrandingHidden } from "@lib/isBrandingHidden"; import prisma from "@lib/prisma"; import { inferSSRProps } from "@lib/types/inferSSRProps"; @@ -82,6 +85,8 @@ function HideBrandingInput(props: { } export default function Settings(props: Props) { + const { locale } = useLocale({ localeProp: props.localeProp }); + const [successModalOpen, setSuccessModalOpen] = useState(false); const usernameRef = useRef(null); const nameRef = useRef(null); @@ -91,8 +96,11 @@ export default function Settings(props: Props) { const [selectedTheme, setSelectedTheme] = useState({ value: props.user.theme }); const [selectedTimeZone, setSelectedTimeZone] = useState({ value: props.user.timeZone }); const [selectedWeekStartDay, setSelectedWeekStartDay] = useState({ value: props.user.weekStart }); + const [selectedLanguage, setSelectedLanguage] = useState({ + value: locale, + label: props.localeLabels[locale], + }); const [imageSrc, setImageSrc] = useState(props.user.avatar); - const [hasErrors, setHasErrors] = useState(false); const [errorMessage, setErrorMessage] = useState(""); @@ -101,6 +109,7 @@ export default function Settings(props: Props) { props.user.theme ? themeOptions.find((theme) => theme.value === props.user.theme) : null ); setSelectedWeekStartDay({ value: props.user.weekStart, label: props.user.weekStart }); + setSelectedLanguage({ value: locale, label: props.localeLabels[locale] }); }, []); const closeSuccessModal = () => { @@ -137,6 +146,7 @@ export default function Settings(props: Props) { const enteredTimeZone = selectedTimeZone.value; const enteredWeekStartDay = selectedWeekStartDay.value; const enteredHideBranding = hideBrandingRef.current.checked; + const enteredLanguage = selectedLanguage.value; // TODO: Add validation @@ -151,6 +161,7 @@ export default function Settings(props: Props) { weekStart: enteredWeekStartDay, hideBranding: enteredHideBranding, theme: selectedTheme ? selectedTheme.value : null, + locale: enteredLanguage, }), headers: { "Content-Type": "application/json", @@ -239,6 +250,21 @@ export default function Settings(props: Props) {
+
+ +
+