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 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 <peeroke@gmail.com> Co-authored-by: Alex Johansson <alexander@n1s.se>
This commit is contained in:
parent
3764a9d462
commit
82e7e51fca
26 changed files with 801 additions and 1685 deletions
2
.babelrc
2
.babelrc
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"presets": ["next/babel"]
|
||||
}
|
||||
}
|
||||
|
|
16
.github/ISSUE_TEMPLATE/bug_report.md
vendored
16
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -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.
|
||||
|
|
13
.github/ISSUE_TEMPLATE/feature_request.md
vendored
13
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -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.)
|
||||
|
|
124
calendso.yaml
124
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: {}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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"] });
|
||||
|
||||
|
|
71
lib/core/i18n/i18n.utils.ts
Normal file
71
lib/core/i18n/i18n.utils.ts
Normal 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
20
lib/hooks/useLocale.ts
Normal 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
10
next-i18next.config.js
Normal 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"),
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"])),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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"])),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -27,6 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
"hideBranding",
|
||||
"theme",
|
||||
"completedOnboarding",
|
||||
"locale",
|
||||
]),
|
||||
bio: req.body.description,
|
||||
},
|
||||
|
|
|
@ -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"])),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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"])),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"])),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "locale" TEXT;
|
|
@ -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
5
public/robots.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
User-agent: *
|
||||
Disallow: /
|
||||
Disallow: /sandbox
|
||||
Disallow: /api
|
||||
Disallow: /static/locales
|
3
public/static/locales/en/common.json
Normal file
3
public/static/locales/en/common.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"new-event-type-btn": "New event type"
|
||||
}
|
3
public/static/locales/ro/common.json
Normal file
3
public/static/locales/ro/common.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"new-event-type-btn": "Nou tip de eveniment"
|
||||
}
|
|
@ -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"]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue