Setup i18n and locale detection (#712)

* feat: setup translations

* feat: i18n setup

* Update pages/settings/profile.tsx

Co-authored-by: Alex Johansson <alexander@n1s.se>

* 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 e2a3d81371.

* 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 <peeroke@gmail.com>
Co-authored-by: Alex Johansson <alexander@n1s.se>
pull/730/head
Mihai C 2021-09-23 11:49:17 +03:00 committed by GitHub
parent 3764a9d462
commit 82e7e51fca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 801 additions and 1685 deletions

View File

@ -1,3 +1,3 @@
{
"presets": ["next/babel"]
}
}

View File

@ -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.

View File

@ -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?
<!--
Provide a clear and concise description of what the problem is.
For example, "I'm always frustrated when..."
-->
(Write your answer here.)
### Describe the solution you'd like
<!--
Provide a clear and concise description of what you want to happen.
-->
(Describe your proposed solution here.)
### Describe alternatives you've considered
<!--
Let us know about other solutions you've tried or researched.
-->
(Write your answer here.)
### Additional context
<!--
Is there anything else you can add about the proposal?
You might want to link to related issues here, if you haven't already.
-->
(Write your answer here.)

View File

@ -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: {}

View File

@ -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();

View File

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

View File

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

20
lib/hooks/useLocale.ts Normal file
View File

@ -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,
};
};

10
next-i18next.config.js Normal file
View File

@ -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"),
};

View File

@ -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,

View File

@ -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",

View File

@ -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<typeof getServerSideProps>) {
}
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"])),
},
};
};

View File

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

View File

@ -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);

View File

@ -27,6 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
"hideBranding",
"theme",
"completedOnboarding",
"locale",
]),
bio: req.body.description,
},

View File

@ -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<typeof getServerSideProps>) => {
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"])),
},
};
};

View File

@ -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 = () => (
<div className="md:py-20">
<UserCalendarIllustration />
@ -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.
</p>
<CreateNewEventDialog canAddEvents={props.canAddEvents} profiles={props.profiles} />
<CreateNewEventDialog
localeProp={locale}
canAddEvents={props.canAddEvents}
profiles={props.profiles}
/>
</div>
</div>
);
@ -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")}
</Button>
)}
{profiles.filter((profile) => profile.teamId).length > 0 && (
<Dropdown>
<DropdownMenuTrigger asChild>
<Button EndIcon={ChevronDownIcon}>New event type</Button>
<Button EndIcon={ChevronDownIcon}>{t("new-event-type-btn")}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Create an event type under your name or a team.</DropdownMenuLabel>
@ -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"])),
},
};
}

View File

@ -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 <Loader />;
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;

View File

@ -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<HTMLInputElement>(null);
const nameRef = useRef<HTMLInputElement>(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<OptionType>({
value: locale,
label: props.localeLabels[locale],
});
const [imageSrc, setImageSrc] = useState<string>(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) {
</div>
<hr className="mt-6" />
</div>
<div>
<label htmlFor="language" className="block text-sm font-medium text-gray-700">
Language
</label>
<div className="mt-1">
<Select
id="languageSelect"
value={selectedLanguage || locale}
onChange={setSelectedLanguage}
classNamePrefix="react-select"
className="react-select-container border border-gray-300 rounded-sm shadow-sm focus:ring-neutral-500 focus:border-neutral-500 mt-1 block w-full sm:text-sm"
options={props.localeOptions}
/>
</div>
</div>
<div>
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
Timezone
@ -376,6 +402,8 @@ export default function Settings(props: Props) {
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const session = await getSession(context);
const locale = await extractLocaleInfo(context.req);
if (!session?.user?.id) {
return { redirect: { permanent: false, destination: "/auth/login" } };
}
@ -402,12 +430,17 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
if (!user) {
throw new Error("User seems logged in but cannot be found in the db");
}
return {
props: {
localeProp: locale,
localeOptions,
localeLabels,
user: {
...user,
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
},
...(await serverSideTranslations(locale, ["common"])),
},
};
};

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "locale" TEXT;

View File

@ -86,6 +86,7 @@ model User {
availability Availability[]
selectedCalendars SelectedCalendar[]
completedOnboarding Boolean? @default(false)
locale String?
twoFactorSecret String?
twoFactorEnabled Boolean @default(false)

5
public/robots.txt Normal file
View File

@ -0,0 +1,5 @@
User-agent: *
Disallow: /
Disallow: /sandbox
Disallow: /api
Disallow: /static/locales

View File

@ -0,0 +1,3 @@
{
"new-event-type-btn": "New event type"
}

View File

@ -0,0 +1,3 @@
{
"new-event-type-btn": "Nou tip de eveniment"
}

View File

@ -1,22 +1,12 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"baseUrl": ".",
"paths": {
"@components/*": [
"components/*"
],
"@lib/*": [
"lib/*"
],
"@ee/*": [
"ee/*"
]
"@components/*": ["components/*"],
"@lib/*": ["lib/*"],
"@ee/*": ["ee/*"]
},
"allowJs": true,
"skipLibCheck": true,
@ -30,13 +20,6 @@
"isolatedModules": true,
"jsx": "preserve"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"lib/*.js"
],
"exclude": [
"node_modules"
]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "lib/*.js"],
"exclude": ["node_modules"]
}

2056
yarn.lock

File diff suppressed because it is too large Load Diff