Add Jitsi Meet Integration (#1674)
* basic integration structure * jitsi logo * add jitsi meet description to event settings page * add JitsiVideoApiAdapter ref #1445 * add LocationType.Jitsi to BookingPage ref #1445 * add LocationType.Jitsi to event-types ref #1445 * add meet.jit.si/cal/uuid support to BookingPage ref #1445 * add basic "cal_provide_jitsi_meeting_url" translation strings ref #1445 * generate meeting id ref #1445 * implement direct jitsi link in /success page ref #1445 * cleanup location link duplicate ref #1445 * full JitsiVideoApiAdapter implementation ref #1445 * check integration availability in /pages/event-types/[type] ref #1445 * add video conferencing link as calendar event location ref #1445 * PR feedback * Update components/booking/pages/BookingPage.tsx don't know - wouldn't do this myself for future proofing but fine... Co-authored-by: Omar López <zomars@me.com> * Update components/booking/pages/BookingPage.tsx 🤷♂️ Co-authored-by: Omar López <zomars@me.com> * cleanup: props.type === "jitsi_video" ref #1445 Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
parent
38b41f0adb
commit
20d2955e68
26 changed files with 171 additions and 20 deletions
components/booking/pages
lib
pages
public
integrations
static/locales
|
@ -16,6 +16,7 @@ import { Controller, useForm, useWatch } from "react-hook-form";
|
|||
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
import { ReactMultiEmail } from "react-multi-email";
|
||||
import { useMutation } from "react-query";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { createPaymentLink } from "@ee/lib/stripe/client";
|
||||
|
||||
|
@ -89,6 +90,9 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
if (!location) {
|
||||
return;
|
||||
}
|
||||
if (location === "integrations:jitsi") {
|
||||
return "https://meet.jit.si/cal/" + uuidv4();
|
||||
}
|
||||
if (location.includes("integration")) {
|
||||
return t("web_conferencing_details_to_follow");
|
||||
}
|
||||
|
@ -143,6 +147,7 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
[LocationType.Phone]: t("phone_call"),
|
||||
[LocationType.GoogleMeet]: "Google Meet",
|
||||
[LocationType.Zoom]: "Zoom Video",
|
||||
[LocationType.Jitsi]: "Jitsi Meet",
|
||||
[LocationType.Daily]: "Daily.co Video",
|
||||
[LocationType.Huddle01]: "Huddle01 Video",
|
||||
[LocationType.Tandem]: "Tandem Video",
|
||||
|
@ -330,6 +335,12 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
{getLocationValue({ locationType: selectedLocation })}
|
||||
</p>
|
||||
)}
|
||||
{selectedLocation === LocationType.Jitsi && (
|
||||
<p className="mb-2 text-gray-500">
|
||||
<LocationMarkerIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||
Jitsi Meet
|
||||
</p>
|
||||
)}
|
||||
<p className="mb-4 text-green-500">
|
||||
<CalendarIcon className="inline-block w-4 h-4 mr-1 -mt-1" />
|
||||
{parseDate(date)}
|
||||
|
|
|
@ -66,6 +66,7 @@ export const getLocationRequestFromIntegration = (location: string) => {
|
|||
location === LocationType.GoogleMeet.valueOf() ||
|
||||
location === LocationType.Zoom.valueOf() ||
|
||||
location === LocationType.Daily.valueOf() ||
|
||||
location === LocationType.Jitsi.valueOf() ||
|
||||
location === LocationType.Huddle01.valueOf() ||
|
||||
location === LocationType.Tandem.valueOf()
|
||||
) {
|
||||
|
|
|
@ -14,6 +14,8 @@ export function getIntegrationName(name: string) {
|
|||
return "Apple Calendar";
|
||||
case "daily_video":
|
||||
return "Daily";
|
||||
case "jitsi_video":
|
||||
return "Jitsi Meet";
|
||||
case "huddle01_video":
|
||||
return "Huddle01";
|
||||
case "tandem_video":
|
||||
|
|
34
lib/integrations/Jitsi/JitsiVideoApiAdapter.ts
Normal file
34
lib/integrations/Jitsi/JitsiVideoApiAdapter.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { PartialReference } from "@lib/events/EventManager";
|
||||
import { VideoApiAdapter, VideoCallData } from "@lib/videoClient";
|
||||
|
||||
const JitsiVideoApiAdapter = (): VideoApiAdapter => {
|
||||
return {
|
||||
getAvailability: () => {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
createMeeting: async (): Promise<VideoCallData> => {
|
||||
const meetingID = uuidv4();
|
||||
return Promise.resolve({
|
||||
type: "jitsi_video",
|
||||
id: meetingID,
|
||||
password: "",
|
||||
url: "https://meet.jit.si/cal/" + meetingID,
|
||||
});
|
||||
},
|
||||
deleteMeeting: async (): Promise<void> => {
|
||||
Promise.resolve();
|
||||
},
|
||||
updateMeeting: (bookingRef: PartialReference): Promise<VideoCallData> => {
|
||||
return Promise.resolve({
|
||||
type: "jitsi_video",
|
||||
id: bookingRef.meetingId as string,
|
||||
password: bookingRef.meetingPassword as string,
|
||||
url: bookingRef.meetingUrl as string,
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default JitsiVideoApiAdapter;
|
|
@ -1,11 +1,7 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import _ from "lodash";
|
||||
|
||||
/**
|
||||
* We can't use aliases in playwright tests (yet)
|
||||
* https://github.com/microsoft/playwright/issues/7121
|
||||
*/
|
||||
import { validJson } from "../../lib/jsonUtils";
|
||||
import { validJson } from "@lib/jsonUtils";
|
||||
|
||||
const credentialData = Prisma.validator<Prisma.CredentialArgs>()({
|
||||
select: { id: true, type: true },
|
||||
|
@ -24,6 +20,7 @@ export type Integration = {
|
|||
| "caldav_calendar"
|
||||
| "apple_calendar"
|
||||
| "stripe_payment"
|
||||
| "jitsi_video"
|
||||
| "huddle01_video"
|
||||
| "metamask_web3";
|
||||
title: string;
|
||||
|
@ -65,6 +62,14 @@ export const ALL_INTEGRATIONS = [
|
|||
description: "Video Conferencing",
|
||||
variant: "conferencing",
|
||||
},
|
||||
{
|
||||
installed: true,
|
||||
type: "jitsi_video",
|
||||
title: "Jitsi Meet",
|
||||
imageSrc: "integrations/jitsi.svg",
|
||||
description: "Video Conferencing",
|
||||
variant: "conferencing",
|
||||
},
|
||||
{
|
||||
installed: true,
|
||||
type: "huddle01_video",
|
||||
|
@ -146,7 +151,10 @@ export function hasIntegration(integrations: IntegrationMeta, type: string): boo
|
|||
(i) =>
|
||||
i.type === type &&
|
||||
!!i.installed &&
|
||||
(type === "daily_video" || type === "huddle01_video" || i.credentials.length > 0)
|
||||
(type === "daily_video" ||
|
||||
type === "jitsi_video" ||
|
||||
type === "huddle01_video" ||
|
||||
i.credentials.length > 0)
|
||||
);
|
||||
}
|
||||
export function hasIntegrationInstalled(type: Integration["type"]): boolean {
|
||||
|
|
|
@ -4,6 +4,7 @@ export enum LocationType {
|
|||
GoogleMeet = "integrations:google:meet",
|
||||
Zoom = "integrations:zoom",
|
||||
Daily = "integrations:daily",
|
||||
Jitsi = "integrations:jitsi",
|
||||
Huddle01 = "integrations:huddle01",
|
||||
Tandem = "integrations:tandem",
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { getUid } from "@lib/CalEventParser";
|
|||
import { EventResult } from "@lib/events/EventManager";
|
||||
import { PartialReference } from "@lib/events/EventManager";
|
||||
import Huddle01VideoApiAdapter from "@lib/integrations/Huddle01/Huddle01VideoApiAdapter";
|
||||
import JitsiVideoApiAdapter from "@lib/integrations/Jitsi/JitsiVideoApiAdapter";
|
||||
import logger from "@lib/logger";
|
||||
|
||||
import DailyVideoApiAdapter from "./integrations/Daily/DailyVideoApiAdapter";
|
||||
|
@ -46,6 +47,9 @@ const getVideoAdapters = (withCredentials: Credential[]): VideoApiAdapter[] =>
|
|||
case "daily_video":
|
||||
acc.push(DailyVideoApiAdapter(cred));
|
||||
break;
|
||||
case "jitsi_video":
|
||||
acc.push(JitsiVideoApiAdapter());
|
||||
break;
|
||||
case "huddle01_video":
|
||||
acc.push(Huddle01VideoApiAdapter());
|
||||
break;
|
||||
|
|
|
@ -115,6 +115,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
|
||||
const defaultLocations = [
|
||||
{ value: LocationType.InPerson, label: t("in_person_meeting") },
|
||||
{ value: LocationType.Jitsi, label: "Jitsi Meet" },
|
||||
{ value: LocationType.Phone, label: t("phone_call") },
|
||||
];
|
||||
|
||||
|
@ -125,7 +126,12 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
const updateMutation = trpc.useMutation("viewer.eventTypes.update", {
|
||||
onSuccess: async ({ eventType }) => {
|
||||
await router.push("/event-types");
|
||||
showToast(t("event_type_updated_successfully", { eventTypeTitle: eventType.title }), "success");
|
||||
showToast(
|
||||
t("event_type_updated_successfully", {
|
||||
eventTypeTitle: eventType.title,
|
||||
}),
|
||||
"success"
|
||||
);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
|
@ -261,6 +267,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
return <p className="text-sm">{t("cal_provide_zoom_meeting_url")}</p>;
|
||||
case LocationType.Daily:
|
||||
return <p className="text-sm">{t("cal_provide_video_meeting_url")}</p>;
|
||||
case LocationType.Jitsi:
|
||||
return <p className="text-sm">{t("cal_provide_jitsi_meeting_url")}</p>;
|
||||
case LocationType.Huddle01:
|
||||
return <p className="text-sm">{t("cal_provide_huddle01_meeting_url")}</p>;
|
||||
case LocationType.Tandem:
|
||||
|
@ -276,7 +284,11 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
setCustomInputs([...customInputs]);
|
||||
};
|
||||
|
||||
const schedulingTypeOptions: { value: SchedulingType; label: string; description: string }[] = [
|
||||
const schedulingTypeOptions: {
|
||||
value: SchedulingType;
|
||||
label: string;
|
||||
description: string;
|
||||
}[] = [
|
||||
{
|
||||
value: SchedulingType.COLLECTIVE,
|
||||
label: t("collective"),
|
||||
|
@ -328,7 +340,10 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
locations: { type: LocationType; address?: string }[];
|
||||
customInputs: EventTypeCustomInput[];
|
||||
users: string[];
|
||||
availability: { openingHours: AvailabilityInput[]; dateOverrides: AvailabilityInput[] };
|
||||
availability: {
|
||||
openingHours: AvailabilityInput[];
|
||||
dateOverrides: AvailabilityInput[];
|
||||
};
|
||||
timeZone: string;
|
||||
periodType: PeriodType;
|
||||
periodDays: number;
|
||||
|
@ -548,6 +563,33 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
<span className="ml-2 text-sm">Tandem Video</span>
|
||||
</div>
|
||||
)}
|
||||
{location.type === LocationType.Jitsi && (
|
||||
<div className="flex items-center flex-grow">
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M32 0C49.6733 0 64 14.3267 64 32C64 49.6733 49.6733 64 32 64C14.3267 64 0 49.6733 0 32C0 14.3267 14.3267 0 32 0Z"
|
||||
fill="#E5E5E4"
|
||||
/>
|
||||
<path
|
||||
d="M32.0002 0.623047C49.3292 0.623047 63.3771 14.6709 63.3771 31.9999C63.3771 49.329 49.3292 63.3768 32.0002 63.3768C14.6711 63.3768 0.623291 49.329 0.623291 31.9999C0.623291 14.6709 14.6716 0.623047 32.0002 0.623047Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M31.9998 3.14014C47.9386 3.14014 60.8597 16.0612 60.8597 32C60.8597 47.9389 47.9386 60.8599 31.9998 60.8599C16.0609 60.8599 3.13989 47.9389 3.13989 32C3.13989 16.0612 16.0609 3.14014 31.9998 3.14014Z"
|
||||
fill="#4A8CFF"
|
||||
/>
|
||||
<path
|
||||
d="M13.1711 22.9581V36.5206C13.1832 39.5875 15.6881 42.0558 18.743 42.0433H38.5125C39.0744 42.0433 39.5266 41.5911 39.5266 41.0412V27.4788C39.5145 24.4119 37.0096 21.9435 33.9552 21.956H14.1857C13.6238 21.956 13.1716 22.4082 13.1716 22.9581H13.1711ZM40.7848 28.2487L48.9469 22.2864C49.6557 21.6998 50.2051 21.8462 50.2051 22.9095V41.0903C50.2051 42.2999 49.5329 42.1536 48.9469 41.7134L40.7848 35.7631V28.2487Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
<span className="ml-2 text-sm">Jitsi Meet</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
|
@ -1350,7 +1392,10 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||
.findIndex((loc) => values.locationType === loc.type);
|
||||
if (existingIdx !== -1) {
|
||||
const copy = formMethods.getValues("locations");
|
||||
copy[existingIdx] = { ...formMethods.getValues("locations")[existingIdx], ...details };
|
||||
copy[existingIdx] = {
|
||||
...formMethods.getValues("locations")[existingIdx],
|
||||
...details,
|
||||
};
|
||||
formMethods.setValue("locations", copy);
|
||||
} else {
|
||||
formMethods.setValue(
|
||||
|
@ -1604,17 +1649,36 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
const locationOptions: OptionTypeBase[] = [];
|
||||
|
||||
if (hasIntegration(integrations, "zoom_video")) {
|
||||
locationOptions.push({ value: LocationType.Zoom, label: "Zoom Video", disabled: true });
|
||||
locationOptions.push({
|
||||
value: LocationType.Zoom,
|
||||
label: "Zoom Video",
|
||||
disabled: true,
|
||||
});
|
||||
}
|
||||
const hasPaymentIntegration = hasIntegration(integrations, "stripe_payment");
|
||||
if (hasIntegration(integrations, "google_calendar")) {
|
||||
locationOptions.push({ value: LocationType.GoogleMeet, label: "Google Meet" });
|
||||
locationOptions.push({
|
||||
value: LocationType.GoogleMeet,
|
||||
label: "Google Meet",
|
||||
});
|
||||
}
|
||||
if (hasIntegration(integrations, "daily_video")) {
|
||||
locationOptions.push({ value: LocationType.Daily, label: "Daily.co Video" });
|
||||
locationOptions.push({
|
||||
value: LocationType.Daily,
|
||||
label: "Daily.co Video",
|
||||
});
|
||||
}
|
||||
if (hasIntegration(integrations, "jitsi_video")) {
|
||||
locationOptions.push({
|
||||
value: LocationType.Jitsi,
|
||||
label: "Jitsi Meet",
|
||||
});
|
||||
}
|
||||
if (hasIntegration(integrations, "huddle01_video")) {
|
||||
locationOptions.push({ value: LocationType.Huddle01, label: "Huddle01 Video" });
|
||||
locationOptions.push({
|
||||
value: LocationType.Huddle01,
|
||||
label: "Huddle01 Video",
|
||||
});
|
||||
}
|
||||
if (hasIntegration(integrations, "tandem_video")) {
|
||||
locationOptions.push({ value: LocationType.Tandem, label: "Tandem Video" });
|
||||
|
|
|
@ -480,7 +480,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
|||
);
|
||||
}
|
||||
/** We don't need to "Connect", just show that it's installed */
|
||||
if (props.type === "daily_video" || props.type === "huddle01_video") {
|
||||
if (["daily_video", "huddle01_video", "jitsi_video"].includes(props.type)) {
|
||||
return (
|
||||
<div className="px-3 py-2 truncate">
|
||||
<h3 className="text-sm font-medium text-gray-700">{t("installed")}</h3>
|
||||
|
|
|
@ -32,7 +32,8 @@ dayjs.extend(timezone);
|
|||
export default function Success(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const { location, name, reschedule } = router.query;
|
||||
const { location: _location, name, reschedule } = router.query;
|
||||
const location = Array.isArray(_location) ? _location[0] : _location;
|
||||
|
||||
const [is24h, setIs24h] = useState(false);
|
||||
const [date, setDate] = useState(dayjs.utc(asStringOrThrow(router.query.date)));
|
||||
|
@ -58,7 +59,7 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
|
|||
function eventLink(): string {
|
||||
const optional: { location?: string } = {};
|
||||
if (location) {
|
||||
optional["location"] = Array.isArray(location) ? location[0] : location;
|
||||
optional["location"] = location;
|
||||
}
|
||||
|
||||
const event = createEvent({
|
||||
|
@ -141,7 +142,15 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
|
|||
{location && (
|
||||
<>
|
||||
<div className="font-medium">{t("where")}</div>
|
||||
<div className="col-span-2">{location}</div>
|
||||
<div className="col-span-2">
|
||||
{location.startsWith("http") ? (
|
||||
<a title="Meeting Link" href={location}>
|
||||
{location}
|
||||
</a>
|
||||
) : (
|
||||
location
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
@ -149,7 +158,7 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
|
|||
</div>
|
||||
{!needsConfirmation && (
|
||||
<div className="flex pt-2 pb-4 mt-5 text-center border-b dark:border-gray-900 sm:mt-0 sm:pt-4">
|
||||
<span className="flex self-center ltr:mr-2 rtl:ml-2 font-medium text-gray-700 dark:text-gray-50">
|
||||
<span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50">
|
||||
{t("add_to_calendar")}
|
||||
</span>
|
||||
<div className="flex justify-center flex-grow text-center">
|
||||
|
@ -162,7 +171,8 @@ export default function Success(props: inferSSRProps<typeof getServerSideProps>)
|
|||
.utc()
|
||||
.format("YYYYMMDDTHHmmss[Z]")}&text=${eventName}&details=${
|
||||
props.eventType.description
|
||||
}` + (typeof location === "string" ? encodeURIComponent(location) : "")
|
||||
}` +
|
||||
(typeof location === "string" ? "&location=" + encodeURIComponent(location) : "")
|
||||
}>
|
||||
<a className="w-10 h-10 px-3 py-2 mx-2 border rounded-sm border-neutral-200 dark:border-neutral-700 dark:text-white">
|
||||
<svg
|
||||
|
|
1
public/integrations/jitsi.svg
Normal file
1
public/integrations/jitsi.svg
Normal file
File diff suppressed because one or more lines are too long
After (image error) Size: 17 KiB |
|
@ -553,6 +553,7 @@
|
|||
"cal_provide_zoom_meeting_url": "Cal poskytne URL Zoom meetingu.",
|
||||
"cal_provide_tandem_meeting_url": "Cal poskytne URL Tandem meetingu.",
|
||||
"cal_provide_video_meeting_url": "Cal poskytne URL Daily video meetingu.",
|
||||
"cal_provide_jitsi_meeting_url": "Cal poskytne URL Jitsi Meet video meetingu.",
|
||||
"cal_provide_huddle01_meeting_url": "Cal poskytne URL Huddle01 web3 video meetingu.",
|
||||
"require_payment": "Vyžadovat platbu",
|
||||
"commission_per_transaction": "provize za transakci",
|
||||
|
|
|
@ -557,6 +557,7 @@
|
|||
"cal_provide_zoom_meeting_url": "Cal stellt eine Zoom Meeting-URL zur Verfügung.",
|
||||
"cal_provide_tandem_meeting_url": "Cal stellt eine Tandem Meeting-URL zur Verfügung.",
|
||||
"cal_provide_video_meeting_url": "Cal stellt eine tägliche Video-Meeting-URL zur Verfügung.",
|
||||
"cal_provide_jitsi_meeting_url": "Cal stellt eine Jitsi Meet URL zur Verfügung.",
|
||||
"cal_provide_huddle01_meeting_url": "Cal stellt eine tägliche Huddle01-Web3-Meeting-URL zur Verfügung.",
|
||||
"require_payment": "Zahlung erforderlich",
|
||||
"commission_per_transaction": "Provision pro Transaktion",
|
||||
|
|
|
@ -559,6 +559,7 @@
|
|||
"cal_provide_zoom_meeting_url": "Cal will provide a Zoom meeting URL.",
|
||||
"cal_provide_tandem_meeting_url": "Cal will provide a Tandem meeting URL.",
|
||||
"cal_provide_video_meeting_url": "Cal will provide a Daily video meeting URL.",
|
||||
"cal_provide_jitsi_meeting_url": "We will generate a Jitsi Meet URL for you.",
|
||||
"cal_provide_huddle01_meeting_url": "Cal will provide a Huddle01 web3 video meeting URL.",
|
||||
"require_payment": "Require Payment",
|
||||
"commission_per_transaction": "commission per transaction",
|
||||
|
|
|
@ -525,6 +525,7 @@
|
|||
"cal_provide_zoom_meeting_url": "Cal proporcionará una URL de reunión de Zoom.",
|
||||
"cal_provide_tandem_meeting_url": "Cal proporcionará una URL de reunión de Tandem.",
|
||||
"cal_provide_video_meeting_url": "Cal proporcionará una URL de reunión de Daily Video.",
|
||||
"cal_provide_jitsi_meeting_url": "Cal proporcionará una URL de reunión de Jitsi Meet.",
|
||||
"cal_provide_huddle01_meeting_url": "Cal proporcionará una URL de reunión de Huddle01 Web3 Video.",
|
||||
"require_payment": "Requiere Pago",
|
||||
"commission_per_transaction": "Comisión por Transacción",
|
||||
|
|
|
@ -491,6 +491,7 @@
|
|||
"cal_provide_zoom_meeting_url": "Cal fournira une URL de réunion Zoom.",
|
||||
"cal_provide_tandem_meeting_url": "Cal fournira une URL de réunion Tandem.",
|
||||
"cal_provide_video_meeting_url": "Cal fournira une URL de réunion Daily video.",
|
||||
"cal_provide_jitsi_meeting_url": "Cal fournira une URL de réunion Jitsi Meet.",
|
||||
"cal_provide_huddle01_meeting_url": "Cal fournira une URL de réunion Huddle01 web3 video.",
|
||||
"require_payment": "Exiger un paiement",
|
||||
"commission_per_transaction": "commission par transaction",
|
||||
|
|
|
@ -517,6 +517,7 @@
|
|||
"cal_provide_zoom_meeting_url": "Cal fornirà un URL di riunione Zoom.",
|
||||
"cal_provide_tandem_meeting_url": "Cal fornirà un URL di riunione Tandem.",
|
||||
"cal_provide_video_meeting_url": "Cal fornirà un URL di riunione Daily video.",
|
||||
"cal_provide_jitsi_meeting_url": "Cal fornirà un URL di riunione Jitsi Meet.",
|
||||
"cal_provide_huddle01_meeting_url": "Cal fornirà un URL di riunione Huddle01 web3 video.",
|
||||
"require_payment": "Richiedi Pagamento",
|
||||
"commission_per_transaction": "commissione per transazione",
|
||||
|
|
|
@ -489,6 +489,7 @@
|
|||
"cal_provide_zoom_meeting_url": "カルはZoomミーティングURLを提供します。",
|
||||
"cal_provide_tandem_meeting_url": "カルはTandemミーティングURLを提供します。",
|
||||
"cal_provide_video_meeting_url": "カルは毎日のビデオミーティングのURLを提供します。",
|
||||
"cal_provide_jitsi_meeting_url": "カルはJitsi MeetミーティングURLを提供します。",
|
||||
"cal_provide_huddle01_meeting_url": "カルはHuddle01 Web3ミーティングURLを提供します。",
|
||||
"require_payment": "お支払いが必要です",
|
||||
"commission_per_transaction": "取引あたりの手数料",
|
||||
|
|
|
@ -512,6 +512,7 @@
|
|||
"cal_provide_zoom_meeting_url": "Cal은 Zoom 회의 URL을 제공합니다.",
|
||||
"cal_provide_tandem_meeting_url": "Cal은 Tandem 회의 URL을 제공합니다.",
|
||||
"cal_provide_video_meeting_url": "Cal은 일일 화상 회의 URL을 제공합니다.",
|
||||
"cal_provide_jitsi_meeting_url": "Cal은 Jitsi Meet 회의 URL을 제공합니다.",
|
||||
"cal_provide_huddle01_meeting_url": "Cal은 Huddle01 Web3 회의 URL을 제공합니다.",
|
||||
"require_payment": "지불 요청",
|
||||
"commission_per_transaction": "거래당 수수료",
|
||||
|
|
|
@ -482,6 +482,7 @@
|
|||
"cal_provide_zoom_meeting_url": "Cal zal een Zoom meeting-URL meegeven in de afspraak bevestiging.",
|
||||
"cal_provide_tandem_meeting_url": "Cal zal een Tandem meeting-URL meegeven in de afspraak bevestiging.",
|
||||
"cal_provide_video_meeting_url": "Cal zal een Daily meeting-URL meegeven in de afspraak bevestiging.",
|
||||
"cal_provide_jitsi_meeting_url": "Cal zal een Jitsi Meet meeting-URL meegeven in de afspraak bevestiging.",
|
||||
"cal_provide_huddle01_meeting_url": "Cal zal een Huddle01 web3 meeting-URL meegeven in de afspraak bevestiging.",
|
||||
"require_payment": "Betaling vereisen",
|
||||
"commission_per_transaction": "commissie per transactie",
|
||||
|
|
|
@ -527,6 +527,7 @@
|
|||
"cal_provide_google_meet_location": "Cal zapewni lokalizację Google Meet.",
|
||||
"cal_provide_zoom_meeting_url": "Cal zapewni URL spotkania Zoom.",
|
||||
"cal_provide_video_meeting_url": "Kal poda adres URL spotkania wideo platformy Daily.",
|
||||
"cal_provide_jitsi_meeting_url": "Cal zapewni URL spotkania Jitsi Meet.",
|
||||
"cal_provide_huddle01_meeting_url": "Cal zapewni URL spotkania Huddle01 Web3.",
|
||||
"require_payment": "Wymagaj płatności",
|
||||
"commission_per_transaction": "prowizja za transakcję",
|
||||
|
|
|
@ -528,6 +528,7 @@
|
|||
"cal_provide_zoom_meeting_url": "Cal fornecerá uma URL de reunião do Zoom.",
|
||||
"cal_provide_tandem_meeting_url": "Cal fornecerá uma URL de reunião do Tandem.",
|
||||
"cal_provide_video_meeting_url": "O Cal irá fornecer um URL de reunião do Daily video.",
|
||||
"cal_provide_jitsi_meeting_url": "O Cal irá fornecer um URL de reunião do Jitsi Meet.",
|
||||
"cal_provide_huddle01_meeting_url": "O Cal irá fornecer um URL de reunião do Huddle01 Web3 video.",
|
||||
"require_payment": "Requerer Pagamento",
|
||||
"commission_per_transaction": "comissão por transação",
|
||||
|
|
|
@ -559,6 +559,7 @@
|
|||
"cal_provide_zoom_meeting_url": "O Cal irá fornecer um URL de reunião do Zoom.",
|
||||
"cal_provide_tandem_meeting_url": "O Cal irá fornecer um URL de reunião do Tandem.",
|
||||
"cal_provide_video_meeting_url": "O Cal irá fornecer um URL de reunião do Daily video.",
|
||||
"cal_provide_jitsi_meeting_url": "O Cal irá fornecer um URL de reunião vídeo do Jitsi Meet.",
|
||||
"cal_provide_huddle01_meeting_url": "O Cal irá fornecer um URL de reunião vídeo do Huddle01 Web3.",
|
||||
"require_payment": "Requer Pagamento",
|
||||
"commission_per_transaction": "comissão por transação",
|
||||
|
|
|
@ -489,6 +489,7 @@
|
|||
"cal_provide_zoom_meeting_url": "Cal va oferi un URL pentru ședința de Zoom.",
|
||||
"cal_provide_tandem_meeting_url": "Cal va oferi un URL pentru ședința de Tandem.",
|
||||
"cal_provide_video_meeting_url": "Cal va oferi un URL pentru ședința de Daily.",
|
||||
"cal_provide_jitsi_meeting_url": "Cal va oferi un URL pentru ședința de Jitsi Meet.",
|
||||
"cal_provide_huddle01_meeting_url": "Cal va oferi un URL pentru ședința de Huddle01 Web3.",
|
||||
"require_payment": "Solicită plata",
|
||||
"commission_per_transaction": "comision per tranzacție",
|
||||
|
|
|
@ -529,6 +529,7 @@
|
|||
"cal_provide_zoom_meeting_url": "Cal создаст ссылку на встречу в Zoom.",
|
||||
"cal_provide_tandem_meeting_url": "Cal создаст ссылку на встречу в Tandem.",
|
||||
"cal_provide_video_meeting_url": "Cal создаст ссылку на встречу в Daily.",
|
||||
"cal_provide_jitsi_meeting_url": "Cal создаст ссылку на встречу в Jitsi Meet.",
|
||||
"cal_provide_huddle01_meeting_url": "Cal создаст ссылку на встречу в Huddle01 Web3.",
|
||||
"require_payment": "Требуется оплата",
|
||||
"commission_per_transaction": "комиссия за сделку",
|
||||
|
|
|
@ -534,6 +534,7 @@
|
|||
"cal_provide_google_meet_location": "Cal 将提供 Google Meet 位置。",
|
||||
"cal_provide_zoom_meeting_url": "Cal 将提供Zoom会议 URL",
|
||||
"cal_provide_video_meeting_url": "Cal 将提供Daily视频会议 URL",
|
||||
"cal_provide_jitsi_meeting_url": "Cal 将提供Jitsi Meet视频会议 URL",
|
||||
"cal_provide_huddle01_meeting_url": "Cal 将提供Huddle01 Web3视频会议 URL",
|
||||
"require_payment": "需要付款",
|
||||
"commission_per_transaction": "每笔交易的佣金",
|
||||
|
|
Loading…
Reference in a new issue