Feat/i18n crowdin (#752)
* feat: add crowdin and supported languages * fix: main branch name * feat: test crowdin integration * feat: add crowdin config skeleton * feat: update crowdin.yml * fix: remove ro translation * test: en translation * test: en translation * New Crowdin translations by Github Action (#735) Co-authored-by: Crowdin Bot <support+bot@crowdin.com> * test: en translation * fix: separate upload/download workflows * wip * New Crowdin translations by Github Action (#738) Co-authored-by: Crowdin Bot <support+bot@crowdin.com> * wip * wip * wip * wip * wip * typo * wip * wip * update crowdin config * update * chore: support i18n de,es,fr,it,pt,ru,ro,en * chore: extract i18n strings * chore: extract booking components strings for i18n * wip * extract more strings * wip * fallback to getServerSideProps for now * New Crowdin translations by Github Action (#874) Co-authored-by: Crowdin Bot <support+bot@crowdin.com> * fix: minor fixes on the datepicker * fix: add dutch lang * fix: linting issues * fix: string * fix: update GHA * cleanup trpc * fix linting Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Crowdin Bot <support+bot@crowdin.com> Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
This commit is contained in:
parent
33a683d4b0
commit
2c9b301b77
46 changed files with 2805 additions and 1876 deletions
25
.github/workflows/crowdin.yml
vendored
Normal file
25
.github/workflows/crowdin.yml
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
name: Crowdin Action
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
synchronize-with-crowdin:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: crowdin action
|
||||
uses: crowdin/github-action@1.4.0
|
||||
with:
|
||||
upload_translations: true
|
||||
download_translations: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
|
@ -5,11 +5,13 @@ import Link from "next/link";
|
|||
import { useRouter } from "next/router";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { useSlots } from "@lib/hooks/useSlots";
|
||||
|
||||
import Loader from "@components/Loader";
|
||||
|
||||
type AvailableTimesProps = {
|
||||
localeProp: string;
|
||||
workingHours: {
|
||||
days: number[];
|
||||
startTime: number;
|
||||
|
@ -27,6 +29,7 @@ type AvailableTimesProps = {
|
|||
};
|
||||
|
||||
const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
localeProp,
|
||||
date,
|
||||
eventLength,
|
||||
eventTypeId,
|
||||
|
@ -36,6 +39,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||
users,
|
||||
schedulingType,
|
||||
}) => {
|
||||
const { t } = useLocale({ localeProp: localeProp });
|
||||
const router = useRouter();
|
||||
const { rescheduleUid } = router.query;
|
||||
|
||||
|
@ -53,8 +57,11 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||
<div className="sm:pl-4 mt-8 sm:mt-0 text-center sm:w-1/3 md:-mb-5">
|
||||
<div className="text-gray-600 font-light text-lg mb-4 text-left">
|
||||
<span className="w-1/2 dark:text-white text-gray-600">
|
||||
<strong>{date.format("dddd")}</strong>
|
||||
<span className="text-gray-500">{date.format(", DD MMMM")}</span>
|
||||
<strong>{t(date.format("dddd").toLowerCase())}</strong>
|
||||
<span className="text-gray-500">
|
||||
{date.format(", DD ")}
|
||||
{t(date.format("MMMM").toLowerCase())}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="md:max-h-[364px] overflow-y-auto">
|
||||
|
@ -90,7 +97,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||
})}
|
||||
{!loading && !error && !slots.length && (
|
||||
<div className="w-full h-full flex flex-col justify-center content-center items-center -mt-4">
|
||||
<h1 className="my-6 text-xl text-black dark:text-white">All booked today.</h1>
|
||||
<h1 className="my-6 text-xl text-black dark:text-white">{t("all_booked_today")}</h1>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@ -103,7 +110,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm text-yellow-700">Could not load the available time slots.</p>
|
||||
<p className="text-sm text-yellow-700">{t("slots_load_fail")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,6 +6,7 @@ import utc from "dayjs/plugin/utc";
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import getSlots from "@lib/slots";
|
||||
|
||||
dayjs.extend(dayjsBusinessDays);
|
||||
|
@ -13,6 +14,7 @@ dayjs.extend(utc);
|
|||
dayjs.extend(timezone);
|
||||
|
||||
const DatePicker = ({
|
||||
localeProp,
|
||||
weekStart,
|
||||
onDatePicked,
|
||||
workingHours,
|
||||
|
@ -26,6 +28,7 @@ const DatePicker = ({
|
|||
periodCountCalendarDays,
|
||||
minimumBookingNotice,
|
||||
}) => {
|
||||
const { t } = useLocale({ localeProp: localeProp });
|
||||
const [days, setDays] = useState<({ disabled: boolean; date: number } | null)[]>([]);
|
||||
|
||||
const [selectedMonth, setSelectedMonth] = useState<number | null>(
|
||||
|
@ -135,8 +138,10 @@ const DatePicker = ({
|
|||
}>
|
||||
<div className="flex text-gray-600 font-light text-xl mb-4">
|
||||
<span className="w-1/2 text-gray-600 dark:text-white">
|
||||
<strong className="text-gray-900 dark:text-white">{inviteeDate().format("MMMM")}</strong>
|
||||
<span className="text-gray-500"> {inviteeDate().format("YYYY")}</span>
|
||||
<strong className="text-gray-900 dark:text-white">
|
||||
{t(inviteeDate().format("MMMM").toLowerCase())}
|
||||
</strong>{" "}
|
||||
<span className="text-gray-500">{inviteeDate().format("YYYY")}</span>
|
||||
</span>
|
||||
<div className="w-1/2 text-right text-gray-600 dark:text-gray-400">
|
||||
<button
|
||||
|
@ -153,11 +158,11 @@ const DatePicker = ({
|
|||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-7 gap-4 text-center border-b border-t dark:border-gray-800 sm:border-0">
|
||||
{["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
||||
{["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||
.sort((a, b) => (weekStart.startsWith(a) ? -1 : weekStart.startsWith(b) ? 1 : 0))
|
||||
.map((weekDay) => (
|
||||
<div key={weekDay} className="uppercase text-gray-500 text-xs tracking-widest my-4">
|
||||
{weekDay}
|
||||
{t(weekDay.toLowerCase()).substring(0, 3)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -4,10 +4,12 @@ import { FC, useEffect, useState } from "react";
|
|||
import TimezoneSelect, { ITimezoneOption } from "react-timezone-select";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import { is24h, timeZone } from "../../lib/clock";
|
||||
|
||||
type Props = {
|
||||
localeProp: string;
|
||||
onSelectTimeZone: (selectedTimeZone: string) => void;
|
||||
onToggle24hClock: (is24hClock: boolean) => void;
|
||||
};
|
||||
|
@ -15,6 +17,7 @@ type Props = {
|
|||
const TimeOptions: FC<Props> = (props) => {
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
||||
const [is24hClock, setIs24hClock] = useState(false);
|
||||
const { t } = useLocale({ localeProp: props.localeProp });
|
||||
|
||||
useEffect(() => {
|
||||
setIs24hClock(is24h());
|
||||
|
@ -35,11 +38,11 @@ const TimeOptions: FC<Props> = (props) => {
|
|||
return selectedTimeZone !== "" ? (
|
||||
<div className="absolute z-10 w-full max-w-80 rounded-sm border border-gray-200 dark:bg-gray-700 dark:border-0 bg-white px-4 py-2">
|
||||
<div className="flex mb-4">
|
||||
<div className="w-1/2 dark:text-white text-gray-600 font-medium">Time Options</div>
|
||||
<div className="w-1/2 dark:text-white text-gray-600 font-medium">{t("time_options")}</div>
|
||||
<div className="w-1/2">
|
||||
<Switch.Group as="div" className="flex items-center justify-end">
|
||||
<Switch.Label as="span" className="mr-3">
|
||||
<span className="text-sm dark:text-white text-gray-500">am/pm</span>
|
||||
<span className="text-sm dark:text-white text-gray-500">{t("am_pm")}</span>
|
||||
</Switch.Label>
|
||||
<Switch
|
||||
checked={is24hClock}
|
||||
|
@ -48,7 +51,7 @@ const TimeOptions: FC<Props> = (props) => {
|
|||
is24hClock ? "bg-black" : "dark:bg-gray-600 bg-gray-200",
|
||||
"relative inline-flex flex-shrink-0 h-5 w-8 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black"
|
||||
)}>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span className="sr-only">{t("use_setting")}</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
|
@ -58,7 +61,7 @@ const TimeOptions: FC<Props> = (props) => {
|
|||
/>
|
||||
</Switch>
|
||||
<Switch.Label as="span" className="ml-3">
|
||||
<span className="text-sm dark:text-white text-gray-500">24h</span>
|
||||
<span className="text-sm dark:text-white text-gray-500">{t("24_h")}</span>
|
||||
</Switch.Label>
|
||||
</Switch.Group>
|
||||
</div>
|
||||
|
|
|
@ -10,6 +10,7 @@ import { FormattedNumber, IntlProvider } from "react-intl";
|
|||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { timeZone } from "@lib/clock";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import useTheme from "@lib/hooks/useTheme";
|
||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
||||
|
@ -29,10 +30,11 @@ dayjs.extend(customParseFormat);
|
|||
|
||||
type Props = AvailabilityTeamPageProps | AvailabilityPageProps;
|
||||
|
||||
const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
|
||||
const AvailabilityPage = ({ profile, eventType, workingHours, localeProp }: Props) => {
|
||||
const router = useRouter();
|
||||
const { rescheduleUid } = router.query;
|
||||
const { isReady } = useTheme(profile.theme);
|
||||
const { t, locale } = useLocale({ localeProp });
|
||||
|
||||
const selectedDate = useMemo(() => {
|
||||
const dateString = asStringOrNull(router.query.date);
|
||||
|
@ -88,8 +90,8 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
|
|||
return (
|
||||
<>
|
||||
<HeadSeo
|
||||
title={`${rescheduleUid ? "Reschedule" : ""} ${eventType.title} | ${profile.name}`}
|
||||
description={`${rescheduleUid ? "Reschedule" : ""} ${eventType.title}`}
|
||||
title={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title} | ${profile.name}`}
|
||||
description={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title}`}
|
||||
name={profile.name}
|
||||
avatar={profile.image}
|
||||
/>
|
||||
|
@ -122,7 +124,7 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
|
|||
{eventType.title}
|
||||
<div>
|
||||
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||
{eventType.length} minutes
|
||||
{eventType.length} {t("minutes")}
|
||||
</div>
|
||||
{eventType.price > 0 && (
|
||||
<div>
|
||||
|
@ -166,7 +168,7 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
|
|||
</h1>
|
||||
<p className="px-2 py-1 mb-1 -ml-2 text-gray-500">
|
||||
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||
{eventType.length} minutes
|
||||
{eventType.length} {t("minutes")}
|
||||
</p>
|
||||
{eventType.price > 0 && (
|
||||
<p className="px-2 py-1 mb-1 -ml-2 text-gray-500">
|
||||
|
@ -186,6 +188,7 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
|
|||
<p className="mt-3 mb-8 text-gray-600 dark:text-gray-200">{eventType.description}</p>
|
||||
</div>
|
||||
<DatePicker
|
||||
localeProp={locale}
|
||||
date={selectedDate}
|
||||
periodType={eventType?.periodType}
|
||||
periodStartDate={eventType?.periodStartDate}
|
||||
|
@ -205,6 +208,7 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
|
|||
|
||||
{selectedDate && (
|
||||
<AvailableTimes
|
||||
localeProp={locale}
|
||||
workingHours={workingHours}
|
||||
timeFormat={timeFormat}
|
||||
minimumBookingNotice={eventType.minimumBookingNotice}
|
||||
|
@ -237,7 +241,11 @@ const AvailabilityPage = ({ profile, eventType, workingHours }: Props) => {
|
|||
)}
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<TimeOptions onSelectTimeZone={handleSelectTimeZone} onToggle24hClock={handleToggle24hClock} />
|
||||
<TimeOptions
|
||||
localeProp={locale}
|
||||
onSelectTimeZone={handleSelectTimeZone}
|
||||
onToggle24hClock={handleToggle24hClock}
|
||||
/>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
|
|
|
@ -20,6 +20,7 @@ import { createPaymentLink } from "@ee/lib/stripe/client";
|
|||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { timeZone } from "@lib/clock";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import useTheme from "@lib/hooks/useTheme";
|
||||
import { LocationType } from "@lib/location";
|
||||
import createBooking from "@lib/mutations/bookings/create-booking";
|
||||
|
@ -36,6 +37,7 @@ import { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
|
|||
type BookingPageProps = BookPageProps | TeamBookingPageProps;
|
||||
|
||||
const BookingPage = (props: BookingPageProps) => {
|
||||
const { t } = useLocale({ localeProp: props.localeProp });
|
||||
const router = useRouter();
|
||||
const { rescheduleUid } = router.query;
|
||||
const { isReady } = useTheme(props.profile.theme);
|
||||
|
@ -67,8 +69,8 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
|
||||
// TODO: Move to translations
|
||||
const locationLabels = {
|
||||
[LocationType.InPerson]: "Link or In-person meeting",
|
||||
[LocationType.Phone]: "Phone call",
|
||||
[LocationType.InPerson]: t("in_person_meeting"),
|
||||
[LocationType.Phone]: t("phone_call"),
|
||||
[LocationType.GoogleMeet]: "Google Meet",
|
||||
[LocationType.Zoom]: "Zoom Video",
|
||||
[LocationType.Daily]: "Daily.co Video",
|
||||
|
@ -85,7 +87,7 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
const data = event.target["custom_" + input.id];
|
||||
if (data) {
|
||||
if (input.type === EventTypeCustomInputType.BOOL) {
|
||||
return input.label + "\n" + (data.checked ? "Yes" : "No");
|
||||
return input.label + "\n" + (data.checked ? t("yes") : t("no"));
|
||||
} else {
|
||||
return input.label + "\n" + data.value;
|
||||
}
|
||||
|
@ -94,7 +96,7 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
.join("\n\n");
|
||||
}
|
||||
if (!!notes && !!event.target.notes.value) {
|
||||
notes += "\n\nAdditional notes:\n" + event.target.notes.value;
|
||||
notes += `\n\n${t("additional_notes")}:\n` + event.target.notes.value;
|
||||
} else {
|
||||
notes += event.target.notes.value;
|
||||
}
|
||||
|
@ -185,8 +187,16 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
<div>
|
||||
<Head>
|
||||
<title>
|
||||
{rescheduleUid ? "Reschedule" : "Confirm"} your {props.eventType.title} with {props.profile.name} |
|
||||
Cal.com
|
||||
{rescheduleUid
|
||||
? t("booking_reschedule_confirmation", {
|
||||
eventTypeTitle: props.eventType.title,
|
||||
profileName: props.profile.name,
|
||||
})
|
||||
: t("booking_confirmation", {
|
||||
eventTypeTitle: props.eventType.title,
|
||||
profileName: props.profile.name,
|
||||
})}{" "}
|
||||
| Cal.com
|
||||
</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
@ -215,7 +225,7 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
</h1>
|
||||
<p className="mb-2 text-gray-500">
|
||||
<ClockIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||
{props.eventType.length} minutes
|
||||
{props.eventType.length} {t("minutes")}
|
||||
</p>
|
||||
{props.eventType.price > 0 && (
|
||||
<p className="px-2 py-1 mb-1 -ml-2 text-gray-500">
|
||||
|
@ -244,8 +254,8 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
<div className="sm:w-1/2 sm:pl-8 sm:pr-4">
|
||||
<form onSubmit={bookingHandler}>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||
Your name
|
||||
<label htmlFor="name" className="block text-sm font-medium dark:text-white text-gray-700">
|
||||
{t("your_name")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
|
@ -262,8 +272,8 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
<div className="mb-4">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||
Email address
|
||||
className="block text-sm font-medium dark:text-white text-gray-700">
|
||||
{t("email_address")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
|
@ -281,7 +291,7 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
{locations.length > 1 && (
|
||||
<div className="mb-4">
|
||||
<span className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||
Location
|
||||
{t("location")}
|
||||
</span>
|
||||
{locations.map((location) => (
|
||||
<label key={location.type} className="block">
|
||||
|
@ -306,12 +316,12 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
<label
|
||||
htmlFor="phone"
|
||||
className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||
Phone Number
|
||||
{t("phone_number")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<PhoneInput
|
||||
name="phone"
|
||||
placeholder="Enter phone number"
|
||||
placeholder={t("enter_phone_number")}
|
||||
id="phone"
|
||||
required
|
||||
className="block w-full border-gray-300 rounded-md shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-black sm:text-sm"
|
||||
|
@ -390,7 +400,7 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
onClick={toggleGuestEmailInput}
|
||||
htmlFor="guests"
|
||||
className="block mb-1 text-sm font-medium text-blue-500 dark:text-white hover:cursor-pointer">
|
||||
+ Additional Guests
|
||||
{t("additional_guests")}
|
||||
</label>
|
||||
)}
|
||||
{guestToggle && (
|
||||
|
@ -430,24 +440,24 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
<label
|
||||
htmlFor="notes"
|
||||
className="block mb-1 text-sm font-medium text-gray-700 dark:text-white">
|
||||
Additional notes
|
||||
{t("additional_notes")}
|
||||
</label>
|
||||
<textarea
|
||||
name="notes"
|
||||
id="notes"
|
||||
rows={3}
|
||||
className="block w-full border-gray-300 rounded-md shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-black sm:text-sm"
|
||||
placeholder="Please share anything that will help prepare for our meeting."
|
||||
className="shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
placeholder={t("share_additional_notes")}
|
||||
defaultValue={props.booking ? props.booking.description : ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-start space-x-2">
|
||||
{/* TODO: add styling props to <Button variant="" color="" /> and get rid of btn-primary */}
|
||||
<Button type="submit" loading={loading}>
|
||||
{rescheduleUid ? "Reschedule" : "Confirm"}
|
||||
{rescheduleUid ? t("reschedule") : t("confirm")}
|
||||
</Button>
|
||||
<Button color="secondary" type="button" onClick={() => router.back()}>
|
||||
Cancel
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -459,7 +469,7 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm text-yellow-700">
|
||||
Could not {rescheduleUid ? "reschedule" : "book"} the meeting.
|
||||
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,10 +3,13 @@ import { CheckIcon } from "@heroicons/react/solid";
|
|||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import { DialogClose, DialogContent } from "@components/Dialog";
|
||||
import { Button } from "@components/ui/Button";
|
||||
|
||||
export type ConfirmationDialogContentProps = {
|
||||
localeProp: string;
|
||||
confirmBtnText?: string;
|
||||
cancelBtnText?: string;
|
||||
onConfirm?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
|
@ -15,7 +18,15 @@ export type ConfirmationDialogContentProps = {
|
|||
};
|
||||
|
||||
export default function ConfirmationDialogContent(props: PropsWithChildren<ConfirmationDialogContentProps>) {
|
||||
const { title, variety, confirmBtnText = "Confirm", cancelBtnText = "Cancel", onConfirm, children } = props;
|
||||
const { t } = useLocale({ localeProp: props.localeProp });
|
||||
const {
|
||||
title,
|
||||
variety,
|
||||
confirmBtnText = t("confirm"),
|
||||
cancelBtnText = t("cancel"),
|
||||
onConfirm,
|
||||
children,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
|
|
|
@ -5,6 +5,7 @@ import React from "react";
|
|||
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
const eventTypeData = Prisma.validator<Prisma.EventTypeArgs>()({
|
||||
select: {
|
||||
|
@ -20,11 +21,14 @@ const eventTypeData = Prisma.validator<Prisma.EventTypeArgs>()({
|
|||
type EventType = Prisma.EventTypeGetPayload<typeof eventTypeData>;
|
||||
|
||||
export type EventTypeDescriptionProps = {
|
||||
localeProp: string;
|
||||
eventType: EventType;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const EventTypeDescription = ({ eventType, className }: EventTypeDescriptionProps) => {
|
||||
export const EventTypeDescription = ({ localeProp, eventType, className }: EventTypeDescriptionProps) => {
|
||||
const { t } = useLocale({ localeProp });
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classNames("text-neutral-500 dark:text-white", className)}>
|
||||
|
@ -41,13 +45,13 @@ export const EventTypeDescription = ({ eventType, className }: EventTypeDescript
|
|||
{eventType.schedulingType ? (
|
||||
<li className="flex whitespace-nowrap">
|
||||
<UsersIcon className="inline mt-0.5 mr-1.5 h-4 w-4 text-neutral-400" aria-hidden="true" />
|
||||
{eventType.schedulingType === SchedulingType.ROUND_ROBIN && "Round Robin"}
|
||||
{eventType.schedulingType === SchedulingType.COLLECTIVE && "Collective"}
|
||||
{eventType.schedulingType === SchedulingType.ROUND_ROBIN && t("round_robin")}
|
||||
{eventType.schedulingType === SchedulingType.COLLECTIVE && t("collective")}
|
||||
</li>
|
||||
) : (
|
||||
<li className="flex whitespace-nowrap">
|
||||
<UserIcon className="inline mt-0.5 mr-1.5 h-4 w-4 text-neutral-400" aria-hidden="true" />
|
||||
1-on-1
|
||||
{t("1_on_1")}
|
||||
</li>
|
||||
)}
|
||||
{eventType.price > 0 && (
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
import React, { SyntheticEvent, useState } from "react";
|
||||
|
||||
import { ErrorCode } from "@lib/auth";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import Modal from "@components/Modal";
|
||||
|
||||
const errorMessages: { [key: string]: string } = {
|
||||
[ErrorCode.IncorrectPassword]: "Current password is incorrect",
|
||||
[ErrorCode.NewPasswordMatchesOld]:
|
||||
"New password matches your old password. Please choose a different password.",
|
||||
};
|
||||
|
||||
const ChangePasswordSection = () => {
|
||||
const ChangePasswordSection = ({ localeProp }: { localeProp: string }) => {
|
||||
const [oldPassword, setOldPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [successModalOpen, setSuccessModalOpen] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const { t } = useLocale({ localeProp });
|
||||
|
||||
const errorMessages: { [key: string]: string } = {
|
||||
[ErrorCode.IncorrectPassword]: t("current_incorrect_password"),
|
||||
[ErrorCode.NewPasswordMatchesOld]: t("new_password_matches_old_password"),
|
||||
};
|
||||
|
||||
const closeSuccessModal = () => {
|
||||
setSuccessModalOpen(false);
|
||||
|
@ -48,10 +49,10 @@ const ChangePasswordSection = () => {
|
|||
}
|
||||
|
||||
const body = await response.json();
|
||||
setErrorMessage(errorMessages[body.error] || "Something went wrong. Please try again");
|
||||
setErrorMessage(errorMessages[body.error] || `${t("something_went_wrong")}${t("please_try_again")}`);
|
||||
} catch (err) {
|
||||
console.error("Error changing password", err);
|
||||
setErrorMessage("Something went wrong. Please try again");
|
||||
console.error(t("error_changing_password"), err);
|
||||
setErrorMessage(`${t("something_went_wrong")}${t("please_try_again")}`);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
@ -60,14 +61,14 @@ const ChangePasswordSection = () => {
|
|||
return (
|
||||
<>
|
||||
<div className="mt-6">
|
||||
<h2 className="font-cal text-lg leading-6 font-medium text-gray-900">Change Password</h2>
|
||||
<h2 className="font-cal text-lg leading-6 font-medium text-gray-900">{t("change_password")}</h2>
|
||||
</div>
|
||||
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={changePasswordHandler}>
|
||||
<div className="py-6 lg:pb-8">
|
||||
<div className="flex">
|
||||
<div className="w-1/2 mr-2">
|
||||
<label htmlFor="current_password" className="block text-sm font-medium text-gray-700">
|
||||
Current Password
|
||||
{t("current_password")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
|
@ -78,13 +79,13 @@ const ChangePasswordSection = () => {
|
|||
id="current_password"
|
||||
required
|
||||
className="shadow-sm focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-sm"
|
||||
placeholder="Your old password"
|
||||
placeholder={t("your_old_password")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-1/2 ml-2">
|
||||
<label htmlFor="new_password" className="block text-sm font-medium text-gray-700">
|
||||
New Password
|
||||
{t("new_password")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
|
@ -95,7 +96,7 @@ const ChangePasswordSection = () => {
|
|||
required
|
||||
onInput={(e) => setNewPassword(e.currentTarget.value)}
|
||||
className="shadow-sm focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-sm"
|
||||
placeholder="Your super secure new password"
|
||||
placeholder={t("super_secure_new_password")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -105,15 +106,15 @@ const ChangePasswordSection = () => {
|
|||
<button
|
||||
type="submit"
|
||||
className="ml-2 bg-neutral-900 border border-transparent rounded-sm shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
|
||||
Save
|
||||
{t("save")}
|
||||
</button>
|
||||
</div>
|
||||
<hr className="mt-4" />
|
||||
</div>
|
||||
</form>
|
||||
<Modal
|
||||
heading="Password updated successfully"
|
||||
description="Your password has been successfully changed."
|
||||
heading={t("password_updated_successfully")}
|
||||
description={t("password_has_been_changed")}
|
||||
open={successModalOpen}
|
||||
handleClose={closeSuccessModal}
|
||||
/>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { SyntheticEvent, useState } from "react";
|
||||
|
||||
import { ErrorCode } from "@lib/auth";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import { Dialog, DialogContent } from "@components/Dialog";
|
||||
import Button from "@components/ui/Button";
|
||||
|
@ -18,12 +19,14 @@ interface DisableTwoFactorAuthModalProps {
|
|||
* Called when the user disables two-factor auth
|
||||
*/
|
||||
onDisable: () => void;
|
||||
localeProp: string;
|
||||
}
|
||||
|
||||
const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuthModalProps) => {
|
||||
const DisableTwoFactorAuthModal = ({ onDisable, onCancel, localeProp }: DisableTwoFactorAuthModalProps) => {
|
||||
const [password, setPassword] = useState("");
|
||||
const [isDisabling, setIsDisabling] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const { t } = useLocale({ localeProp });
|
||||
|
||||
async function handleDisable(e: SyntheticEvent) {
|
||||
e.preventDefault();
|
||||
|
@ -43,13 +46,13 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
|
|||
|
||||
const body = await response.json();
|
||||
if (body.error === ErrorCode.IncorrectPassword) {
|
||||
setErrorMessage("Password is incorrect.");
|
||||
setErrorMessage(t("incorrect_password"));
|
||||
} else {
|
||||
setErrorMessage("Something went wrong.");
|
||||
setErrorMessage(t("something_went_wrong"));
|
||||
}
|
||||
} catch (e) {
|
||||
setErrorMessage("Something went wrong.");
|
||||
console.error("Error disabling two-factor authentication", e);
|
||||
setErrorMessage(t("something_went_wrong"));
|
||||
console.error(t("error_disabling_2fa"), e);
|
||||
} finally {
|
||||
setIsDisabling(false);
|
||||
}
|
||||
|
@ -58,15 +61,12 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
|
|||
return (
|
||||
<Dialog open={true}>
|
||||
<DialogContent>
|
||||
<TwoFactorModalHeader
|
||||
title="Disable two-factor authentication"
|
||||
description="If you need to disable 2FA, we recommend re-enabling it as soon as possible."
|
||||
/>
|
||||
<TwoFactorModalHeader title={t("disable_2fa")} description={t("disable_2fa_recommendation")} />
|
||||
|
||||
<form onSubmit={handleDisable}>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="password" className="mt-4 block text-sm font-medium text-gray-700">
|
||||
Password
|
||||
{t("password")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
|
@ -90,10 +90,10 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
|
|||
className="ml-2"
|
||||
onClick={handleDisable}
|
||||
disabled={password.length === 0 || isDisabling}>
|
||||
Disable
|
||||
{t("disable")}
|
||||
</Button>
|
||||
<Button color="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { SyntheticEvent, useState } from "react";
|
||||
|
||||
import { ErrorCode } from "@lib/auth";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import { Dialog, DialogContent } from "@components/Dialog";
|
||||
import Button from "@components/ui/Button";
|
||||
|
@ -18,6 +19,7 @@ interface EnableTwoFactorModalProps {
|
|||
* Called when the user enables two-factor auth
|
||||
*/
|
||||
onEnable: () => void;
|
||||
localeProp: string;
|
||||
}
|
||||
|
||||
enum SetupStep {
|
||||
|
@ -45,7 +47,7 @@ const WithStep = ({
|
|||
return step === current ? children : null;
|
||||
};
|
||||
|
||||
const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps) => {
|
||||
const EnableTwoFactorModal = ({ onEnable, onCancel, localeProp }: EnableTwoFactorModalProps) => {
|
||||
const [step, setStep] = useState(SetupStep.ConfirmPassword);
|
||||
const [password, setPassword] = useState("");
|
||||
const [totpCode, setTotpCode] = useState("");
|
||||
|
@ -53,6 +55,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
|||
const [secret, setSecret] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const { t } = useLocale({ localeProp });
|
||||
|
||||
async function handleSetup(e: SyntheticEvent) {
|
||||
e.preventDefault();
|
||||
|
@ -76,13 +79,13 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
|||
}
|
||||
|
||||
if (body.error === ErrorCode.IncorrectPassword) {
|
||||
setErrorMessage("Password is incorrect.");
|
||||
setErrorMessage(t("incorrect_password"));
|
||||
} else {
|
||||
setErrorMessage("Something went wrong.");
|
||||
setErrorMessage(t("something_went_wrong"));
|
||||
}
|
||||
} catch (e) {
|
||||
setErrorMessage("Something went wrong.");
|
||||
console.error("Error setting up two-factor authentication", e);
|
||||
setErrorMessage(t("something_went_wrong"));
|
||||
console.error(t("error_enabling_2fa"), e);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
@ -108,13 +111,13 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
|||
}
|
||||
|
||||
if (body.error === ErrorCode.IncorrectTwoFactorCode) {
|
||||
setErrorMessage("Code is incorrect. Please try again.");
|
||||
setErrorMessage(`${t("code_is_incorrect")} ${t("please_try_again")}`);
|
||||
} else {
|
||||
setErrorMessage("Something went wrong.");
|
||||
setErrorMessage(t("something_went_wrong"));
|
||||
}
|
||||
} catch (e) {
|
||||
setErrorMessage("Something went wrong.");
|
||||
console.error("Error enabling up two-factor authentication", e);
|
||||
setErrorMessage(t("something_went_wrong"));
|
||||
console.error(t("error_enabling_2fa"), e);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
@ -123,16 +126,13 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
|||
return (
|
||||
<Dialog open={true}>
|
||||
<DialogContent>
|
||||
<TwoFactorModalHeader
|
||||
title="Enable two-factor authentication"
|
||||
description={setupDescriptions[step]}
|
||||
/>
|
||||
<TwoFactorModalHeader title={t("enable_2fa")} description={setupDescriptions[step]} />
|
||||
|
||||
<WithStep step={SetupStep.ConfirmPassword} current={step}>
|
||||
<form onSubmit={handleSetup}>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="password" className="mt-4 block text-sm font-medium text-gray-700">
|
||||
Password
|
||||
{t("password")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
|
@ -162,7 +162,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
|||
<form onSubmit={handleEnable}>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="code" className="mt-4 block text-sm font-medium text-gray-700">
|
||||
Code
|
||||
{t("code")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
|
@ -191,12 +191,12 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
|||
className="ml-2"
|
||||
onClick={handleSetup}
|
||||
disabled={password.length === 0 || isSubmitting}>
|
||||
Continue
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</WithStep>
|
||||
<WithStep step={SetupStep.DisplayQrCode} current={step}>
|
||||
<Button type="submit" className="ml-2" onClick={() => setStep(SetupStep.EnterTotpCode)}>
|
||||
Continue
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</WithStep>
|
||||
<WithStep step={SetupStep.EnterTotpCode} current={step}>
|
||||
|
@ -205,11 +205,11 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
|
|||
className="ml-2"
|
||||
onClick={handleEnable}
|
||||
disabled={totpCode.length !== 6 || isSubmitting}>
|
||||
Enable
|
||||
{t("enable")}
|
||||
</Button>
|
||||
</WithStep>
|
||||
<Button color="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
|
|
@ -1,37 +1,45 @@
|
|||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import Badge from "@components/ui/Badge";
|
||||
import Button from "@components/ui/Button";
|
||||
|
||||
import DisableTwoFactorModal from "./DisableTwoFactorModal";
|
||||
import EnableTwoFactorModal from "./EnableTwoFactorModal";
|
||||
|
||||
const TwoFactorAuthSection = ({ twoFactorEnabled }: { twoFactorEnabled: boolean }) => {
|
||||
const TwoFactorAuthSection = ({
|
||||
twoFactorEnabled,
|
||||
localeProp,
|
||||
}: {
|
||||
twoFactorEnabled: boolean;
|
||||
localeProp: string;
|
||||
}) => {
|
||||
const [enabled, setEnabled] = useState(twoFactorEnabled);
|
||||
const [enableModalOpen, setEnableModalOpen] = useState(false);
|
||||
const [disableModalOpen, setDisableModalOpen] = useState(false);
|
||||
const { t, locale } = useLocale({ localeProp });
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row items-center">
|
||||
<h2 className="font-cal text-lg leading-6 font-medium text-gray-900">Two-Factor Authentication</h2>
|
||||
<h2 className="font-cal text-lg leading-6 font-medium text-gray-900">{t("2fa")}</h2>
|
||||
<Badge className="text-xs ml-2" variant={enabled ? "success" : "gray"}>
|
||||
{enabled ? "Enabled" : "Disabled"}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Add an extra layer of security to your account in case your password is stolen.
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-gray-500">{t("add_an_extra_layer_of_security")}</p>
|
||||
|
||||
<Button
|
||||
className="mt-6"
|
||||
type="submit"
|
||||
onClick={() => (enabled ? setDisableModalOpen(true) : setEnableModalOpen(true))}>
|
||||
{enabled ? "Disable" : "Enable"} Two-Factor Authentication
|
||||
{enabled ? "Disable" : "Enable"} {t("2fa")}
|
||||
</Button>
|
||||
|
||||
{enableModalOpen && (
|
||||
<EnableTwoFactorModal
|
||||
localeProp={locale}
|
||||
onEnable={() => {
|
||||
setEnabled(true);
|
||||
setEnableModalOpen(false);
|
||||
|
@ -42,6 +50,7 @@ const TwoFactorAuthSection = ({ twoFactorEnabled }: { twoFactorEnabled: boolean
|
|||
|
||||
{disableModalOpen && (
|
||||
<DisableTwoFactorModal
|
||||
localeProp={locale}
|
||||
onDisable={() => {
|
||||
setEnabled(false);
|
||||
setDisableModalOpen(false);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { ArrowLeftIcon, PlusIcon, TrashIcon } from "@heroicons/react/outline";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { Member } from "@lib/member";
|
||||
import { Team } from "@lib/team";
|
||||
|
||||
|
@ -16,7 +17,11 @@ import ErrorAlert from "@components/ui/alerts/Error";
|
|||
|
||||
import MemberList from "./MemberList";
|
||||
|
||||
export default function EditTeam(props: { team: Team | undefined | null; onCloseEdit: () => void }) {
|
||||
export default function EditTeam(props: {
|
||||
localeProp: string;
|
||||
team: Team | undefined | null;
|
||||
onCloseEdit: () => void;
|
||||
}) {
|
||||
const [members, setMembers] = useState([]);
|
||||
|
||||
const nameRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
|
||||
|
@ -30,6 +35,7 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
|
|||
const [inviteModalTeam, setInviteModalTeam] = useState<Team | null | undefined>();
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [imageSrc, setImageSrc] = useState<string>("");
|
||||
const { t, locale } = useLocale({ localeProp: props.localeProp });
|
||||
|
||||
const loadMembers = () =>
|
||||
fetch("/api/teams/" + props.team?.id + "/membership")
|
||||
|
@ -132,19 +138,19 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
|
|||
size="sm"
|
||||
StartIcon={ArrowLeftIcon}
|
||||
onClick={() => props.onCloseEdit()}>
|
||||
Back
|
||||
{t("back")}
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<div className="pb-5 pr-4 sm:pb-6">
|
||||
<h3 className="text-lg font-bold leading-6 text-gray-900">{props.team?.name}</h3>
|
||||
<div className="max-w-xl mt-2 text-sm text-gray-500">
|
||||
<p>Manage your team</p>
|
||||
<p>{t("manage_your_team")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="mt-2" />
|
||||
<h3 className="font-cal font-bold leading-6 text-gray-900 mt-7 text-md">Profile</h3>
|
||||
<h3 className="font-cal font-bold leading-6 text-gray-900 mt-7 text-md">{t("profile")}</h3>
|
||||
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={updateTeamHandler}>
|
||||
{hasErrors && <ErrorAlert message={errorMessage} />}
|
||||
<div className="py-6 lg:pb-8">
|
||||
|
@ -152,18 +158,22 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
|
|||
<div className="flex-grow space-y-6">
|
||||
<div className="block sm:flex">
|
||||
<div className="w-full mb-6 sm:w-1/2 sm:mr-2">
|
||||
<UsernameInput ref={teamUrlRef} defaultValue={props.team?.slug} label={"My team URL"} />
|
||||
<UsernameInput
|
||||
ref={teamUrlRef}
|
||||
defaultValue={props.team?.slug}
|
||||
label={t("my_team_url")}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full sm:w-1/2 sm:ml-2">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||
Team name
|
||||
{t("team_name")}
|
||||
</label>
|
||||
<input
|
||||
ref={nameRef}
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
placeholder="Your team name"
|
||||
placeholder={t("your_team_name")}
|
||||
required
|
||||
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-sm shadow-sm focus:outline-none focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"
|
||||
defaultValue={props.team?.name}
|
||||
|
@ -172,7 +182,7 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
|
|||
</div>
|
||||
<div>
|
||||
<label htmlFor="about" className="block text-sm font-medium text-gray-700">
|
||||
About
|
||||
{t("about")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<textarea
|
||||
|
@ -182,9 +192,7 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
|
|||
rows={3}
|
||||
defaultValue={props.team?.bio}
|
||||
className="block w-full mt-1 border-gray-300 rounded-sm shadow-sm focus:ring-neutral-500 focus:border-neutral-500 sm:text-sm"></textarea>
|
||||
<p className="mt-2 text-sm text-gray-500">
|
||||
A few sentences about your team. This will appear on your team's URL page.
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-gray-500">{t("team_description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -206,7 +214,7 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
|
|||
<ImageUploader
|
||||
target="logo"
|
||||
id="logo-upload"
|
||||
buttonMsg={imageSrc !== "" ? "Edit logo" : "Upload a logo"}
|
||||
buttonMsg={imageSrc !== "" ? t("edit_logo") : t("upload_a_logo")}
|
||||
handleAvatarChange={handleLogoChange}
|
||||
imageSrc={imageSrc ?? props.team?.logo}
|
||||
/>
|
||||
|
@ -214,20 +222,25 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
|
|||
<hr className="mt-6" />
|
||||
</div>
|
||||
<div className="flex justify-between mt-7">
|
||||
<h3 className="font-cal font-bold leading-6 text-gray-900 text-md">Members</h3>
|
||||
<h3 className="font-cal font-bold leading-6 text-gray-900 text-md">{t("members")}</h3>
|
||||
<div className="relative flex items-center">
|
||||
<Button
|
||||
type="button"
|
||||
color="secondary"
|
||||
StartIcon={PlusIcon}
|
||||
onClick={() => onInviteMember(props.team)}>
|
||||
New Member
|
||||
{t("new_member")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{!!members.length && (
|
||||
<MemberList members={members} onRemoveMember={onRemoveMember} onChange={loadMembers} />
|
||||
<MemberList
|
||||
localeProp={locale}
|
||||
members={members}
|
||||
onRemoveMember={onRemoveMember}
|
||||
onChange={loadMembers}
|
||||
/>
|
||||
)}
|
||||
<hr className="mt-6" />
|
||||
</div>
|
||||
|
@ -245,14 +258,14 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
|
|||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label htmlFor="hide-branding" className="font-medium text-gray-700">
|
||||
Disable Cal.com branding
|
||||
{t("disable_cal_branding")}
|
||||
</label>
|
||||
<p className="text-gray-500">Hide all Cal.com branding from your public pages.</p>
|
||||
<p className="text-gray-500">{t("disable_cal_branding_description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr className="mt-6" />
|
||||
</div>
|
||||
<h3 className="font-bold leading-6 text-gray-900 mt-7 text-md">Danger Zone</h3>
|
||||
<h3 className="font-bold leading-6 text-gray-900 mt-7 text-md">{t("danger_zone")}</h3>
|
||||
<div>
|
||||
<div className="relative flex items-start">
|
||||
<Dialog>
|
||||
|
@ -262,16 +275,15 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
|
|||
}}
|
||||
className="btn-sm btn-white">
|
||||
<TrashIcon className="group-hover:text-red text-gray-700 w-3.5 h-3.5 mr-2 inline-block" />
|
||||
Disband Team
|
||||
{t("disband_team")}
|
||||
</DialogTrigger>
|
||||
<ConfirmationDialogContent
|
||||
localeProp={locale}
|
||||
variety="danger"
|
||||
title="Disband Team"
|
||||
confirmBtnText="Yes, disband team"
|
||||
cancelBtnText="Cancel"
|
||||
title={t("disband_team")}
|
||||
confirmBtnText={t("confirm_disband_team")}
|
||||
onConfirm={() => deleteTeam()}>
|
||||
Are you sure you want to disband this team? Anyone who you've shared this team
|
||||
link with will no longer be able to book using it.
|
||||
{t("disband_team_confirmation_message")}
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
@ -281,19 +293,23 @@ export default function EditTeam(props: { team: Team | undefined | null; onClose
|
|||
<hr className="mt-8" />
|
||||
<div className="flex justify-end py-4">
|
||||
<Button type="submit" color="primary">
|
||||
Save
|
||||
{t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<Modal
|
||||
heading="Team updated successfully"
|
||||
description="Your team has been updated successfully."
|
||||
heading={t("team_updated_successfully")}
|
||||
description={t("your_team_updated_successfully")}
|
||||
open={successModalOpen}
|
||||
handleClose={closeSuccessModal}
|
||||
/>
|
||||
{showMemberInvitationModal && (
|
||||
<MemberInvitationModal team={inviteModalTeam} onExit={onMemberInvitationModalExit} />
|
||||
<MemberInvitationModal
|
||||
localeProp={locale}
|
||||
team={inviteModalTeam}
|
||||
onExit={onMemberInvitationModalExit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import { UsersIcon } from "@heroicons/react/outline";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { Team } from "@lib/team";
|
||||
|
||||
import Button from "@components/ui/Button";
|
||||
|
||||
export default function MemberInvitationModal(props: { team: Team | undefined | null; onExit: () => void }) {
|
||||
export default function MemberInvitationModal(props: {
|
||||
localeProp: string;
|
||||
team: Team | undefined | null;
|
||||
onExit: () => void;
|
||||
}) {
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const { t } = useLocale({ localeProp: props.localeProp });
|
||||
|
||||
const handleError = async (res: Response) => {
|
||||
const responseData = await res.json();
|
||||
|
@ -64,10 +70,10 @@ export default function MemberInvitationModal(props: { team: Team | undefined |
|
|||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||
Invite a new member
|
||||
{t("invite_new_member")}
|
||||
</h3>
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Invite someone to your team.</p>
|
||||
<p className="text-sm text-gray-400">{t("invite_new_team_member")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -75,7 +81,7 @@ export default function MemberInvitationModal(props: { team: Team | undefined |
|
|||
<div>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="inviteUser" className="block text-sm font-medium text-gray-700">
|
||||
Email or Username
|
||||
{t("email_or_username")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
|
@ -88,13 +94,13 @@ export default function MemberInvitationModal(props: { team: Team | undefined |
|
|||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block mb-2 text-sm font-medium tracking-wide text-gray-700" htmlFor="role">
|
||||
Role
|
||||
{t("role")}
|
||||
</label>
|
||||
<select
|
||||
id="role"
|
||||
className="block w-full mt-1 border-gray-300 rounded-md shadow-sm focus:ring-black focus:border-black sm:text-sm">
|
||||
<option value="MEMBER">Member</option>
|
||||
<option value="OWNER">Owner</option>
|
||||
<option value="MEMBER">{t("member")}</option>
|
||||
<option value="OWNER">{t("owner")}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="relative flex items-start">
|
||||
|
@ -109,7 +115,7 @@ export default function MemberInvitationModal(props: { team: Team | undefined |
|
|||
</div>
|
||||
<div className="ml-2 text-sm">
|
||||
<label htmlFor="sendInviteEmail" className="font-medium text-gray-700">
|
||||
Send an invite email
|
||||
{t("send_invite_email")}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -122,10 +128,10 @@ export default function MemberInvitationModal(props: { team: Team | undefined |
|
|||
)}
|
||||
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||
<Button type="submit" color="primary" className="ml-2">
|
||||
Invite
|
||||
{t("invite")}
|
||||
</Button>
|
||||
<Button type="button" color="secondary" onClick={props.onExit}>
|
||||
Cancel
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { Member } from "@lib/member";
|
||||
|
||||
import MemberListItem from "./MemberListItem";
|
||||
|
||||
export default function MemberList(props: {
|
||||
localeProp: string;
|
||||
members: Member[];
|
||||
onRemoveMember: (text: Member) => void;
|
||||
onChange: (text: string) => void;
|
||||
}) {
|
||||
const { locale } = useLocale({ localeProp: props.localeProp });
|
||||
|
||||
const selectAction = (action: string, member: Member) => {
|
||||
switch (action) {
|
||||
case "remove":
|
||||
|
@ -20,6 +24,7 @@ export default function MemberList(props: {
|
|||
<ul className="px-4 mb-2 bg-white border divide-y divide-gray-200 rounded">
|
||||
{props.members.map((member) => (
|
||||
<MemberListItem
|
||||
localeProp={locale}
|
||||
onChange={props.onChange}
|
||||
key={member.id}
|
||||
member={member}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { DotsHorizontalIcon, UserRemoveIcon } from "@heroicons/react/outline";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { Member } from "@lib/member";
|
||||
|
||||
import { Dialog, DialogTrigger } from "@components/Dialog";
|
||||
|
@ -11,11 +12,13 @@ import Button from "@components/ui/Button";
|
|||
import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/Dropdown";
|
||||
|
||||
export default function MemberListItem(props: {
|
||||
localeProp: string;
|
||||
member: Member;
|
||||
onActionSelect: (text: string) => void;
|
||||
onChange: (text: string) => void;
|
||||
}) {
|
||||
const [member] = useState(props.member);
|
||||
const { t, locale } = useLocale({ localeProp: props.localeProp });
|
||||
|
||||
return (
|
||||
member && (
|
||||
|
@ -41,21 +44,21 @@ export default function MemberListItem(props: {
|
|||
{props.member.role === "INVITEE" && (
|
||||
<>
|
||||
<span className="self-center h-6 px-3 py-1 mr-2 text-xs text-yellow-700 capitalize rounded-md bg-yellow-50">
|
||||
Pending
|
||||
{t("pending")}
|
||||
</span>
|
||||
<span className="self-center h-6 px-3 py-1 mr-4 text-xs text-pink-700 capitalize rounded-md bg-pink-50">
|
||||
Member
|
||||
{t("member")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{props.member.role === "MEMBER" && (
|
||||
<span className="self-center h-6 px-3 py-1 mr-4 text-xs text-pink-700 capitalize rounded-md bg-pink-50">
|
||||
Member
|
||||
{t("member")}
|
||||
</span>
|
||||
)}
|
||||
{props.member.role === "OWNER" && (
|
||||
<span className="self-center h-6 px-3 py-1 mr-4 text-xs text-blue-700 capitalize rounded-md bg-blue-50">
|
||||
Owner
|
||||
{t("owner")}
|
||||
</span>
|
||||
)}
|
||||
<Dropdown>
|
||||
|
@ -73,16 +76,16 @@ export default function MemberListItem(props: {
|
|||
color="warn"
|
||||
StartIcon={UserRemoveIcon}
|
||||
className="w-full">
|
||||
Remove User
|
||||
{t("remove_member")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<ConfirmationDialogContent
|
||||
localeProp={locale}
|
||||
variety="danger"
|
||||
title="Remove member"
|
||||
confirmBtnText="Yes, remove member"
|
||||
cancelBtnText="Cancel"
|
||||
title={t("remove_member")}
|
||||
confirmBtnText={t("confirm_remove_member")}
|
||||
onConfirm={() => props.onActionSelect("remove")}>
|
||||
Are you sure you want to remove this member from the team?
|
||||
{t("remove_member_confirmation_message")}
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
</DropdownMenuItem>
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { Team } from "@lib/team";
|
||||
|
||||
import TeamListItem from "./TeamListItem";
|
||||
|
||||
export default function TeamList(props: {
|
||||
localeProp: string;
|
||||
teams: Team[];
|
||||
onChange: () => void;
|
||||
onEditTeam: (text: Team) => void;
|
||||
}) {
|
||||
const { locale } = useLocale({ localeProp: props.localeProp });
|
||||
|
||||
const selectAction = (action: string, team: Team) => {
|
||||
switch (action) {
|
||||
case "edit":
|
||||
|
@ -30,6 +34,7 @@ export default function TeamList(props: {
|
|||
<ul className="px-4 mb-2 bg-white border divide-y divide-gray-200 rounded">
|
||||
{props.teams.map((team: Team) => (
|
||||
<TeamListItem
|
||||
localeProp={locale}
|
||||
onChange={props.onChange}
|
||||
key={team.id}
|
||||
team={team}
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import showToast from "@lib/notification";
|
||||
|
||||
import { Dialog, DialogTrigger } from "@components/Dialog";
|
||||
|
@ -30,12 +31,14 @@ interface Team {
|
|||
}
|
||||
|
||||
export default function TeamListItem(props: {
|
||||
localeProp: string;
|
||||
onChange: () => void;
|
||||
key: number;
|
||||
team: Team;
|
||||
onActionSelect: (text: string) => void;
|
||||
}) {
|
||||
const [team, setTeam] = useState<Team | null>(props.team);
|
||||
const { t, locale } = useLocale({ localeProp: props.localeProp });
|
||||
|
||||
const acceptInvite = () => invitationResponse(true);
|
||||
const declineInvite = () => invitationResponse(false);
|
||||
|
@ -79,24 +82,24 @@ export default function TeamListItem(props: {
|
|||
{props.team.role === "INVITEE" && (
|
||||
<div>
|
||||
<Button type="button" color="secondary" onClick={declineInvite}>
|
||||
Reject
|
||||
{t("reject")}
|
||||
</Button>
|
||||
<Button type="button" color="primary" className="ml-1" onClick={acceptInvite}>
|
||||
Accept
|
||||
{t("accept")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{props.team.role === "MEMBER" && (
|
||||
<div>
|
||||
<Button type="button" color="primary" onClick={declineInvite}>
|
||||
Leave
|
||||
{t("leave")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{props.team.role === "OWNER" && (
|
||||
<div className="flex space-x-4">
|
||||
<span className="self-center h-6 px-3 py-1 text-xs text-gray-700 capitalize rounded-md bg-gray-50">
|
||||
Owner
|
||||
{t("owner")}
|
||||
</span>
|
||||
<Tooltip content="Copy link">
|
||||
<Button
|
||||
|
@ -104,7 +107,7 @@ export default function TeamListItem(props: {
|
|||
navigator.clipboard.writeText(
|
||||
process.env.NEXT_PUBLIC_APP_URL + "/team/" + props.team.slug
|
||||
);
|
||||
showToast("Link copied!", "success");
|
||||
showToast(t("link_copied"), "success");
|
||||
}}
|
||||
size="icon"
|
||||
color="minimal"
|
||||
|
@ -124,14 +127,16 @@ export default function TeamListItem(props: {
|
|||
className="w-full"
|
||||
onClick={() => props.onActionSelect("edit")}
|
||||
StartIcon={PencilAltIcon}>
|
||||
Edit team
|
||||
{" "}
|
||||
{t("edit_team")}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Link href={`${process.env.NEXT_PUBLIC_APP_URL}/team/${props.team.slug}`} passHref={true}>
|
||||
<a target="_blank">
|
||||
<Button type="button" color="minimal" className="w-full" StartIcon={ExternalLinkIcon}>
|
||||
Preview team page
|
||||
{" "}
|
||||
{t("preview_team")}
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
|
@ -146,17 +151,16 @@ export default function TeamListItem(props: {
|
|||
color="warn"
|
||||
StartIcon={TrashIcon}
|
||||
className="w-full">
|
||||
Disband Team
|
||||
{t("disband_team")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<ConfirmationDialogContent
|
||||
localeProp={locale}
|
||||
variety="danger"
|
||||
title="Disband Team"
|
||||
confirmBtnText="Yes, disband team"
|
||||
cancelBtnText="Cancel"
|
||||
confirmBtnText={t("confirm_disband_team")}
|
||||
onConfirm={() => props.onActionSelect("disband")}>
|
||||
Are you sure you want to disband this team? Anyone who you've shared this team
|
||||
link with will no longer be able to book using it.
|
||||
{t("disband_team_confirmation_message")}
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
</DropdownMenuItem>
|
||||
|
|
|
@ -4,11 +4,15 @@ import classnames from "classnames";
|
|||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import Avatar from "@components/ui/Avatar";
|
||||
import Button from "@components/ui/Button";
|
||||
import Text from "@components/ui/Text";
|
||||
|
||||
const Team = ({ team }) => {
|
||||
const Team = ({ team, localeProp }) => {
|
||||
const { t } = useLocale({ localeProp: localeProp });
|
||||
|
||||
const Member = ({ member }) => {
|
||||
const classes = classnames(
|
||||
"group",
|
||||
|
@ -71,7 +75,7 @@ const Team = ({ team }) => {
|
|||
{team.eventTypes.length > 0 && (
|
||||
<aside className="text-center dark:text-white mt-8">
|
||||
<Button color="secondary" href={`/team/${team.slug}`} shallow={true} StartIcon={ArrowLeftIcon}>
|
||||
Go back
|
||||
{t("go_back")}
|
||||
</Button>
|
||||
</aside>
|
||||
)}
|
||||
|
|
127
crowdin.yml
Normal file
127
crowdin.yml
Normal file
|
@ -0,0 +1,127 @@
|
|||
#
|
||||
# Your Crowdin credentials
|
||||
#
|
||||
"project_id_env" : "CROWDIN_PROJECT_ID"
|
||||
"api_token_env" : "CROWDIN_PERSONAL_TOKEN"
|
||||
"base_path" : "."
|
||||
"base_url" : "https://cal.crowdin.com"
|
||||
|
||||
#
|
||||
# Choose file structure in Crowdin
|
||||
# e.g. true or false
|
||||
#
|
||||
"preserve_hierarchy": true
|
||||
|
||||
#
|
||||
# Files configuration
|
||||
#
|
||||
files: [
|
||||
{
|
||||
#
|
||||
# Source files filter
|
||||
# e.g. "/resources/en/*.json"
|
||||
#
|
||||
"source" : "/public/static/locales/en/*.json",
|
||||
|
||||
#
|
||||
# Where translations will be placed
|
||||
# e.g. "/resources/%two_letters_code%/%original_file_name%"
|
||||
#
|
||||
"translation" : "/public/static/locales/%two_letters_code%/%original_file_name%",
|
||||
|
||||
#
|
||||
# Files or directories for ignore
|
||||
# e.g. ["/**/?.txt", "/**/[0-9].txt", "/**/*\?*.txt"]
|
||||
#
|
||||
#"ignore" : [],
|
||||
|
||||
#
|
||||
# The dest allows you to specify a file name in Crowdin
|
||||
# e.g. "/messages.json"
|
||||
#
|
||||
#"dest" : "",
|
||||
|
||||
#
|
||||
# File type
|
||||
# e.g. "json"
|
||||
#
|
||||
#"type" : "",
|
||||
|
||||
#
|
||||
# The parameter "update_option" is optional. If it is not set, after the files update the translations for changed strings will be removed. Use to fix typos and for minor changes in the source strings
|
||||
# e.g. "update_as_unapproved" or "update_without_changes"
|
||||
#
|
||||
#"update_option" : "",
|
||||
|
||||
#
|
||||
# Start block (for XML only)
|
||||
#
|
||||
|
||||
#
|
||||
# Defines whether to translate tags attributes.
|
||||
# e.g. 0 or 1 (Default is 1)
|
||||
#
|
||||
# "translate_attributes" : 1,
|
||||
|
||||
#
|
||||
# Defines whether to translate texts placed inside the tags.
|
||||
# e.g. 0 or 1 (Default is 1)
|
||||
#
|
||||
# "translate_content" : 1,
|
||||
|
||||
#
|
||||
# This is an array of strings, where each item is the XPaths to DOM element that should be imported
|
||||
# e.g. ["/content/text", "/content/text[@value]"]
|
||||
#
|
||||
# "translatable_elements" : [],
|
||||
|
||||
#
|
||||
# Defines whether to split long texts into smaller text segments
|
||||
# e.g. 0 or 1 (Default is 1)
|
||||
#
|
||||
# "content_segmentation" : 1,
|
||||
|
||||
#
|
||||
# End block (for XML only)
|
||||
#
|
||||
|
||||
#
|
||||
# Start .properties block
|
||||
#
|
||||
|
||||
#
|
||||
# Defines whether single quote should be escaped by another single quote or backslash in exported translations
|
||||
# e.g. 0 or 1 or 2 or 3 (Default is 3)
|
||||
# 0 - do not escape single quote;
|
||||
# 1 - escape single quote by another single quote;
|
||||
# 2 - escape single quote by backslash;
|
||||
# 3 - escape single quote by another single quote only in strings containing variables ( {0} ).
|
||||
#
|
||||
# "escape_quotes" : 3,
|
||||
|
||||
#
|
||||
# Defines whether any special characters (=, :, ! and #) should be escaped by backslash in exported translations.
|
||||
# e.g. 0 or 1 (Default is 0)
|
||||
# 0 - do not escape special characters
|
||||
# 1 - escape special characters by a backslash
|
||||
#
|
||||
# "escape_special_characters": 0
|
||||
#
|
||||
|
||||
#
|
||||
# End .properties block
|
||||
#
|
||||
|
||||
#
|
||||
# Does the first line contain header?
|
||||
# e.g. true or false
|
||||
#
|
||||
#"first_line_contains_header" : true,
|
||||
|
||||
#
|
||||
# for spreadsheets
|
||||
# e.g. "identifier,source_phrase,context,uk,ru,fr"
|
||||
#
|
||||
# "scheme" : "",
|
||||
}
|
||||
]
|
|
@ -6,8 +6,8 @@ import prisma from "@lib/prisma";
|
|||
|
||||
import { i18n } from "../../../next-i18next.config";
|
||||
|
||||
export const extractLocaleInfo = async (req: IncomingMessage) => {
|
||||
const session = await getSession({ req: req });
|
||||
export const getOrSetUserLocaleFromHeaders = async (req: IncomingMessage) => {
|
||||
const session = await getSession({ req });
|
||||
const preferredLocale = parser.pick(i18n.locales, req.headers["accept-language"]);
|
||||
|
||||
if (session?.user?.id) {
|
||||
|
@ -58,7 +58,14 @@ interface localeType {
|
|||
|
||||
export const localeLabels: localeType = {
|
||||
en: "English",
|
||||
fr: "French",
|
||||
it: "Italian",
|
||||
ru: "Russian",
|
||||
es: "Spanish",
|
||||
de: "German",
|
||||
pt: "Portuguese",
|
||||
ro: "Romanian",
|
||||
nl: "Dutch",
|
||||
};
|
||||
|
||||
export type OptionType = {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { useTranslation } from "next-i18next";
|
||||
|
||||
type LocaleProps = {
|
||||
type LocaleProp = {
|
||||
localeProp: string;
|
||||
};
|
||||
|
||||
export const useLocale = (props: LocaleProps) => {
|
||||
export const useLocale = (props: LocaleProp) => {
|
||||
const { i18n, t } = useTranslation("common");
|
||||
|
||||
if (i18n.language !== props.localeProp) {
|
||||
|
|
|
@ -4,7 +4,7 @@ const path = require("path");
|
|||
module.exports = {
|
||||
i18n: {
|
||||
defaultLocale: "en",
|
||||
locales: ["en", "ro"],
|
||||
locales: ["en", "fr", "it", "ru", "es", "de", "pt", "ro", "nl"],
|
||||
},
|
||||
localePath: path.resolve("./public/static/locales"),
|
||||
};
|
||||
|
|
119
pages/[user].tsx
119
pages/[user].tsx
|
@ -1,29 +1,23 @@
|
|||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||
import { GetStaticPaths, GetStaticPropsContext } from "next";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import useTheme from "@lib/hooks/useTheme";
|
||||
import prisma from "@lib/prisma";
|
||||
import { trpc } from "@lib/trpc";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import EventTypeDescription from "@components/eventtype/EventTypeDescription";
|
||||
import { HeadSeo } from "@components/seo/head-seo";
|
||||
import Avatar from "@components/ui/Avatar";
|
||||
|
||||
import { ssg } from "@server/ssg";
|
||||
|
||||
export default function User(props: inferSSRProps<typeof getStaticProps>) {
|
||||
const { username } = props;
|
||||
// data of query below will be will be prepopulated b/c of `getStaticProps`
|
||||
const query = trpc.useQuery(["booking.userEventTypes", { username }]);
|
||||
const { isReady } = useTheme(query.data?.user.theme);
|
||||
if (!query.data) {
|
||||
// this shold never happen as we do `blocking: true`
|
||||
return <>...</>;
|
||||
}
|
||||
const { user, eventTypes } = query.data;
|
||||
export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
const { isReady } = useTheme(props.user.theme);
|
||||
const { user, localeProp, eventTypes } = props;
|
||||
const { t, locale } = useLocale({ localeProp });
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -56,7 +50,7 @@ export default function User(props: inferSSRProps<typeof getStaticProps>) {
|
|||
<Link href={`/${user.username}/${type.slug}`}>
|
||||
<a className="block px-6 py-4">
|
||||
<h2 className="font-semibold text-neutral-900 dark:text-white">{type.title}</h2>
|
||||
<EventTypeDescription eventType={type} />
|
||||
<EventTypeDescription localeProp={locale} eventType={type} />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
@ -65,8 +59,10 @@ export default function User(props: inferSSRProps<typeof getStaticProps>) {
|
|||
{eventTypes.length === 0 && (
|
||||
<div className="shadow overflow-hidden rounded-sm">
|
||||
<div className="p-8 text-center text-gray-400 dark:text-white">
|
||||
<h2 className="font-cal font-semibold text-3xl text-gray-600 dark:text-white">Uh oh!</h2>
|
||||
<p className="max-w-md mx-auto">This user hasn't set up any event types yet.</p>
|
||||
<h2 className="font-cal font-semibold text-3xl text-gray-600 dark:text-white">
|
||||
{t("uh_oh")}
|
||||
</h2>
|
||||
<p className="max-w-md mx-auto">{t("no_event_types_have_been_setup")}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -77,43 +73,76 @@ export default function User(props: inferSSRProps<typeof getStaticProps>) {
|
|||
);
|
||||
}
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const allUsers = await prisma.user.findMany({
|
||||
select: {
|
||||
username: true,
|
||||
},
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const username = (context.query.user as string).toLowerCase();
|
||||
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
// will statically render everyone on the PRO plan
|
||||
// the rest will be statically rendered on first visit
|
||||
plan: "PRO",
|
||||
username: username.toLowerCase(),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
name: true,
|
||||
bio: true,
|
||||
avatar: true,
|
||||
theme: true,
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
const usernames = allUsers.flatMap((u) => (u.username ? [u.username] : []));
|
||||
return {
|
||||
paths: usernames.map((user) => ({
|
||||
params: { user },
|
||||
})),
|
||||
|
||||
// https://nextjs.org/docs/basic-features/data-fetching#fallback-blocking
|
||||
fallback: "blocking",
|
||||
};
|
||||
};
|
||||
|
||||
export async function getStaticProps(context: GetStaticPropsContext<{ user: string }>) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const username = context.params!.user;
|
||||
const data = await ssg.fetchQuery("booking.userEventTypes", { username });
|
||||
|
||||
if (!data) {
|
||||
if (!user) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
const eventTypesWithHidden = await prisma.eventType.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
teamId: null,
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
{
|
||||
users: {
|
||||
some: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
title: true,
|
||||
length: true,
|
||||
description: true,
|
||||
hidden: true,
|
||||
schedulingType: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
},
|
||||
take: user.plan === "FREE" ? 1 : undefined,
|
||||
});
|
||||
|
||||
const eventTypes = eventTypesWithHidden.filter((evt) => !evt.hidden);
|
||||
|
||||
return {
|
||||
props: {
|
||||
trpcState: ssg.dehydrate(),
|
||||
username,
|
||||
localeProp: locale,
|
||||
user,
|
||||
eventTypes,
|
||||
...(await serverSideTranslations(locale, ["common"])),
|
||||
},
|
||||
revalidate: 1,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@ import { GetServerSidePropsContext } from "next";
|
|||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { extractLocaleInfo } from "@lib/core/i18n/i18n.utils";
|
||||
import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
|
||||
import prisma from "@lib/prisma";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
|
@ -16,7 +16,9 @@ export default function Type(props: AvailabilityPageProps) {
|
|||
}
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const locale = await extractLocaleInfo(context.req);
|
||||
const locale = await getOrSetUserLocaleFromHeaders(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);
|
||||
const typeParam = asStringOrNull(context.query.type);
|
||||
const dateParam = asStringOrNull(context.query.date);
|
||||
|
|
|
@ -5,7 +5,7 @@ import { GetServerSidePropsContext } from "next";
|
|||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
|
||||
import { asStringOrThrow } from "@lib/asStringOrNull";
|
||||
import { extractLocaleInfo } from "@lib/core/i18n/i18n.utils";
|
||||
import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
|
||||
import prisma from "@lib/prisma";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
|
@ -21,7 +21,7 @@ export default function Book(props: BookPageProps) {
|
|||
}
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const locale = await extractLocaleInfo(context.req);
|
||||
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
|
|
|
@ -37,7 +37,8 @@ import {
|
|||
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 { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import getIntegrations, { hasIntegration } from "@lib/integrations/getIntegrations";
|
||||
import { LocationType } from "@lib/location";
|
||||
import deleteEventType from "@lib/mutations/event-types/delete-event-type";
|
||||
|
@ -84,6 +85,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
const { eventType, locationOptions, availability, team, teamMembers, hasPaymentIntegration, currency } =
|
||||
props;
|
||||
|
||||
const { locale } = useLocale({ localeProp: props.localeProp });
|
||||
const router = useRouter();
|
||||
const [successModalOpen, setSuccessModalOpen] = useState(false);
|
||||
|
||||
|
@ -983,6 +985,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
Delete
|
||||
</DialogTrigger>
|
||||
<ConfirmationDialogContent
|
||||
localeProp={locale}
|
||||
variety="danger"
|
||||
title="Delete Event Type"
|
||||
confirmBtnText="Yes, delete event type"
|
||||
|
@ -1097,7 +1100,7 @@ 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 locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||
|
||||
const typeParam = parseInt(asStringOrThrow(query.type));
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ 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 { getOrSetUserLocaleFromHeaders } 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";
|
||||
|
@ -56,10 +56,7 @@ 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 { locale } = useLocale({ localeProp: props.localeProp });
|
||||
|
||||
const CreateFirstEventTypeView = () => (
|
||||
<div className="md:py-20">
|
||||
|
@ -164,7 +161,7 @@ const EventTypesPage = (props: PageProps) => {
|
|||
</span>
|
||||
)}
|
||||
</div>
|
||||
<EventTypeDescription eventType={type} />
|
||||
<EventTypeDescription localeProp={locale} eventType={type} />
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
|
@ -379,13 +376,13 @@ const CreateNewEventDialog = ({
|
|||
disabled: true,
|
||||
})}
|
||||
StartIcon={PlusIcon}>
|
||||
{t("new-event-type-btn")}
|
||||
{t("new_event_type_btn")}
|
||||
</Button>
|
||||
)}
|
||||
{profiles.filter((profile) => profile.teamId).length > 0 && (
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button EndIcon={ChevronDownIcon}>{t("new-event-type-btn")}</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>
|
||||
|
@ -563,7 +560,7 @@ const CreateNewEventDialog = ({
|
|||
|
||||
export async function getServerSideProps(context) {
|
||||
const session = await getSession(context);
|
||||
const locale = await extractLocaleInfo(context.req);
|
||||
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||
|
|
|
@ -7,7 +7,12 @@ import TimezoneSelect from "react-timezone-select";
|
|||
|
||||
import { asStringOrNull, asStringOrUndefined } from "@lib/asStringOrNull";
|
||||
import { getSession } from "@lib/auth";
|
||||
import { extractLocaleInfo, localeLabels, localeOptions, OptionType } from "@lib/core/i18n/i18n.utils";
|
||||
import {
|
||||
getOrSetUserLocaleFromHeaders,
|
||||
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";
|
||||
|
@ -416,7 +421,7 @@ export default function Settings(props: Props) {
|
|||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const session = await getSession(context);
|
||||
const locale = await extractLocaleInfo(context.req);
|
||||
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import React from "react";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
import SettingsShell from "@components/SettingsShell";
|
||||
|
@ -8,12 +11,14 @@ import Shell from "@components/Shell";
|
|||
import ChangePasswordSection from "@components/security/ChangePasswordSection";
|
||||
import TwoFactorAuthSection from "@components/security/TwoFactorAuthSection";
|
||||
|
||||
export default function Security({ user }) {
|
||||
export default function Security({ user, localeProp }) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { locale, t } = useLocale({ localeProp });
|
||||
return (
|
||||
<Shell heading="Security" subtitle="Manage your account's security.">
|
||||
<Shell heading={t("security")} subtitle={t("manage_account_security")}>
|
||||
<SettingsShell>
|
||||
<ChangePasswordSection />
|
||||
<TwoFactorAuthSection twoFactorEnabled={user.twoFactorEnabled} />
|
||||
<ChangePasswordSection localeProp={locale} />
|
||||
<TwoFactorAuthSection localeProp={locale} twoFactorEnabled={user.twoFactorEnabled} />
|
||||
</SettingsShell>
|
||||
</Shell>
|
||||
);
|
||||
|
@ -21,6 +26,8 @@ export default function Security({ user }) {
|
|||
|
||||
export async function getServerSideProps(context) {
|
||||
const session = await getSession(context);
|
||||
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||
}
|
||||
|
@ -38,6 +45,11 @@ export async function getServerSideProps(context) {
|
|||
});
|
||||
|
||||
return {
|
||||
props: { session, user },
|
||||
props: {
|
||||
localeProp: locale,
|
||||
session,
|
||||
user,
|
||||
...(await serverSideTranslations(locale, ["common"])),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,9 +3,12 @@ import { PlusIcon } from "@heroicons/react/solid";
|
|||
import { GetServerSideProps } from "next";
|
||||
import type { Session } from "next-auth";
|
||||
import { useSession } from "next-auth/client";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { Member } from "@lib/member";
|
||||
import { Team } from "@lib/team";
|
||||
|
||||
|
@ -17,7 +20,7 @@ import TeamList from "@components/team/TeamList";
|
|||
import TeamListItem from "@components/team/TeamListItem";
|
||||
import Button from "@components/ui/Button";
|
||||
|
||||
export default function Teams() {
|
||||
export default function Teams(props: { localeProp: string }) {
|
||||
const noop = () => undefined;
|
||||
const [, loading] = useSession();
|
||||
const [teams, setTeams] = useState([]);
|
||||
|
@ -26,6 +29,7 @@ export default function Teams() {
|
|||
const [editTeamEnabled, setEditTeamEnabled] = useState(false);
|
||||
const [teamToEdit, setTeamToEdit] = useState<Team | null>();
|
||||
const nameRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
|
||||
const { locale } = useLocale({ localeProp: props.localeProp });
|
||||
|
||||
const handleErrors = async (resp: Response) => {
|
||||
if (!resp.ok) {
|
||||
|
@ -110,7 +114,11 @@ export default function Teams() {
|
|||
</div>
|
||||
<div>
|
||||
{!!teams.length && (
|
||||
<TeamList teams={teams} onChange={loadData} onEditTeam={editTeam}></TeamList>
|
||||
<TeamList
|
||||
localeProp={locale}
|
||||
teams={teams}
|
||||
onChange={loadData}
|
||||
onEditTeam={editTeam}></TeamList>
|
||||
)}
|
||||
|
||||
{!!invites.length && (
|
||||
|
@ -119,6 +127,7 @@ export default function Teams() {
|
|||
<ul className="px-4 mt-4 mb-2 bg-white border divide-y divide-gray-200 rounded">
|
||||
{invites.map((team: Team) => (
|
||||
<TeamListItem
|
||||
localeProp={locale}
|
||||
onChange={loadData}
|
||||
key={team.id}
|
||||
team={team}
|
||||
|
@ -131,7 +140,7 @@ export default function Teams() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!!editTeamEnabled && <EditTeam team={teamToEdit} onCloseEdit={onCloseEdit} />}
|
||||
{!!editTeamEnabled && <EditTeam localeProp={locale} team={teamToEdit} onCloseEdit={onCloseEdit} />}
|
||||
{showCreateTeamModal && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 overflow-y-auto"
|
||||
|
@ -200,11 +209,16 @@ export default function Teams() {
|
|||
// Export the `session` prop to use sessions with Server Side Rendering
|
||||
export const getServerSideProps: GetServerSideProps<{ session: Session | null }> = async (context) => {
|
||||
const session = await getSession(context);
|
||||
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||
if (!session) {
|
||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||
}
|
||||
|
||||
return {
|
||||
props: { session },
|
||||
props: {
|
||||
session,
|
||||
localeProp: locale,
|
||||
...(await serverSideTranslations(locale, ["common"])),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import { ArrowRightIcon } from "@heroicons/react/solid";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import useTheme from "@lib/hooks/useTheme";
|
||||
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
|
||||
import prisma from "@lib/prisma";
|
||||
|
@ -18,9 +21,10 @@ import AvatarGroup from "@components/ui/AvatarGroup";
|
|||
import Button from "@components/ui/Button";
|
||||
import Text from "@components/ui/Text";
|
||||
|
||||
function TeamPage({ team }: inferSSRProps<typeof getServerSideProps>) {
|
||||
function TeamPage({ team, localeProp }: inferSSRProps<typeof getServerSideProps>) {
|
||||
const { isReady } = useTheme();
|
||||
const showMembers = useToggleQuery("members");
|
||||
const { t, locale } = useLocale({ localeProp: localeProp });
|
||||
|
||||
const eventTypes = (
|
||||
<ul className="space-y-3">
|
||||
|
@ -33,7 +37,7 @@ function TeamPage({ team }: inferSSRProps<typeof getServerSideProps>) {
|
|||
<a className="px-6 py-4 flex justify-between">
|
||||
<div className="flex-shrink">
|
||||
<h2 className="font-cal font-semibold text-neutral-900 dark:text-white">{type.title}</h2>
|
||||
<EventTypeDescription className="text-sm" eventType={type} />
|
||||
<EventTypeDescription localeProp={locale} className="text-sm" eventType={type} />
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<AvatarGroup
|
||||
|
@ -64,7 +68,7 @@ function TeamPage({ team }: inferSSRProps<typeof getServerSideProps>) {
|
|||
<Avatar alt={teamName} imageSrc={team.logo} className="mx-auto w-20 h-20 rounded-full mb-4" />
|
||||
<Text variant="headline">{teamName}</Text>
|
||||
</div>
|
||||
{(showMembers.isOn || !team.eventTypes.length) && <Team team={team} />}
|
||||
{(showMembers.isOn || !team.eventTypes.length) && <Team localeProp={locale} team={team} />}
|
||||
{!showMembers.isOn && team.eventTypes.length > 0 && (
|
||||
<div className="mx-auto max-w-3xl">
|
||||
{eventTypes}
|
||||
|
@ -75,7 +79,7 @@ function TeamPage({ team }: inferSSRProps<typeof getServerSideProps>) {
|
|||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="px-2 bg-gray-100 text-sm text-gray-500 dark:bg-black dark:text-gray-500">
|
||||
OR
|
||||
{t("or")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -86,7 +90,7 @@ function TeamPage({ team }: inferSSRProps<typeof getServerSideProps>) {
|
|||
EndIcon={ArrowRightIcon}
|
||||
href={`/team/${team.slug}?members=1`}
|
||||
shallow={true}>
|
||||
Book a team member instead
|
||||
{t("book_a_team_member")}
|
||||
</Button>
|
||||
</aside>
|
||||
</div>
|
||||
|
@ -98,6 +102,7 @@ function TeamPage({ team }: inferSSRProps<typeof getServerSideProps>) {
|
|||
}
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||
const slug = Array.isArray(context.query?.slug) ? context.query.slug.pop() : context.query.slug;
|
||||
|
||||
const userSelect = Prisma.validator<Prisma.UserSelect>()({
|
||||
|
@ -160,7 +165,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
|
||||
return {
|
||||
props: {
|
||||
localeProp: locale,
|
||||
team,
|
||||
...(await serverSideTranslations(locale, ["common"])),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@ import { GetServerSidePropsContext } from "next";
|
|||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { extractLocaleInfo } from "@lib/core/i18n/i18n.utils";
|
||||
import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
|
||||
import prisma from "@lib/prisma";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
|
@ -15,7 +15,7 @@ export default function TeamType(props: AvailabilityTeamPageProps) {
|
|||
}
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const locale = await extractLocaleInfo(context.req);
|
||||
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||
const slugParam = asStringOrNull(context.query.slug);
|
||||
const typeParam = asStringOrNull(context.query.type);
|
||||
const dateParam = asStringOrNull(context.query.date);
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { GetServerSidePropsContext } from "next";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import "react-phone-number-input/style.css";
|
||||
|
||||
import { asStringOrThrow } from "@lib/asStringOrNull";
|
||||
import { getOrSetUserLocaleFromHeaders } from "@lib/core/i18n/i18n.utils";
|
||||
import prisma from "@lib/prisma";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
|
@ -14,6 +16,7 @@ export default function TeamBookingPage(props: TeamBookingPageProps) {
|
|||
}
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const locale = await getOrSetUserLocaleFromHeaders(context.req);
|
||||
const eventTypeId = parseInt(asStringOrThrow(context.query.type));
|
||||
if (typeof eventTypeId !== "number" || eventTypeId % 1 !== 0) {
|
||||
return {
|
||||
|
@ -86,6 +89,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
|
||||
return {
|
||||
props: {
|
||||
localeProp: locale,
|
||||
profile: {
|
||||
...eventTypeObject.team,
|
||||
slug: "team/" + eventTypeObject.slug,
|
||||
|
@ -94,6 +98,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
},
|
||||
eventType: eventTypeObject,
|
||||
booking,
|
||||
...(await serverSideTranslations(locale, ["common"])),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
147
public/static/locales/de/common.json
Normal file
147
public/static/locales/de/common.json
Normal file
|
@ -0,0 +1,147 @@
|
|||
{
|
||||
"edit_logo": "Logo bearbeiten",
|
||||
"upload_a_logo": "Logo hochladen",
|
||||
"enable": "Aktivieren",
|
||||
"code": "Code",
|
||||
"code_is_incorrect": "Code ist falsch.",
|
||||
"add_an_extra_layer_of_security": "Fügen Sie Ihrem Konto eine zusätzliche Sicherheitsstufe hinzu, falls Ihr Passwort gestohlen wird.",
|
||||
"2fa": "Zwei-Faktor-Authentifizierung",
|
||||
"enable_2fa": "Zwei-Faktor-Authentifizierung aktivieren",
|
||||
"disable_2fa": "Zwei-Faktor-Authentifizierung deaktivieren",
|
||||
"disable_2fa_recommendation": "Falls Sie 2FA deaktivieren müssen, empfehlen wir, es so schnell wie möglich wieder zu aktivieren.",
|
||||
"error_disabling_2fa": "Fehler beim Deaktivieren der Zwei-Faktor-Authentifizierung",
|
||||
"error_enabling_2fa": "Fehler beim Einrichten der Zwei-Faktor-Authentifizierung",
|
||||
"security": "Sicherheit",
|
||||
"manage_account_security": "Verwalten Sie die Sicherheit Ihres Kontos.",
|
||||
"password": "Passwort",
|
||||
"password_updated_successfully": "Passwort erfolgreich aktualisiert",
|
||||
"password_has_been_changed": "Ihr Passwort wurde erfolgreich geändert.",
|
||||
"error_changing_password": "Fehler beim Ändern des Passworts",
|
||||
"something_went_wrong": "Etwas ist schief gelaufen",
|
||||
"please_try_again": "Bitte erneut versuchen",
|
||||
"super_secure_new_password": "Ihr supersicheres neues Passwort",
|
||||
"new_password": "Neues Passwort",
|
||||
"your_old_password": "Ihr altes Passwort",
|
||||
"current_password": "Aktuelles Passwort",
|
||||
"change_password": "Passwort ändern",
|
||||
"new_password_matches_old_password": "Neues Passwort stimmt mit Ihrem alten Passwort überein. Bitte wählen Sie ein anderes Passwort.",
|
||||
"current_incorrect_password": "Aktuelles Passwort ist falsch",
|
||||
"incorrect_password": "Passwort ist falsch",
|
||||
"1_on_1": "1-on-1",
|
||||
"24_h": "24 Std",
|
||||
"use_setting": "Benutze Einstellung",
|
||||
"am_pm": "am/pm",
|
||||
"time_options": "Zeitoptionen",
|
||||
"january": "Januar",
|
||||
"february": "Februar",
|
||||
"march": "März",
|
||||
"april": "April",
|
||||
"may": "Mai",
|
||||
"june": "Juni",
|
||||
"july": "Juli",
|
||||
"august": "August",
|
||||
"september": "September",
|
||||
"october": "Oktober",
|
||||
"november": "November",
|
||||
"december": "Dezember",
|
||||
"monday": "Montag",
|
||||
"tuesday": "Dienstag",
|
||||
"wednesday": "Mittwoch",
|
||||
"thursday": "Donnerstag",
|
||||
"friday": "Freitag",
|
||||
"saturday": "Samstag",
|
||||
"sunday": "Sonntag",
|
||||
"all_booked_today": "Ausgebucht für heute.",
|
||||
"slots_load_fail": "Die verfügbaren Zeitfenster konnten nicht geladen werden.",
|
||||
"additional_guests": "+ Weitere Gäste",
|
||||
"your_name": "Ihr Name",
|
||||
"email_address": "E-Mail Adresse",
|
||||
"location": "Ort",
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
"additional_notes": "Zusätzliche Notizen",
|
||||
"booking_fail": "Termin konnte nicht gebucht werden.",
|
||||
"reschedule_fail": "Termin konnte nicht neugeplant werden.",
|
||||
"share_additional_notes": "Bitten teilen Sie Notizen zur Vorbereitung des Termins, falls nötig.",
|
||||
"booking_confirmation": "Bestätigen Sie {{eventTypeTitle}} mit {{profileName}}",
|
||||
"booking_reschedule_confirmation": "Planen Sie Ihr {{eventTypeTitle}} mit {{profileName}} um",
|
||||
"in_person_meeting": "Link oder Vor-Ort-Termin",
|
||||
"phone_call": "Telefonat",
|
||||
"phone_number": "Telefonnummer",
|
||||
"enter_phone_number": "Telefonnummer eingeben",
|
||||
"reschedule": "Neuplanen",
|
||||
"book_a_team_member": "Teammitglied stattdessen buchen",
|
||||
"or": "ODER",
|
||||
"go_back": "Zurück",
|
||||
"email_or_username": "E-Mail oder Benutzername",
|
||||
"send_invite_email": "Einladungs-E-Mail senden",
|
||||
"role": "Rolle",
|
||||
"edit_team": "Team bearbeiten",
|
||||
"reject": "Ablehnen",
|
||||
"accept": "Annehmen",
|
||||
"leave": "Verlassen",
|
||||
"profile": "Profil",
|
||||
"my_team_url": "Meine Team-URL",
|
||||
"team_name": "Teamname",
|
||||
"your_team_name": "Ihr Teamname",
|
||||
"team_updated_successfully": "Team erfolgreich aktualisiert",
|
||||
"your_team_updated_successfully": "Ihr Team wurde erfolgreich aktualisiert.",
|
||||
"about": "Beschreibung",
|
||||
"team_description": "Ein paar Sätze über Ihr Team auf der öffentlichen Teamseite.",
|
||||
"members": "Mitglieder",
|
||||
"member": "Mitglied",
|
||||
"owner": "Inhaber",
|
||||
"new_member": "Neues Mitglied",
|
||||
"invite": "Einladen",
|
||||
"invite_new_member": "Ein neues Mitglied einladen",
|
||||
"invite_new_team_member": "Laden Sie jemanden in Ihr Team ein.",
|
||||
"disable_cal_branding": "Cal.com Werbung deaktivieren",
|
||||
"disable_cal_branding_description": "Verstecken Sie Cal.com Werbung auf Ihren öffentlichen Seiten.",
|
||||
"danger_zone": "Achtung",
|
||||
"back": "Zurück",
|
||||
"cancel": "Abbrechen",
|
||||
"continue": "Weiter",
|
||||
"confirm": "Bestätigen",
|
||||
"disband_team": "Team auflösen",
|
||||
"disband_team_confirmation_message": "Bist du sicher, dass du dieses Team auflösen möchtest? Jeder der diesen Team-Link erhalten hat, kann Sie nicht mehr buchen.",
|
||||
"remove_member_confirmation_message": "Sind Sie sicher, dass Sie dieses Mitglied aus dem Team entfernen möchten?",
|
||||
"confirm_disband_team": "Ja, Team auflösen",
|
||||
"confirm_remove_member": "Ja, Mitglied entfernen",
|
||||
"remove_member": "Mitglied entfernen",
|
||||
"manage_your_team": "Team verwalten",
|
||||
"submit": "Abschicken",
|
||||
"delete": "Löschen",
|
||||
"update": "Aktualisieren",
|
||||
"save": "Speichern",
|
||||
"pending": "Ausstehend",
|
||||
"open_options": "Optionen öffnen",
|
||||
"copy_link": "Link kopieren",
|
||||
"preview": "Vorschau",
|
||||
"link_copied": "Link kopiert!",
|
||||
"title": "Titel",
|
||||
"description": "Beschreibung",
|
||||
"quick_video_meeting": "Ein schnelles Video-Meeting.",
|
||||
"scheduling_type": "Termintyp",
|
||||
"preview_team": "Teamvorschau",
|
||||
"collective": "Kollektiv",
|
||||
"collective_description": "Planen Sie Meetings, bei denen alle ausgewählten Teammitglieder verfügbar sind.",
|
||||
"duration": "Dauer",
|
||||
"minutes": "Minuten",
|
||||
"round_robin": "Round Robin",
|
||||
"round_robin_description": "Treffen zwischen mehreren Teammitgliedern durchwechseln.",
|
||||
"url": "URL",
|
||||
"hidden": "Versteckt",
|
||||
"readonly": "Schreibgeschützt",
|
||||
"plan_upgrade": "Sie müssen Ihr Paket upgraden, um mehr als einen aktiven Ereignistyp zu haben.",
|
||||
"plan_upgrade_instructions": "Zum Upgrade, gehen Sie auf <a href=\"https://cal.com/upgrade\" className=\"underline\">https://cal.com/upgrade</a>",
|
||||
"event_types_page_title": "Ereignistypen",
|
||||
"event_types_page_subtitle": "Erstellen Sie teilbare Ereignisse, die andere Personen buchen können.",
|
||||
"new_event_type_btn": "Neuer Ereignistyp",
|
||||
"new_event_type_heading": "Erstellen Sie Ihren ersten Ereignistyp",
|
||||
"new_event_type_description": "Mit Ereignistypen kann man verfügbare Zeiten im Kalendar freigeben, die andere Personen dann buchen können.",
|
||||
"new_event_title": "Neuen Ereignistyp hinzufügen",
|
||||
"new_event_subtitle": "Erstellen Sie einen Ereignistyp unter Ihrem Namen oder einem Team.",
|
||||
"new_team_event": "Neuen Ereignistyp hinzufügen",
|
||||
"new_event_description": "Erstellen Sie einen neuen Ereignistyp, mit dem Personen Zeiten buchen können.",
|
||||
"event_type_created_successfully": "{{eventTypeTitle}} Ereignistyp erfolgreich erstellt"
|
||||
}
|
|
@ -1,3 +1,149 @@
|
|||
{
|
||||
"new-event-type-btn": "New event type"
|
||||
"uh_oh": "Uh oh!",
|
||||
"no_event_types_have_been_setup": "This user hasn't set up any event types yet.",
|
||||
"edit_logo": "Edit logo",
|
||||
"upload_a_logo": "Upload a logo",
|
||||
"enable": "Enable",
|
||||
"code": "Code",
|
||||
"code_is_incorrect": "Code is incorrect.",
|
||||
"add_an_extra_layer_of_security": "Add an extra layer of security to your account in case your password is stolen.",
|
||||
"2fa": "Two-Factor Authentication",
|
||||
"enable_2fa": "Enable two-factor authentication",
|
||||
"disable_2fa": "Disable two-factor authentication",
|
||||
"disable_2fa_recommendation": "If you need to disable 2FA, we recommend re-enabling it as soon as possible.",
|
||||
"error_disabling_2fa": "Error disabling two-factor authentication",
|
||||
"error_enabling_2fa": "Error setting up two-factor authentication",
|
||||
"security": "Security",
|
||||
"manage_account_security": "Manage your account's security.",
|
||||
"password": "Password",
|
||||
"password_updated_successfully": "Password updated successfully",
|
||||
"password_has_been_changed": "Your password has been successfully changed.",
|
||||
"error_changing_password": "Error changing password",
|
||||
"something_went_wrong": "Something went wrong",
|
||||
"please_try_again": "Please try again",
|
||||
"super_secure_new_password": "Your super secure new password",
|
||||
"new_password": "New Password",
|
||||
"your_old_password": "Your old password",
|
||||
"current_password": "Current Password",
|
||||
"change_password": "Change Password",
|
||||
"new_password_matches_old_password": "New password matches your old password. Please choose a different password.",
|
||||
"current_incorrect_password": "Current password is incorrect",
|
||||
"incorrect_password": "Password is incorrect",
|
||||
"1_on_1": "1-on-1",
|
||||
"24_h": "24h",
|
||||
"use_setting": "Use setting",
|
||||
"am_pm": "am/pm",
|
||||
"time_options": "Time options",
|
||||
"january": "January",
|
||||
"february": "February",
|
||||
"march": "March",
|
||||
"april": "April",
|
||||
"may": "May",
|
||||
"june": "June",
|
||||
"july": "July",
|
||||
"august": "August",
|
||||
"september": "September",
|
||||
"october": "October",
|
||||
"november": "November",
|
||||
"december": "December",
|
||||
"monday": "Monday",
|
||||
"tuesday": "Tuesday",
|
||||
"wednesday": "Wednesday",
|
||||
"thursday": "Thursday",
|
||||
"friday": "Friday",
|
||||
"saturday": "Saturday",
|
||||
"sunday": "Sunday",
|
||||
"all_booked_today": "All booked today.",
|
||||
"slots_load_fail": "Could not load the available time slots.",
|
||||
"additional_guests": "+ Additional Guests",
|
||||
"your_name": "Your name",
|
||||
"email_address": "Email address",
|
||||
"location": "Location",
|
||||
"yes": "yes",
|
||||
"no": "no",
|
||||
"additional_notes": "Additional notes",
|
||||
"booking_fail": "Could not book the meeting.",
|
||||
"reschedule_fail": "Could not reschedule the meeting.",
|
||||
"share_additional_notes": "Please share anything that will help prepare for our meeting.",
|
||||
"booking_confirmation": "Confirm your {{eventTypeTitle}} with {{profileName}}",
|
||||
"booking_reschedule_confirmation": "Reschedule your {{eventTypeTitle}} with {{profileName}}",
|
||||
"in_person_meeting": "Link or In-person meeting",
|
||||
"phone_call": "Phone call",
|
||||
"phone_number": "Phone Number",
|
||||
"enter_phone_number": "Enter phone number",
|
||||
"reschedule": "Reschedule",
|
||||
"book_a_team_member": "Book a team member instead",
|
||||
"or": "OR",
|
||||
"go_back": "Go back",
|
||||
"email_or_username": "Email or Username",
|
||||
"send_invite_email": "Send an invite email",
|
||||
"role": "Role",
|
||||
"edit_team": "Edit team",
|
||||
"reject": "Reject",
|
||||
"accept": "Accept",
|
||||
"leave": "Leave",
|
||||
"profile": "Profile",
|
||||
"my_team_url": "My team URL",
|
||||
"team_name": "Team name",
|
||||
"your_team_name": "Your team name",
|
||||
"team_updated_successfully": "Team updated successfully",
|
||||
"your_team_updated_successfully": "Your team has been updated successfully.",
|
||||
"about": "About",
|
||||
"team_description": "A few sentences about your team. This will appear on your team's URL page.",
|
||||
"members": "Members",
|
||||
"member": "Member",
|
||||
"owner": "Owner",
|
||||
"new_member": "New Member",
|
||||
"invite": "Invite",
|
||||
"invite_new_member": "Invite a new member",
|
||||
"invite_new_team_member": "Invite someone to your team.",
|
||||
"disable_cal_branding": "Disable Cal.com branding",
|
||||
"disable_cal_branding_description": "Hide all Cal.com branding from your public pages.",
|
||||
"danger_zone": "Danger Zone",
|
||||
"back": "Back",
|
||||
"cancel": "Cancel",
|
||||
"continue": "Continue",
|
||||
"confirm": "Confirm",
|
||||
"disband_team": "Disband Team",
|
||||
"disband_team_confirmation_message": "Are you sure you want to disband this team? Anyone who you've shared this team link with will no longer be able to book using it.",
|
||||
"remove_member_confirmation_message": "Are you sure you want to remove this member from the team?",
|
||||
"confirm_disband_team": "Yes, disband team",
|
||||
"confirm_remove_member": "Yes, remove member",
|
||||
"remove_member": "Remove member",
|
||||
"manage_your_team": "Manage your team",
|
||||
"submit": "Submit",
|
||||
"delete": "Delete",
|
||||
"update": "Update",
|
||||
"save": "Save",
|
||||
"pending": "Pending",
|
||||
"open_options": "Open options",
|
||||
"copy_link": "Copy link to event",
|
||||
"preview": "Preview",
|
||||
"link_copied": "Link copied!",
|
||||
"title": "Title",
|
||||
"description": "Description",
|
||||
"quick_video_meeting": "A quick video meeting.",
|
||||
"scheduling_type": "Scheduling Type",
|
||||
"preview_team": "Preview team",
|
||||
"collective": "Collective",
|
||||
"collective_description": "Schedule meetings when all selected team members are available.",
|
||||
"duration": "Duration",
|
||||
"minutes": "minutes",
|
||||
"round_robin": "Round Robin",
|
||||
"round_robin_description": "Cycle meetings between multiple team members.",
|
||||
"url": "URL",
|
||||
"hidden": "Hidden",
|
||||
"readonly": "Readonly",
|
||||
"plan_upgrade": "You need to upgrade your plan to have more than one active event type.",
|
||||
"plan_upgrade_instructions": "To upgrade, go to <a href=\"https://cal.com/upgrade\" className=\"underline\">https://cal.com/upgrade</a>",
|
||||
"event_types_page_title": "Event Types",
|
||||
"event_types_page_subtitle": "Create events to share for people to book on your calendar.",
|
||||
"new_event_type_btn": "New event type",
|
||||
"new_event_type_heading": "Create your first event type",
|
||||
"new_event_type_description": "Event types enable you to share links that show available times on your calendar and allow people to make bookings with you.",
|
||||
"new_event_title": "Add a new event type",
|
||||
"new_event_subtitle": "Create an event type under your name or a team.",
|
||||
"new_team_event": "Add a new team event type",
|
||||
"new_event_description": "Create a new event type for people to book times with.",
|
||||
"event_type_created_successfully": "{{eventTypeTitle}} event type created successfully"
|
||||
}
|
||||
|
|
147
public/static/locales/es/common.json
Normal file
147
public/static/locales/es/common.json
Normal file
|
@ -0,0 +1,147 @@
|
|||
{
|
||||
"edit_logo": "Cambiar la marca",
|
||||
"upload_a_logo": "Subir una marca",
|
||||
"enable": "Habilitar",
|
||||
"code": "Código",
|
||||
"code_is_incorrect": "El código es incorrecto.",
|
||||
"add_an_extra_layer_of_security": "Agregue una capa adicional de seguridad a su cuenta en caso de que le roben su contraseña.",
|
||||
"2fa": "Autorización de dos factores",
|
||||
"enable_2fa": "Habilitar la autenticación de dos factores",
|
||||
"disable_2fa": "Deshabilitar la autenticación de dos factores",
|
||||
"disable_2fa_recommendation": "Si necesita deshabilitar 2FA, le recomendamos que lo vuelva a habilitar lo antes posible.",
|
||||
"error_disabling_2fa": "Error al deshabilitar la autenticación de dos factores",
|
||||
"error_enabling_2fa": "Error al configurar la autenticación de dos factores",
|
||||
"security": "Seguridad",
|
||||
"manage_account_security": "Administra la seguridad de tu cuenta.",
|
||||
"password": "Contraseña",
|
||||
"password_updated_successfully": "Contraseña actualizada con éxito",
|
||||
"password_has_been_changed": "Su contraseña se ha cambiado correctamente.",
|
||||
"error_changing_password": "Error al cambiar la contraseña",
|
||||
"something_went_wrong": "Algo ha fallado",
|
||||
"please_try_again": "Por favor, inténtalo de nuevo",
|
||||
"super_secure_new_password": "Su nueva contraseña super segura",
|
||||
"new_password": "Nueva contraseña",
|
||||
"your_old_password": "Su contraseña antigua",
|
||||
"current_password": "Contraseña actual",
|
||||
"change_password": "Cambiar Contraseña",
|
||||
"new_password_matches_old_password": "La nueva contraseña coincide con su contraseña antigua. Por favor, elija una contraseña diferente.",
|
||||
"current_incorrect_password": "La contraseña actual es incorrecta",
|
||||
"incorrect_password": "La contraseña es incorrecta",
|
||||
"1_on_1": "1 a 1",
|
||||
"24_h": "24hs",
|
||||
"use_setting": "Usar configuración",
|
||||
"am_pm": "am/pm",
|
||||
"time_options": "Opciones de tiempo",
|
||||
"january": "Enero",
|
||||
"february": "Febrero",
|
||||
"march": "Marzo",
|
||||
"april": "Abril",
|
||||
"may": "Mayo",
|
||||
"june": "Junio",
|
||||
"july": "Julio",
|
||||
"august": "Agosto",
|
||||
"september": "Septiembre",
|
||||
"october": "Octubre",
|
||||
"november": "Noviembre",
|
||||
"december": "Diciembre",
|
||||
"monday": "Lunes",
|
||||
"tuesday": "Martes",
|
||||
"wednesday": "Miércoles",
|
||||
"thursday": "Jueves",
|
||||
"friday": "Viernes",
|
||||
"saturday": "Sábado",
|
||||
"sunday": "Domingo",
|
||||
"all_booked_today": "Todo reservado hoy.",
|
||||
"slots_load_fail": "No se pudo cargar el intervalo de tiempo disponible.",
|
||||
"additional_guests": "+ Invitados adicionales",
|
||||
"your_name": "Tu nombre",
|
||||
"email_address": "Correo electrónico",
|
||||
"location": "Ubicación",
|
||||
"yes": "sí",
|
||||
"no": "no",
|
||||
"additional_notes": "Notas adicionales",
|
||||
"booking_fail": "No se pudo reservar la reunión.",
|
||||
"reschedule_fail": "No se pudo cambiar la reunión.",
|
||||
"share_additional_notes": "Por favor comparta cualquier cosa que nos ayude preparar para esta reunión.",
|
||||
"booking_confirmation": "Confirma tu {{eventTypeTitle}} con {{profileName}}",
|
||||
"booking_reschedule_confirmation": "Cambia tu {{eventTypeTitle}} con {{profileName}}",
|
||||
"in_person_meeting": "Reunión en línea o en persona",
|
||||
"phone_call": "Llamada telefónica",
|
||||
"phone_number": "Número telefónico",
|
||||
"enter_phone_number": "Entra un número de teléfono",
|
||||
"reschedule": "Cambiar",
|
||||
"book_a_team_member": "Reservar un miembro del equipo en su lugar",
|
||||
"or": "O",
|
||||
"go_back": "Volver",
|
||||
"email_or_username": "Correo electrónico o nombre de usuario",
|
||||
"send_invite_email": "Enviar una invitación electrónica",
|
||||
"role": "Título",
|
||||
"edit_team": "Editar equipo",
|
||||
"reject": "Rechazar",
|
||||
"accept": "Aceptar",
|
||||
"leave": "Salir",
|
||||
"profile": "Perfil",
|
||||
"my_team_url": "URL de mi equipo",
|
||||
"team_name": "Nombre del equipo",
|
||||
"your_team_name": "Nombre de tu equipo",
|
||||
"team_updated_successfully": "Equipo actualizado correctamente",
|
||||
"your_team_updated_successfully": "Tu equipo se ha actualizado correctamente.",
|
||||
"about": "Acerca de",
|
||||
"team_description": "Algunas frases sobre tu equipo. Esto aparecerá en la página de tu equipo.",
|
||||
"members": "Miembros",
|
||||
"member": "Miembro",
|
||||
"owner": "Propietario",
|
||||
"new_member": "Nuevo miembro",
|
||||
"invite": "Invitar",
|
||||
"invite_new_member": "Invita a un nuevo miembro",
|
||||
"invite_new_team_member": "Invita a alguien a tu equipo.",
|
||||
"disable_cal_branding": "Desactivar marca de Cal.com",
|
||||
"disable_cal_branding_description": "Ocultar todas las marcas de Cal.com de sus páginas públicas.",
|
||||
"danger_zone": "Zona peligrosa",
|
||||
"back": "Atrás",
|
||||
"cancel": "Cancelar",
|
||||
"continue": "Continuar",
|
||||
"confirm": "Confirmar",
|
||||
"disband_team": "Disolver Equipo",
|
||||
"disband_team_confirmation_message": "¿Estás seguro de que quieres disolver este equipo? Cualquiera con quien has compartido este enlace de equipo ya no podrá reservar usando el mismo.",
|
||||
"remove_member_confirmation_message": "¿Estás seguro de que quieres eliminar este miembro del equipo?",
|
||||
"confirm_disband_team": "Sí, disolver equipo",
|
||||
"confirm_remove_member": "Sí, eliminar miembro",
|
||||
"remove_member": "Eliminar miembro",
|
||||
"manage_your_team": "Administra tu equipo",
|
||||
"submit": "Enviar",
|
||||
"delete": "Eliminar",
|
||||
"update": "Actualizar",
|
||||
"save": "Guardar",
|
||||
"pending": "Pendiente",
|
||||
"open_options": "Abrir opciones",
|
||||
"copy_link": "Copiar enlace al evento",
|
||||
"preview": "Vista previa",
|
||||
"link_copied": "¡Enlace copiado!",
|
||||
"title": "Título",
|
||||
"description": "Descripción",
|
||||
"quick_video_meeting": "Una reunión de vídeo rápida.",
|
||||
"scheduling_type": "Tipo de programación",
|
||||
"preview_team": "Vista previa del equipo",
|
||||
"collective": "Colectivo",
|
||||
"collective_description": "Programe reuniones cuando todos los miembros del equipo seleccionados estén disponibles.",
|
||||
"duration": "Duración",
|
||||
"minutes": "minutos",
|
||||
"round_robin": "Petición firmada por turnos",
|
||||
"round_robin_description": "Ciclo de reuniones entre varios miembros del equipo.",
|
||||
"url": "URL",
|
||||
"hidden": "Oculto",
|
||||
"readonly": "Sólo lectura",
|
||||
"plan_upgrade": "Necesitas actualizar tu plan para tener más de un tipo de evento activo.",
|
||||
"plan_upgrade_instructions": "Para actualizar, dirígete a <a href=\"https://cal.com/upgrade\" className=\"underline\">https://cal.com/upgrade</a>",
|
||||
"event_types_page_title": "Tipos de Evento",
|
||||
"event_types_page_subtitle": "Crea eventos para que la gente que invites reserve en tu calendario.",
|
||||
"new_event_type_btn": "Nuevo tipo de evento",
|
||||
"new_event_type_heading": "Crea tu primer tipo de evento",
|
||||
"new_event_type_description": "Los tipos de eventos te permiten compartir enlaces que muestran las horas disponibles en tu calendario y permitir que la gente haga reservas contigo.",
|
||||
"new_event_title": "Agregar un nuevo tipo de evento",
|
||||
"new_event_subtitle": "Crea un tipo de evento bajo tu nombre o equipo.",
|
||||
"new_team_event": "Agregar un nuevo tipo de evento de equipo",
|
||||
"new_event_description": "Crea un nuevo tipo de evento con el que la gente pueda hacer reservaciónes.",
|
||||
"event_type_created_successfully": "{{eventTypeTitle}} tipo de evento creado con éxito"
|
||||
}
|
1
public/static/locales/fr/common.json
Normal file
1
public/static/locales/fr/common.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
public/static/locales/it/common.json
Normal file
1
public/static/locales/it/common.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
public/static/locales/nl/common.json
Normal file
1
public/static/locales/nl/common.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
147
public/static/locales/pt/common.json
Normal file
147
public/static/locales/pt/common.json
Normal file
|
@ -0,0 +1,147 @@
|
|||
{
|
||||
"edit_logo": "Editar Logo",
|
||||
"upload_a_logo": "Carregar Logo",
|
||||
"enable": "Ativar",
|
||||
"code": "Código",
|
||||
"code_is_incorrect": "O código está incorreto.",
|
||||
"add_an_extra_layer_of_security": "Adicione uma camada extra de segurança à sua conta, caso a sua palavra-passe seja roubada.",
|
||||
"2fa": "Autenticação com dois fatores",
|
||||
"enable_2fa": "Ativar autenticação de dois fatores",
|
||||
"disable_2fa": "Desativar autenticação de dois fatores",
|
||||
"disable_2fa_recommendation": "Se precisar desativar o 2FA, recomendamos reativá-lo o mais rápido possível.",
|
||||
"error_disabling_2fa": "Erro ao desativar autenticação de dois fatores",
|
||||
"error_enabling_2fa": "Erro ao configurar a autenticação de dois fatores",
|
||||
"security": "Segurança",
|
||||
"manage_account_security": "Gerir a segurança da sua conta.",
|
||||
"password": "Palavra-Passe",
|
||||
"password_updated_successfully": "Palavra-Passe atualizada com sucesso",
|
||||
"password_has_been_changed": "A sua palavra-passe foi alterada com sucesso.",
|
||||
"error_changing_password": "Erro ao alterar a palavra-passe",
|
||||
"something_went_wrong": "Ocorreu um erro",
|
||||
"please_try_again": "Por favor, tente novamente",
|
||||
"super_secure_new_password": "A sua nova palavra-passe é super segura",
|
||||
"new_password": "Nova Palavra-Passe",
|
||||
"your_old_password": "A sua palavra-passe antiga",
|
||||
"current_password": "Palavra-Passe Atual",
|
||||
"change_password": "Alterar Palavra-Passe",
|
||||
"new_password_matches_old_password": "Nova palavra-passe é igual à palavra-passe antiga. Por favor, escolha uma palavra-passe diferente.",
|
||||
"current_incorrect_password": "Palavra-Passe atual está incorreta",
|
||||
"incorrect_password": "Palavra-Passe incorreta",
|
||||
"1_on_1": "1 para 1",
|
||||
"24_h": "24h",
|
||||
"use_setting": "Usar Configuração",
|
||||
"am_pm": "am/pm",
|
||||
"time_options": "Opções de Hora",
|
||||
"january": "Janeiro",
|
||||
"february": "Fevereiro",
|
||||
"march": "Março",
|
||||
"april": "Abril",
|
||||
"may": "Maio",
|
||||
"june": "Junho",
|
||||
"july": "Julho",
|
||||
"august": "Agosto",
|
||||
"september": "Setembro",
|
||||
"october": "Outubro",
|
||||
"november": "Novembro",
|
||||
"december": "Dezembro",
|
||||
"monday": "Segunda-Feira",
|
||||
"tuesday": "Terça-Feira",
|
||||
"wednesday": "Quarta-Feira",
|
||||
"thursday": "Quinta-Feira",
|
||||
"friday": "Sexta-Feira",
|
||||
"saturday": "Sábado",
|
||||
"sunday": "Domingo",
|
||||
"all_booked_today": "Todo o Dia Reservado.",
|
||||
"slots_load_fail": "Não foi possível carregar os horários disponíveis.",
|
||||
"additional_guests": "+ Convidados Adicionais",
|
||||
"your_name": "Seu Nome",
|
||||
"email_address": "Endereço de E-mail",
|
||||
"location": "Localização",
|
||||
"yes": "Sim",
|
||||
"no": "Não",
|
||||
"additional_notes": "Notas Adicionais",
|
||||
"booking_fail": "Não foi possível agendar a reunião.",
|
||||
"reschedule_fail": "Não foi possível re-agendar a reunião.",
|
||||
"share_additional_notes": "Por favor, partilhe qualquer informação para preparar a nossa reunião.",
|
||||
"booking_confirmation": "Confirme o seu {{eventTypeTitle}} com {{profileName}}",
|
||||
"booking_reschedule_confirmation": "Reagende o seu {{eventTypeTitle}} com {{profileName}}",
|
||||
"in_person_meeting": "Link ou Reunião Presencial",
|
||||
"phone_call": "Chamada Telefónica",
|
||||
"phone_number": "Número de Telefone",
|
||||
"enter_phone_number": "Inserir Número do Telefone",
|
||||
"reschedule": "Reagendar",
|
||||
"book_a_team_member": "Reserve um Membro da sua Equipa no seu lugar",
|
||||
"or": "Ou",
|
||||
"go_back": "Voltar atrás",
|
||||
"email_or_username": "E-mail ou Nome de Utilizador",
|
||||
"send_invite_email": "Enviar um e-mail de convite",
|
||||
"role": "Função",
|
||||
"edit_team": "Editar Equipa",
|
||||
"reject": "Rejeitar",
|
||||
"accept": "Aceitar",
|
||||
"leave": "Ausente",
|
||||
"profile": "Perfil",
|
||||
"my_team_url": "URL da Minha Equipa",
|
||||
"team_name": "Nome da Equipa",
|
||||
"your_team_name": "Nome da sua equipa",
|
||||
"team_updated_successfully": "Equipa atualizada com sucesso",
|
||||
"your_team_updated_successfully": "A sua equipa foi atualizada com sucesso.",
|
||||
"about": "Sobre",
|
||||
"team_description": "Algumas frases sobre a sua equipa. Isso aparecerá na sua equipa'URL .",
|
||||
"members": "Membros",
|
||||
"member": "Membro",
|
||||
"owner": "Proprietário",
|
||||
"new_member": "Novo Membro",
|
||||
"invite": "Convidar",
|
||||
"invite_new_member": "Convidar um Novo Membro",
|
||||
"invite_new_team_member": "Convide alguém para a sua equipa.",
|
||||
"disable_cal_branding": "Desativar a marca Cal.com",
|
||||
"disable_cal_branding_description": "Ocultar todas as marcas de Cal.com das suas páginas públicas.",
|
||||
"danger_zone": "Zona de Perigo",
|
||||
"back": "Anterior",
|
||||
"cancel": "Cancelar",
|
||||
"continue": "Continuar",
|
||||
"confirm": "Confirmar",
|
||||
"disband_team": "Dissolver Equipa",
|
||||
"disband_team_confirmation_message": "Tem a certeza de que deseja dissolver esta equipa? Qualquer pessoa com quem've partilhou este link de equipa não conseguirá fazer uma reserva usando-o.",
|
||||
"remove_member_confirmation_message": "Tem a certeza de que deseja remover este membro da equipa?",
|
||||
"confirm_disband_team": "Sim, dissolver equipa",
|
||||
"confirm_remove_member": "Sim, remover membro",
|
||||
"remove_member": "Remover Membro",
|
||||
"manage_your_team": "Gerir a sua equipa",
|
||||
"submit": "Enviar",
|
||||
"delete": "Apagar",
|
||||
"update": "Atualizar",
|
||||
"save": "Guardar",
|
||||
"pending": "Pendente",
|
||||
"open_options": "Abrir Opções",
|
||||
"copy_link": "Copiar link do evento",
|
||||
"preview": "Pré-Visualizar",
|
||||
"link_copied": "Link copiado!",
|
||||
"title": "Título",
|
||||
"description": "Descrição",
|
||||
"quick_video_meeting": "Uma breve reunião em vídeo.",
|
||||
"scheduling_type": "Tipo do Agendamento",
|
||||
"preview_team": "Pré-visualizar Equipa",
|
||||
"collective": "Coletivo",
|
||||
"collective_description": "Agende reuniões quando todos os membros selecionados da equipa estiverem disponíveis.",
|
||||
"duration": "Duração",
|
||||
"minutes": "Minutos",
|
||||
"round_robin": "Round Robin",
|
||||
"round_robin_description": "Reuniões de ciclo entre vários membros da equipa.",
|
||||
"url": "URL",
|
||||
"hidden": "Oculto",
|
||||
"readonly": "Somente Leitura",
|
||||
"plan_upgrade": "Precisa atualizar o seu plano para ter mais de um tipo de evento ativo.",
|
||||
"plan_upgrade_instructions": "Para fazer a atualização, aceda <a href=\"https://cal.com/upgrade\" className=\"underline\">https://cal.com/upgrade</a>",
|
||||
"event_types_page_title": "Tipo de Eventos",
|
||||
"event_types_page_subtitle": "Crie eventos para partilhar, para que as pessoas façam reservas no seu calendário.",
|
||||
"new_event_type_btn": "Novo tipo de evento",
|
||||
"new_event_type_heading": "Crie o seu primeiro tipo de evento",
|
||||
"new_event_type_description": "Os tipos de evento permitem partilhar links que mostram os horários disponíveis na sua agenda e permitem que as pessoas façam reservas consigo.",
|
||||
"new_event_title": "Adicionar um novo tipo de evento",
|
||||
"new_event_subtitle": "Crie um tipo de evento sob o seu nome ou equipa.",
|
||||
"new_team_event": "Adicionar um novo tipo de evento de equipa",
|
||||
"new_event_description": "Crie um novo tipo de evento para as pessoas reservarem uma hora.",
|
||||
"event_type_created_successfully": "{{eventTypeTitle}} tipo de evento criado com sucesso"
|
||||
}
|
|
@ -1,3 +1 @@
|
|||
{
|
||||
"new-event-type-btn": "Nou tip de eveniment"
|
||||
}
|
||||
{}
|
||||
|
|
147
public/static/locales/ru/common.json
Normal file
147
public/static/locales/ru/common.json
Normal file
|
@ -0,0 +1,147 @@
|
|||
{
|
||||
"edit_logo": "Изменить логотип",
|
||||
"upload_a_logo": "Загрузить логотип",
|
||||
"enable": "Включить",
|
||||
"code": "Код",
|
||||
"code_is_incorrect": "Неверный код.",
|
||||
"add_an_extra_layer_of_security": "Добавьте дополнительный уровень безопасности в свою учетную запись на случай кражи пароля.",
|
||||
"2fa": "Двухфакторная авторизация",
|
||||
"enable_2fa": "Включить двухфакторную авторизацию",
|
||||
"disable_2fa": "Отключить двухфакторную авторизацию",
|
||||
"disable_2fa_recommendation": "Если вам нужно отключить двухфакторную авторизацию, мы рекомендуем включить её как можно скорее.",
|
||||
"error_disabling_2fa": "Ошибка отключения двухфакторной авторизации",
|
||||
"error_enabling_2fa": "Ошибка настройки двухфакторной авторизации",
|
||||
"security": "Безопасность",
|
||||
"manage_account_security": "Управление безопасностью вашего аккаунта.",
|
||||
"password": "Пароль",
|
||||
"password_updated_successfully": "Пароль успешно обновлен",
|
||||
"password_has_been_changed": "Ваш пароль был успешно изменен.",
|
||||
"error_changing_password": "Ошибка при изменении пароля",
|
||||
"something_went_wrong": "Что-то пошло не так",
|
||||
"please_try_again": "Пожалуйста, попробуйте еще раз",
|
||||
"super_secure_new_password": "Ваш супербезопасный новый пароль",
|
||||
"new_password": "Новый пароль",
|
||||
"your_old_password": "Ваш старый пароль",
|
||||
"current_password": "Текущий пароль",
|
||||
"change_password": "Изменить пароль",
|
||||
"new_password_matches_old_password": "Новый пароль совпадает с вашим старым паролем. Пожалуйста, выберите другой пароль.",
|
||||
"current_incorrect_password": "Неверный текущий пароль",
|
||||
"incorrect_password": "Неверный пароль",
|
||||
"1_on_1": "1-на-1",
|
||||
"24_h": "24 часа",
|
||||
"use_setting": "Использовать настройки",
|
||||
"am_pm": "am/pm",
|
||||
"time_options": "Настройки времени",
|
||||
"january": "Январь",
|
||||
"february": "Февраль",
|
||||
"march": "Март",
|
||||
"april": "Апрель",
|
||||
"may": "Май",
|
||||
"june": "Июнь",
|
||||
"july": "Июль",
|
||||
"august": "Август",
|
||||
"september": "Сентябрь",
|
||||
"october": "Октябрь",
|
||||
"november": "Ноябрь",
|
||||
"december": "Декабрь",
|
||||
"monday": "Понедельник",
|
||||
"tuesday": "Вторник",
|
||||
"wednesday": "Среда",
|
||||
"thursday": "Четверг",
|
||||
"friday": "Пятница",
|
||||
"saturday": "Суббота",
|
||||
"sunday": "Воскресенье",
|
||||
"all_booked_today": "Сегодня всё забронировано.",
|
||||
"slots_load_fail": "Не удалось загрузить доступные временные интервалы.",
|
||||
"additional_guests": "+ Дополнительные гости",
|
||||
"your_name": "Ваше имя",
|
||||
"email_address": "Адрес электронной почты",
|
||||
"location": "Местоположение",
|
||||
"yes": "да",
|
||||
"no": "нет",
|
||||
"additional_notes": "Дополнительная информация",
|
||||
"booking_fail": "Не удалось забронировать встречу.",
|
||||
"reschedule_fail": "Не удалось перенести встречу.",
|
||||
"share_additional_notes": "Дополнительная информация, которая может помочь подготовиться к нашей встрече.",
|
||||
"booking_confirmation": "Подтвердите вашу встречу «{{eventTypeTitle}}» с {{profileName}}",
|
||||
"booking_reschedule_confirmation": "Перенесите вашу встречу «{{eventTypeTitle}}» с {{profileName}}",
|
||||
"in_person_meeting": "Ссылка или личная встреча",
|
||||
"phone_call": "Телефонный звонок",
|
||||
"phone_number": "Номер телефона",
|
||||
"enter_phone_number": "Введите номер телефона",
|
||||
"reschedule": "Перенести",
|
||||
"book_a_team_member": "Забронировать встречу с одним из членов команды",
|
||||
"or": "ИЛИ",
|
||||
"go_back": "Вернуться",
|
||||
"email_or_username": "Email или имя пользователя",
|
||||
"send_invite_email": "Отправить приглашение по электронной почте",
|
||||
"role": "Роль",
|
||||
"edit_team": "Редактировать команду",
|
||||
"reject": "Отклонить",
|
||||
"accept": "Принять",
|
||||
"leave": "Покинуть",
|
||||
"profile": "Профиль",
|
||||
"my_team_url": "URL-адрес моей команды",
|
||||
"team_name": "Название команды",
|
||||
"your_team_name": "Название вашей команды",
|
||||
"team_updated_successfully": "Команда успешно обновлена",
|
||||
"your_team_updated_successfully": "Ваша команда успешно обновлена.",
|
||||
"about": "О нас",
|
||||
"team_description": "Несколько предложений о вашей команде. Это появится на странице вашей команды.",
|
||||
"members": "Участники",
|
||||
"member": "Участник",
|
||||
"owner": "Владелец",
|
||||
"new_member": "Новый участник",
|
||||
"invite": "Пригласить",
|
||||
"invite_new_member": "Пригласить нового участника",
|
||||
"invite_new_team_member": "Пригласите кого-нибудь в вашу команду.",
|
||||
"disable_cal_branding": "Отключить брендинг Cal.com",
|
||||
"disable_cal_branding_description": "Скрыть весь брендинг Cal.com с ваших публичных страниц.",
|
||||
"danger_zone": "Опасная зона",
|
||||
"back": "Назад",
|
||||
"cancel": "Отмена",
|
||||
"continue": "Продолжить",
|
||||
"confirm": "Подтвердить",
|
||||
"disband_team": "Распустить команду",
|
||||
"disband_team_confirmation_message": "Вы уверены, что хотите распустить эту команду? Любой, с кем вы поделились ссылкой на эту команду, больше не сможет забронировать её.",
|
||||
"remove_member_confirmation_message": "Вы уверены, что хотите удалить этого участника из команды?",
|
||||
"confirm_disband_team": "Да, распустить команду",
|
||||
"confirm_remove_member": "Да, удалить участника",
|
||||
"remove_member": "Удалить участника",
|
||||
"manage_your_team": "Управление вашей командой",
|
||||
"submit": "Отправить",
|
||||
"delete": "Удалить",
|
||||
"update": "Обновить",
|
||||
"save": "Сохранить",
|
||||
"pending": "В ожидании",
|
||||
"open_options": "Открыть настройки",
|
||||
"copy_link": "Скопировать ссылку на событие",
|
||||
"preview": "Предпросмотр",
|
||||
"link_copied": "Ссылка скопирована!",
|
||||
"title": "Заголовок",
|
||||
"description": "Описание",
|
||||
"quick_video_meeting": "Быстрая видео-встреча.",
|
||||
"scheduling_type": "Тип расписания",
|
||||
"preview_team": "Предпросмотр команды",
|
||||
"collective": "Коллективная встреча",
|
||||
"collective_description": "Расписание встреч, когда доступны все выбранные члены команды.",
|
||||
"duration": "Продолжительность",
|
||||
"minutes": "мин.",
|
||||
"round_robin": "По кругу",
|
||||
"round_robin_description": "Цикл встреч между несколькими членами команды.",
|
||||
"url": "URL",
|
||||
"hidden": "Скрытый",
|
||||
"readonly": "Только для чтения",
|
||||
"plan_upgrade": "Необходимо обновить тарифный план, чтобы иметь более одного активного типа события.",
|
||||
"plan_upgrade_instructions": "Для повышения перейдите на <a href=\"https://cal.com/upgrade\" className=\"underline\">https://cal.com/upgrade</a>",
|
||||
"event_types_page_title": "Типы мероприятий",
|
||||
"event_types_page_subtitle": "Создайте мероприятие, чтобы поделиться с людьми для бронирования в вашем календаре.",
|
||||
"new_event_type_btn": "Новый тип мероприятия",
|
||||
"new_event_type_heading": "Создайте свой первый тип мероприятия",
|
||||
"new_event_type_description": "Типы мероприятий позволяют делиться ссылками, которые показывают время в вашем календаре и позволяют людям бронировать встречи с вами.",
|
||||
"new_event_title": "Добавить новый тип события",
|
||||
"new_event_subtitle": "Создайте тип события для себя или команды.",
|
||||
"new_team_event": "Добавить новый тип события команды",
|
||||
"new_event_description": "Создайте новый тип мероприятия, с помощью которого люди смогут забронировать время.",
|
||||
"event_type_created_successfully": "{{eventTypeTitle}} тип мероприятия успешно создан"
|
||||
}
|
|
@ -4,7 +4,6 @@
|
|||
import superjson from "superjson";
|
||||
|
||||
import { createRouter } from "../createRouter";
|
||||
import { bookingRouter } from "./booking";
|
||||
import { viewerRouter } from "./viewer";
|
||||
|
||||
/**
|
||||
|
@ -24,7 +23,6 @@ export const appRouter = createRouter()
|
|||
* @link https://trpc.io/docs/error-formatting
|
||||
*/
|
||||
// .formatError(({ shape, error }) => { })
|
||||
.merge("viewer.", viewerRouter)
|
||||
.merge("booking.", bookingRouter);
|
||||
.merge("viewer.", viewerRouter);
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { createRouter } from "../createRouter";
|
||||
|
||||
export const bookingRouter = createRouter().query("userEventTypes", {
|
||||
input: z.object({
|
||||
username: z.string().min(1),
|
||||
}),
|
||||
async resolve({ input, ctx }) {
|
||||
const { prisma } = ctx;
|
||||
const { username } = input;
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
username: username.toLowerCase(),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
name: true,
|
||||
bio: true,
|
||||
avatar: true,
|
||||
theme: true,
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const eventTypesWithHidden = await prisma.eventType.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
teamId: null,
|
||||
},
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
{
|
||||
users: {
|
||||
some: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
title: true,
|
||||
length: true,
|
||||
description: true,
|
||||
hidden: true,
|
||||
schedulingType: true,
|
||||
price: true,
|
||||
currency: true,
|
||||
},
|
||||
take: user.plan === "FREE" ? 1 : undefined,
|
||||
});
|
||||
const eventTypes = eventTypesWithHidden.filter((evt) => !evt.hidden);
|
||||
return {
|
||||
user,
|
||||
eventTypes,
|
||||
};
|
||||
},
|
||||
});
|
Loading…
Reference in a new issue