Compare commits

...
111 Commits
Author SHA1 Message Date
Alex van Andel 0910f65b87 v3.5.2
release.yml / release (push) Canceled after 0s
2023-11-23 16:56:52 +00:00
Alex van Andelandsean-brydon 343f8ee303 Avatar write and unset, ensure no bad behaviour (#12504)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-11-23 15:39:50 +00:00
Hariom Balhara de479bb2da fix: getting-started crash and build failure (#12506) 2023-11-22 22:41:20 -07:00
Omar López 5dc3065a47 v3.5.1
release.yml / release (push) Canceled after 0s
2023-11-22 15:42:24 -07:00
Manpreet Singh 36d315343c fix: adds teamId to team events payload (#12417) 2023-11-22 14:54:18 -05:00
zomars 2171a320f5 fix: Locks Stripe version 2023-11-22 12:19:30 -07:00
JA 7ee035c3a3 feat(webhooks): pass webhook secret to testTrigger (#12187) 2023-11-22 14:17:50 -05:00
Greg PabianandOmar López 1de05ebf3d chore: migrate the event-types page to the app directory (#12390)
Co-authored-by: Omar López <zomars@me.com>
2023-11-22 19:17:33 +00:00
zomars 53d7e2ec3f syncpack fix-mismatches 2023-11-22 12:08:30 -07:00
Udit Takkar f65c7e413f fix: default organizer bug in managed event type (#11921) 2023-11-22 12:43:25 -05:00
Joe Au-YeungandAlex van Andel b762f60214 test: Integration Test GCal Primary Calendar (#12011)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-11-22 11:15:47 -03:00
Joe Au-Yeung 2498785c49 chore: Clean Up Delete Credential Selected Calendar Error Message (#12353) 2023-11-22 10:22:03 -03:00
Matt Nicolls 9a6683e01d fix: include eventTypeId in BOOKING_CANCELLED event (#12445) 2023-11-22 12:04:51 +00:00
sebzzandUdit Takkar 2853288f49 docs: add google credentials in example env (#11695)
* docs:add google credentials in example env

* docs: add a space after #

* chore: update .env.example

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
2023-11-22 11:49:27 +00:00
Morgan d04226ab9a fix: alby payment isPaid always false on create (#12463) 2023-11-22 11:39:00 +00:00
cb7ddc455a chore: Add team invite tests (#12425)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-11-22 11:26:43 +00:00
Adugna TadesseandJoe Au-Yeung 73aa1e8a22 outlook second account fix (#12013)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-11-22 11:01:29 +00:00
Udit Takkar a3b5263b76 chore: reset form on submission (#12465) 2023-11-22 10:42:19 +00:00
Peer Richelsen af2c6c0844 chore: ignore "platform" in pr-assign-team workflow (#12487) 2023-11-22 10:25:56 +00:00
Peer Richelsen 113195224a chore: fixed cal.ai thumbnail (#12486) 2023-11-22 10:23:54 +00:00
Peer Richelsen 828092c1d0 chore: fix cal.ai price (#12485) 2023-11-22 10:18:09 +00:00
Somay ChauhanandKeith Williams 4b060fc2cd fix: opening team invite link in email throws error on signup page (#12397)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-11-22 06:49:37 -03:00
Syed Ali ShahbazandUdit Takkar 16c5b070b6 fix: Admin Logic for event-type API endpoint (#12482)
* Fix Admin logic

* chore: fix prettier

---------

Co-authored-by: Udit Takkar <udit222001@gmail.com>
2023-11-22 06:46:03 -03:00
Hariom Balhara 48dde246e9 test: Add more orgs tests (#12241) 2023-11-21 14:03:01 -03:00
Morgan 556b382f75 fix: better errors for googlecalendar integration (#12403) 2023-11-21 15:14:25 +00:00
85237c4985 fix: Date overrides UI bug depending on screen size (#12423)
* Update DateOverrideInputDialog.tsx

fix: Date overrides UI bug depending on screen size (calcom#12406)

* chore: remove comment

---------

Co-authored-by: madhurgoyal19 <35370133+madhurgoyal19@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2023-11-21 14:56:59 +00:00
Crowdin Bot 404bc0e4d6 New Crowdin translations by Github Action 2023-11-21 12:13:50 +00:00
Morgan 00553e897b fix: alby payment could not be created (#12460)
* fix: alby payment could not be created

* fixup! fix: alby payment could not be created

* fixup! fixup! fix: alby payment could not be created
2023-11-21 12:09:50 +00:00
Crowdin Bot ed2ce005c9 New Crowdin translations by Github Action 2023-11-20 16:42:20 +00:00
Peer Richelsen 058844b6b8 chore: paid support wip (#12419)
* paid support wip

* nit

* added upgrade box to intercom

* nit
2023-11-20 22:08:39 +05:30
77fa3702db fix: slug value in webhook payload (#12290)
Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-11-20 15:39:21 +00:00
9eb2ae470c fix: [embed-react] types fix for compatible with TypeScript's "moduleResolution": "bundler" (#12327)
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-11-20 13:33:27 +00:00
Somay Chauhan 3de7b73f5f fix: don't send notes to booker if "Hide notes in calendar" is on (#12333) 2023-11-20 13:28:40 +00:00
Alex van Andel 8e7785ca64 fix: Better error reporting & some fixes to deleteUser API (#12439) 2023-11-20 13:28:25 +00:00
sean-brydonandUdit Takkar 608b891536 fix: identifer (#12426)
Co-authored-by: Udit Takkar <udit222001@gmail.com>
2023-11-20 13:28:10 +00:00
Alex van AndelandPeer Richelsen 4f26ca1a7b feat: Base implementation of v2 of avatars (#12369)
* feat: Base implementation of v2 of avatars

* Make avatarUrl and logoUrl entirely optional

* Made necessary backwards compat changes

* fix: type errors

* Fix: OG image

* fix types

* Consistency with other behaviour, ux tweak

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-11-20 12:49:38 +00:00
Hariom Balhara c55b36f235 fix: Members count when team slug is same as org slug (#12124) 2023-11-20 18:01:50 +05:30
sean-brydon bdd3b132d4 feat: troubleshooter with weekly view (V2) (#12280)
* Inital UI + layout setup

* use booker approach of grid

* event-select - sidebar + store work

* adds get schedule by event-type-slug

* Calendar toggle

* Load schedule from event slug

* Add busy events to calendar

* useschedule

* Store more event info than just slug

* Add date override to calendar

* Changes sizes on smaller screens

* add event title as a tooltip

* Ensure header navigation works

* Stop navigator throwing errors on inital render

* Correct br

* Event duration fixes

* Add getMoreInfo if user is authed with current request.username

* Add calendar color map wip

* Add WIP comments for coloured outlines

* Revert more info changes

* Calculate date override correctly

* Add description option

* Fix inital schedule data not being populated

* Nudge overlap over to make it clearer

* Fix disabled state

* WIP on math logic

* Event list overlapping events logic

* NIT about width

* i18n + manage calendars link

* Delete old troubleshooter

* Update packages/features/calendars/weeklyview/components/event/EventList.tsx

* Remove t-slots

* Fix i18n & install calendar action

* sm:imrovments

* NITS

* Fix types

* fix: back button

* Month prop null as we control from query param

* Add head SEO

* Fix headseo import

* Fix date override tests
2023-11-20 17:49:33 +05:30
Crowdin Bot 9a4c20cca4 New Crowdin translations by Github Action 2023-11-20 11:08:01 +00:00
Hariom Balhara 2144fcb23e fix: add node-mocks-http to web (#12435) 2023-11-20 16:34:29 +05:30
Crowdin Bot f2c39fc786 New Crowdin translations by Github Action 2023-11-17 16:12:37 +00:00
Crowdin Bot e83e0a770c New Crowdin translations by Github Action 2023-11-17 16:09:31 +00:00
Omar Lópezandsean-brydon c818ef3188 feat: implements basic user locking for admins (#12393)
* feat: implements basic user locking for admins

* Update sendPasswordReset.handler.ts

* check fixes

* Update packages/features/ee/users/components/UsersTable.tsx

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-11-17 16:06:29 +00:00
MehulandPeer Richelsen 176564f4ed fix: logo lack of contrast (#12401)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-11-17 14:10:58 +00:00
Morgan 27d59b6413 fix: add idx scheduleId on eventType (#12400) 2023-11-17 11:01:13 +00:00
Somay ChauhanandPeer Richelsen 57a65401e7 fix: weird date override behaviour (#12292)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-11-16 21:09:48 +00:00
DmytroHryshynandzomars 28acbe549a chore: [app dir bootstrapping 9]: replace useSearchParams with useCompatSearchParams hook (#12056)
Co-authored-by: zomars <zomars@me.com>
2023-11-16 13:38:27 -07:00
Keith Williams 3534e3c224 Revert "perf: Increased memory/vCPU for slots perf (#12387)" (#12389)
This reverts commit 0b96ef5476.
2023-11-16 20:40:42 +02:00
Erik e48a6c3dcf chore: Disable i18n cache in development (#12374) 2023-11-16 16:56:24 +00:00
Carina WollendorferandCarinaWolli 7d09ccb0d7 fix: add sendgrid-specific header only for SendGrid smtp server (#11932)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-11-16 13:51:30 -03:00
Morgan 9029c5b4c4 fix: change api root statusCode to 200 (#12362) 2023-11-16 16:50:43 +00:00
a4c1df3658 refactor: team settings redesign (#12230)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-11-16 16:18:24 +00:00
Keith Williams 0b96ef5476 perf: Increased memory/vCPU for slots perf (#12387) 2023-11-16 11:57:37 -03:00
shaharyarshamshiandPeer Richelsen ca9e0f5b72 refactor: Removed the dirName config for app store cli (#12321)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-11-16 13:59:01 +00:00
a9418b06d6 fix: Wrong spelling of the word "pode" (fix-wrongStringPT-BR) (#12344)
Co-authored-by: gitstart-calcom <gitstart-calcom@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-11-16 13:15:38 +00:00
Crowdin Bot 833a326c5b New Crowdin translations by Github Action 2023-11-16 13:09:03 +00:00
sean-brydon b7e2a62e26 chore: Use apiLogin instead of user.login in e2e tests (#12382) 2023-11-16 13:05:39 +00:00
Crowdin Bot f687056d60 New Crowdin translations by Github Action 2023-11-16 08:51:33 +00:00
0a39f53a4b fix(stripePaymentCallback): better errors (#12223)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-11-16 08:48:46 +00:00
Joe Au-Yeung ea0a64624c fix: saving credential id for payment apps (#12251)
## What does this PR do?

<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->

This PR adds the `credentialId` to payment app data. This fixes a bug where team installed payment apps were not working with team events.

Fixes # (issue)

<!-- Please provide a loom video for visual changes to speed up reviews
 Loom Video: https://www.loom.com/
-->

## Requirement/Documentation

<!-- Please provide all documents that are important to understand the reason of that PR. -->

- If there is a requirement document, please, share it here.
- If there is ab UI/UX design document, please, share it here.

## Type of change

<!-- Please delete bullets that are not relevant. -->

- [x] Bug fix (non-breaking change which fixes an issue)

## How should this be tested?

<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. Write details that help to start the tests -->

- Install Stripe for the individual user
- Enable it for the individual's event type
- The `credentialId` should be saved to the metadata
- Install Stripe to the user's team
- Enable it in the team's event type
- The `credentialId` should be saved to the metadata

## Mandatory Tasks

- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.

## Checklist

<!-- Remove bullet points below that don't apply to you -->

- I haven't checked if new and existing unit tests pass locally with my changes
2023-11-15 19:59:43 -07:00
Erik 6a8726f5f8 chore: Insights readonly DB client (#12373) 2023-11-15 14:50:20 -07:00
Omar López eb97e1660b v3.5.0
release.yml / release (push) Canceled after 0s
2023-11-15 14:48:10 -07:00
Udit Takkar 371a0f7245 feat: booking errors logging (#12325)
Fixes: https://github.com/calcom/cal.com/issues/12297
Fixes https://github.com/calcom/cal.com/issues/11234

- Displaying error message and X-Vercel-Id( Unique Request Id ) to user on book event form
- Improve error logging 
- Add Error codes

Few things to discuss

1) How to handle calendar integration failures ?
   Currently if for example google integration is broken and someone is trying to book that person then we log the error but don't inform the user that the google calendar is broken and the meeting goes through.
   
 Should I throw error when integration is broken ?
   
<img width="758" alt="Screenshot 2023-11-12 at 12 52 36 AM" src="https://github.com/calcom/cal.com/assets/53316345/c4d921c4-9c8a-4b9b-82a2-bbe0fdbcb3d4">

   
2)  How to handle conferencing app failures? 
 We just default to Cal Video  as location if we are unable to generated conferencing url and log the error and not inform the user(organizer).
2023-11-15 12:52:19 -07:00
Carina Wollendorfer 270d4f6e82 fix: round-robin booked out of availability (#12376)
## What does this PR do?

Fixes that it can happen that Round Robin host is booked outside of availability. 

I found and fixed the following two scenarios where this can happen: 
- when host has a date override
- when host is available for only a part the event time (for example, booking time 9:00-11:00 and user is only available between 10:00-11:00)

Fixes #10315
Fixes #11690

It also fixes that it can happen that round robin doesn't correctly pick the luck user (least recently booked). This happened when a user was an attendee of a booking before, then we always compared this booking and never the actual last booking of this user. 

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How should this be tested?

#### Booked outside of availability: 
1. 
- Create Round Robin event and assign user1 and user2 as round robin hosts
   - event duration: 120 minutes 
- user 1 availability: 
  - Monday to Friday 9:00-17:00
- user2 availability: 
  - Monday to Friday 10:00-17:00
- Book event at a 9:00 slot -> check if i user1 is booked 
- Book event again at a 9:00 slot -> check if user1 is booked again (user2 is not available at that time)

2.
- Change availability of user2
   - Mark Monday as unavailable 
   -   Add date override on any day this month 
- Book any Monday this month -> see that user 1 is booked 
- Again Book any Monday this month -> see that user 1 is booked again 

#### Wrong lucky user 
- Book event and add user1's email as the attendee email address
- Book several slots where both users should be available, and see that it alternates between user1 and user2 (before it ended up always booking user1)

## Mandatory Tasks

- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
2023-11-15 12:49:03 -07:00
DmytroHryshyn 48e7b616b8 chore: [app dir bootstrapping 10] adds ab tests middleware (#12045)
## What does this PR do?
 - Adds AB tests middleware that redirects users to the app-dir pages with probability that is defined by env var
 
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->

Fixes # (issue)

<!-- Please provide a loom video for visual changes to speed up reviews
 Loom Video: https://www.loom.com/
-->

## Requirement/Documentation

<!-- Please provide all documents that are important to understand the reason of that PR. -->

This PR requires new env variables:
`APP_ROUTER_EVENT_TYPES_ENABLED`  - boolean that defines if app dir event-types page redirect is enabled
`AB_TEST_BUCKET_PROBABILITY` - number [0, 100] that defines the percentage of users getting redirected to the app dir pages

## Type of change

<!-- Please delete bullets that are not relevant. -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] Chore (refactoring code, technical debt, workflow improvements)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

## How should this be tested?

<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. Write details that help to start the tests -->
Does not requires testing, implements middleware as a dead code

## Mandatory Tasks

- [x] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
2023-11-15 11:06:18 -07:00
Crowdin Bot 8b80ea99c2 New Crowdin translations by Github Action 2023-11-15 16:18:21 +00:00
Rajiv Sahal 150c6e0f50 refactor: rename AtomsProvider to CalProvider (#12375)
* rename context provider

* rename atoms provider export
2023-11-15 16:14:37 +00:00
Crowdin Bot 4037e87fbd New Crowdin translations by Github Action 2023-11-15 12:33:14 +00:00
a804a29516 feat: Stripe paid apps flow (#12103)
* chore: Stripe paid apps flow

* chore: Subscription

* chore: Webhooks

* chore: Abstract functions

* chore: Lockfile

* chore: Webhook handler

* chore: Use catch-all

* chore: Webhook changes, etc

* chore: Cleanup

* chore: Use actual price id

* chore: Updates

* chore: Install normally until expiry date

* Disable team install for paid apps and cal.ai\

* Fix the same at another place

* Fix Typescript error

* redactedCause doesnt have message has enumerable prop

* Fix reinstallation of an already installed app

* chore: Remove unused deps

* chore: Ensure index

* chore: Price in usd

* chore: PR suggestion

* Fix missing packages in yarn.lock

---------

Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-11-15 09:29:41 -03:00
Crowdin Bot 87b514b91b New Crowdin translations by Github Action 2023-11-15 10:14:51 +00:00
Crowdin Bot 693ca04626 New Crowdin translations by Github Action 2023-11-15 10:11:45 +00:00
Crowdin Bot 67f83c7e7d New Crowdin translations by Github Action 2023-11-15 10:08:40 +00:00
Crowdin Bot ecfecd832d New Crowdin translations by Github Action 2023-11-15 10:05:17 +00:00
Crowdin Bot e55302cbc5 New Crowdin translations by Github Action 2023-11-15 10:02:20 +00:00
Crowdin Bot 9514a68039 New Crowdin translations by Github Action 2023-11-15 09:59:22 +00:00
Crowdin Bot 4e3d159401 New Crowdin translations by Github Action 2023-11-15 09:56:40 +00:00
Mehul a519941b81 fix avatar remove button (#12249) 2023-11-15 15:23:33 +05:30
Crowdin Bot f62c58532d New Crowdin translations by Github Action 2023-11-15 00:52:06 +00:00
Ash Davis 0981c4cb23 feat: POST/PATCH availabilities API to accept date field (#12238) 2023-11-14 21:49:27 -03:00
9d06f6dd0e fix: Fix 'Book a new time' link in request-reschedule for Team Event (#12261)
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-11-14 13:23:44 -07:00
Morgan 00df493623 revert: "fix: create prisma client singleton with immediately invoked function" (#12366) 2023-11-14 20:22:35 +00:00
Crowdin Bot 2cf2b11a7b New Crowdin translations by Github Action 2023-11-14 19:32:20 +00:00
Joe Au-Yeung 5c3fd0993e fix: getting shimmer app keys (#12363)
* Fix getting shimmer app keys

* Read from config
2023-11-15 00:59:32 +05:30
Crowdin Bot 811e655720 New Crowdin translations by Github Action 2023-11-14 15:30:43 +00:00
Crowdin Bot defaafc9be New Crowdin translations by Github Action 2023-11-14 15:27:31 +00:00
Crowdin Bot e0c6feff1f New Crowdin translations by Github Action 2023-11-14 15:23:26 +00:00
Morgan d17dcbd319 v3.4.10
release.yml / release (push) Canceled after 0s
2023-11-14 17:19:22 +02:00
Rajiv Sahal 90d52663ac feat: AtomsProvider component (#12354)
* init: atoms provider component

* chore: add atoms provider to index of atoms package

* cleanup comments
2023-11-14 20:47:48 +05:30
sean-brydon ada74467bc fix: Callback redirect (#12361) 2023-11-14 12:15:50 -03:00
Crowdin Bot 7db758c2d5 New Crowdin translations by Github Action 2023-11-14 14:04:22 +00:00
Amit Sharma 2867d53442 fix resend team member invite (#12360) 2023-11-14 13:59:49 +00:00
Crowdin Bot 31067fcd45 New Crowdin translations by Github Action 2023-11-14 12:59:06 +00:00
Hariom Balhara 8c2ce972cc fix: team booking page having same slug as the org (#12213) 2023-11-14 18:25:46 +05:30
Crowdin Bot d3ab11e38e New Crowdin translations by Github Action 2023-11-14 11:54:31 +00:00
Crowdin Bot 3d596fcb1a New Crowdin translations by Github Action 2023-11-14 11:51:13 +00:00
Crowdin Bot 820b45a912 New Crowdin translations by Github Action 2023-11-14 11:48:30 +00:00
Udit Takkar b945849736 chore: variables names (#12352) 2023-11-14 11:45:09 +00:00
Crowdin Bot 6b2c59b733 New Crowdin translations by Github Action 2023-11-14 10:27:14 +00:00
Crowdin Bot b7f85d785a New Crowdin translations by Github Action 2023-11-14 10:23:40 +00:00
Crowdin Bot 08e4e816c6 New Crowdin translations by Github Action 2023-11-14 10:20:41 +00:00
Crowdin Bot 20430022e1 New Crowdin translations by Github Action 2023-11-14 10:17:32 +00:00
Crowdin Bot e4c71e5d2c New Crowdin translations by Github Action 2023-11-14 10:14:23 +00:00
Crowdin Bot 28eda407be New Crowdin translations by Github Action 2023-11-14 10:11:07 +00:00
Crowdin Bot 9dab171520 New Crowdin translations by Github Action 2023-11-14 10:07:51 +00:00
Crowdin Bot d5f359291d New Crowdin translations by Github Action 2023-11-14 10:03:34 +00:00
Morgan 3c1d0a9bf1 fix: add translations to api dockerfile (#12349) 2023-11-14 09:59:32 +00:00
Crowdin Bot 3057edbd87 New Crowdin translations by Github Action 2023-11-14 04:59:52 +00:00
Carina WollendorferandCarinaWolli a2f859b55a fix: unhandled promise rejection in scheduleWorkflowReminder (#12301)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-11-14 10:27:16 +05:30
Crowdin Bot b2f4eaee63 New Crowdin translations by Github Action 2023-11-13 21:53:32 +00:00
Hariom Balhara b66c3f3452 fix: Org: Any team URL with slug starting with d is 404 (#12302) 2023-11-13 14:50:54 -07:00
Keith Williams 5ab0b85e52 chore: Reduce slots API serverless memory configuration (#12270)
* chore: Reduce API serverless memory configuration

* Only limit to slots for now

* Moved the vercel.json file to root

* More fun with vercel.json files

* Trying new pattern

* Using a pattern that matches the Vercel deployment summary function name

* Trying to set the memory directly in the handler file

* Trying it on all functions

* Changed glob

* Moved vercel.json file to root

* Trying a new glob

* Give me all functions

* Trying just * for all functions

* Trying again...

* Putting the config on the defaultResponder

* Seeing if web app takes setings

* Changed .js to .ts

* Changed root path

* Added back vercel.json now that the path is changed

* Reduced API slots memory to 512

* Removed unneeded code changes'

* Update defaultResponder.ts
2023-11-13 15:37:26 +00:00
325 changed files with 7979 additions and 2485 deletions

No files matched your search

+26
View File
@@ -25,6 +25,7 @@ CALCOM_LICENSE_KEY=
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso" DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
UPSTASH_REDIS_REST_URL= UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN= UPSTASH_REDIS_REST_TOKEN=
INSIGHTS_DATABASE_URL=
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy # Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
# Cold boots will be faster and you'll be able to scale your DB independently of your app. # Cold boots will be faster and you'll be able to scale your DB independently of your app.
@@ -106,6 +107,19 @@ NEXT_PUBLIC_HELPSCOUT_KEY=
NEXT_PUBLIC_FRESHCHAT_TOKEN= NEXT_PUBLIC_FRESHCHAT_TOKEN=
NEXT_PUBLIC_FRESHCHAT_HOST= NEXT_PUBLIC_FRESHCHAT_HOST=
# Google OAuth credentials
# To enable Login with Google you need to:
# 1. Set `GOOGLE_API_CREDENTIALS` above
# 2. Set `GOOGLE_LOGIN_ENABLED` to `true`
# When self-hosting please ensure you configure the Google integration as an Internal app so no one else can login to your instance
# @see https://support.google.com/cloud/answer/6158849#public-and-internal&zippy=%2Cpublic-and-internal-applications
GOOGLE_LOGIN_ENABLED=false
# - GOOGLE CALENDAR/MEET/LOGIN
# Needed to enable Google Calendar integration and Login with Google
# @see https://github.com/calcom/cal.com#obtaining-the-google-api-credentials
GOOGLE_API_CREDENTIALS=
# Inbox to send user feedback # Inbox to send user feedback
SEND_FEEDBACK_EMAIL= SEND_FEEDBACK_EMAIL=
@@ -236,6 +250,12 @@ AUTH_BEARER_TOKEN_VERCEL=
E2E_TEST_APPLE_CALENDAR_EMAIL="" E2E_TEST_APPLE_CALENDAR_EMAIL=""
E2E_TEST_APPLE_CALENDAR_PASSWORD="" E2E_TEST_APPLE_CALENDAR_PASSWORD=""
# - CALCOM QA ACCOUNT
# Used for E2E tests on Cal.com that require 3rd party integrations
E2E_TEST_CALCOM_QA_EMAIL="qa@example.com"
# Replace with your own password
E2E_TEST_CALCOM_QA_PASSWORD="password"
# - APP CREDENTIAL SYNC *********************************************************************************** # - APP CREDENTIAL SYNC ***********************************************************************************
# Used for self-hosters that are implementing Cal.com into their applications that already have certain integrations # Used for self-hosters that are implementing Cal.com into their applications that already have certain integrations
# Under settings/admin/apps ensure that all app secrets are set the same as the parent application # Under settings/admin/apps ensure that all app secrets are set the same as the parent application
@@ -263,3 +283,9 @@ E2E_TEST_OIDC_USER_EMAIL=
E2E_TEST_OIDC_USER_PASSWORD= E2E_TEST_OIDC_USER_PASSWORD=
# *********************************************************************************************************** # ***********************************************************************************************************
# provide a value between 0 and 100 to ensure the percentage of traffic
# redirected from the legacy to the future pages
AB_TEST_BUCKET_PROBABILITY=50
# whether we redirect to the future/event-types from event-types or not
APP_ROUTER_EVENT_TYPES_ENABLED=1
+1 -1
View File
@@ -13,4 +13,4 @@ jobs:
with: with:
repo-token: ${{ secrets.GH_ACCESS_TOKEN }} repo-token: ${{ secrets.GH_ACCESS_TOKEN }}
organization-name: calcom organization-name: calcom
ignore-labels: "app-store, ai, authentication, automated-testing, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier" ignore-labels: "app-store, ai, authentication, automated-testing, platform, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, teams, webhooks, workflows, zapier"
+20 -29
View File
@@ -1,7 +1,7 @@
import type { NextMiddleware } from "next-api-middleware"; import type { NextMiddleware } from "next-api-middleware";
import { CONSOLE_URL } from "@calcom/lib/constants"; import { CONSOLE_URL } from "@calcom/lib/constants";
import prisma, { customPrisma } from "@calcom/prisma"; import { customPrisma } from "@calcom/prisma";
const LOCAL_CONSOLE_URL = process.env.NEXT_PUBLIC_CONSOLE_URL || CONSOLE_URL; const LOCAL_CONSOLE_URL = process.env.NEXT_PUBLIC_CONSOLE_URL || CONSOLE_URL;
@@ -12,41 +12,32 @@ export const customPrismaClient: NextMiddleware = async (req, res, next) => {
} = req; } = req;
// If no custom api Id is provided, attach to request the regular cal.com prisma client. // If no custom api Id is provided, attach to request the regular cal.com prisma client.
if (!key) { if (!key) {
req.prisma = prisma; req.prisma = customPrisma();
await next(); await next();
return; return;
} }
try { // If we have a key, we check if the deployment matching the key, has a databaseUrl value set.
// If we have a key, we check if the deployment matching the key, has a databaseUrl value set. const databaseUrl = await fetch(`${LOCAL_CONSOLE_URL}/api/deployments/database?key=${key}`)
const databaseUrl = await fetch(`${LOCAL_CONSOLE_URL}/api/deployments/database?key=${key}`) .then((res) => res.json())
.then((res) => res.json()) .then((res) => res.databaseUrl);
.then((res) => res.databaseUrl);
if (!databaseUrl) { if (!databaseUrl) {
res.status(400).json({ error: "no databaseUrl set up at your instance yet" }); res.status(400).json({ error: "no databaseUrl set up at your instance yet" });
return; return;
} }
req.prisma = customPrisma({ datasources: { db: { url: databaseUrl } } }); req.prisma = customPrisma({ datasources: { db: { url: databaseUrl } } });
/* @note: /* @note:
In order to skip verifyApiKey for customPrisma requests, In order to skip verifyApiKey for customPrisma requests,
we pass isAdmin true, and userId 0, if we detect them later, we pass isAdmin true, and userId 0, if we detect them later,
we skip verifyApiKey logic and pass onto next middleware instead. we skip verifyApiKey logic and pass onto next middleware instead.
*/ */
req.isAdmin = true; req.isAdmin = true;
req.isCustomPrisma = true; req.isCustomPrisma = true;
// We don't need the key from here and on. Prevents unrecognized key errors. // We don't need the key from here and on. Prevents unrecognized key errors.
delete req.query.key; delete req.query.key;
await next(); await next();
await req.prisma.$disconnect(); await req.prisma.$disconnect();
// @ts-expect-error testing // @ts-expect-error testing
delete req.prisma; delete req.prisma;
} catch (err) {
if (req.prisma) {
await req.prisma.$disconnect();
// @ts-expect-error testing
delete req.prisma;
}
throw err;
}
}; };
+2
View File
@@ -26,6 +26,7 @@ const schemaAvailabilityCreateParams = z
startTime: z.date().or(z.string()), startTime: z.date().or(z.string()),
endTime: z.date().or(z.string()), endTime: z.date().or(z.string()),
days: z.array(z.number()).optional(), days: z.array(z.number()).optional(),
date: z.date().or(z.string()).optional(),
}) })
.strict(); .strict();
@@ -34,6 +35,7 @@ const schemaAvailabilityEditParams = z
startTime: z.date().or(z.string()).optional(), startTime: z.date().or(z.string()).optional(),
endTime: z.date().or(z.string()).optional(), endTime: z.date().or(z.string()).optional(),
days: z.array(z.number()).optional(), days: z.array(z.number()).optional(),
date: z.date().or(z.string()).optional(),
}) })
.strict(); .strict();
+10 -1
View File
@@ -21,7 +21,16 @@ export const schemaSchedulePublic = z
.merge( .merge(
z.object({ z.object({
availability: z availability: z
.array(Availability.pick({ id: true, eventTypeId: true, days: true, startTime: true, endTime: true })) .array(
Availability.pick({
id: true,
eventTypeId: true,
date: true,
days: true,
startTime: true,
endTime: true,
})
)
.transform((v) => .transform((v) =>
v.map((item) => ({ v.map((item) => ({
...item, ...item,
+6 -1
View File
@@ -316,8 +316,13 @@ async function checkPermissions(req: NextApiRequest) {
statusCode: 401, statusCode: 401,
message: "ADMIN required for `userId`", message: "ADMIN required for `userId`",
}); });
if (!isAdmin && body.teamId)
throw new HttpError({
statusCode: 401,
message: "ADMIN required for `teamId`",
});
/* Admin users are required to pass in a userId or teamId */ /* Admin users are required to pass in a userId or teamId */
if (isAdmin && (!body.userId || !body.teamId)) if (isAdmin && !body.userId && !body.teamId)
throw new HttpError({ statusCode: 400, message: "`userId` or `teamId` required" }); throw new HttpError({ statusCode: 400, message: "`userId` or `teamId` required" });
} }
+1 -1
View File
@@ -1,5 +1,5 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
export default async function CalcomApi(_: NextApiRequest, res: NextApiResponse) { export default async function CalcomApi(_: NextApiRequest, res: NextApiResponse) {
res.status(201).json({ message: "Welcome to Cal.com API - docs are at https://developer.cal.com/api" }); res.status(200).json({ message: "Welcome to Cal.com API - docs are at https://developer.cal.com/api" });
} }
+11 -2
View File
@@ -1,5 +1,6 @@
import type { NextApiRequest } from "next"; import type { NextApiRequest } from "next";
import { deleteUser } from "@calcom/features/users/lib/userDeletionService";
import { HttpError } from "@calcom/lib/http-error"; import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server"; import { defaultResponder } from "@calcom/lib/server";
@@ -41,10 +42,18 @@ export async function deleteHandler(req: NextApiRequest) {
// Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user // Here we only check for ownership of the user if the user is not admin, otherwise we let ADMIN's edit any user
if (!isAdmin && query.userId !== req.userId) throw new HttpError({ statusCode: 403, message: "Forbidden" }); if (!isAdmin && query.userId !== req.userId) throw new HttpError({ statusCode: 403, message: "Forbidden" });
const user = await prisma.user.findUnique({ where: { id: query.userId } }); const user = await prisma.user.findUnique({
where: { id: query.userId },
select: {
id: true,
email: true,
metadata: true,
},
});
if (!user) throw new HttpError({ statusCode: 404, message: "User not found" }); if (!user) throw new HttpError({ statusCode: 404, message: "User not found" });
await prisma.user.delete({ where: { id: user.id } }); await deleteUser(user);
return { message: `User with id: ${user.id} deleted successfully` }; return { message: `User with id: ${user.id} deleted successfully` };
} }
+2 -1
View File
@@ -8,6 +8,7 @@ import { describe, expect, test, vi } from "vitest";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import sendPayload from "@calcom/features/webhooks/lib/sendPayload"; import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder"; import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
@@ -148,7 +149,7 @@ describe.skipIf(true)("POST /api/bookings", () => {
expect(res._getStatusCode()).toBe(500); expect(res._getStatusCode()).toBe(500);
expect(JSON.parse(res._getData())).toEqual( expect(JSON.parse(res._getData())).toEqual(
expect.objectContaining({ expect.objectContaining({
message: "No available users found.", message: ErrorCode.NoAvailableUsersFound,
}) })
); );
}); });
+7
View File
@@ -0,0 +1,7 @@
{
"functions": {
"pages/api/slots/*.ts": {
"memory": 512
}
}
}
+2 -2
View File
@@ -10,7 +10,7 @@
"@calcom/config": "*", "@calcom/config": "*",
"@calcom/dayjs": "*", "@calcom/dayjs": "*",
"@calcom/ui": "*", "@calcom/ui": "*",
"@radix-ui/react-avatar": "^1.0.0", "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.0", "@radix-ui/react-collapsible": "^1.0.0",
"@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-dropdown-menu": "^2.0.5",
@@ -49,7 +49,7 @@
"storybook-addon-designs": "^6.3.1", "storybook-addon-designs": "^6.3.1",
"storybook-addon-next": "^1.6.9", "storybook-addon-next": "^1.6.9",
"storybook-react-i18next": "^1.1.2", "storybook-react-i18next": "^1.1.2",
"tailwindcss": "^3.3.1", "tailwindcss": "^3.3.3",
"typescript": "^4.9.4", "typescript": "^4.9.4",
"vite": "^4.1.2" "vite": "^4.1.2"
} }
+68
View File
@@ -0,0 +1,68 @@
import { getBucket } from "abTest/utils";
import type { NextMiddleware, NextRequest } from "next/server";
import { NextResponse, URLPattern } from "next/server";
import z from "zod";
const ROUTES: [URLPattern, boolean][] = [
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
].map(([pathname, enabled]) => [
new URLPattern({
pathname,
}),
enabled,
]);
const FUTURE_ROUTES_OVERRIDE_COOKIE_NAME = "x-calcom-future-routes-override";
const FUTURE_ROUTES_ENABLED_COOKIE_NAME = "x-calcom-future-routes-enabled";
const bucketSchema = z.union([z.literal("legacy"), z.literal("future")]);
export const abTestMiddlewareFactory =
(next: (req: NextRequest) => Promise<NextResponse<unknown>>): NextMiddleware =>
async (req: NextRequest) => {
const response = await next(req);
const { pathname } = req.nextUrl;
const override = req.cookies.has(FUTURE_ROUTES_OVERRIDE_COOKIE_NAME);
const route = ROUTES.find(([regExp]) => regExp.test(req.url)) ?? null;
const enabled = route !== null ? route[1] || override : false;
if (pathname.includes("future") || !enabled) {
return response;
}
const safeParsedBucket = override
? { success: true as const, data: "future" as const }
: bucketSchema.safeParse(req.cookies.get(FUTURE_ROUTES_ENABLED_COOKIE_NAME)?.value);
if (!safeParsedBucket.success) {
// cookie does not exist or it has incorrect value
const bucket = getBucket();
response.cookies.set(FUTURE_ROUTES_ENABLED_COOKIE_NAME, bucket, {
expires: Date.now() + 1000 * 60 * 30,
httpOnly: true,
}); // 30 min in ms
if (bucket === "legacy") {
return response;
}
const url = req.nextUrl.clone();
url.pathname = `future${pathname}/`;
return NextResponse.rewrite(url, response);
}
if (safeParsedBucket.data === "legacy") {
return response;
}
const url = req.nextUrl.clone();
url.pathname = `future${pathname}/`;
return NextResponse.rewrite(url, response);
};
+9
View File
@@ -0,0 +1,9 @@
import { AB_TEST_BUCKET_PROBABILITY } from "@calcom/lib/constants";
const cryptoRandom = () => {
return crypto.getRandomValues(new Uint8Array(1))[0] / 0xff;
};
export const getBucket = () => {
return cryptoRandom() * 100 < AB_TEST_BUCKET_PROBABILITY ? "future" : "legacy";
};
+8
View File
@@ -0,0 +1,8 @@
"use client";
import { createHydrateClient } from "app/_trpc/createHydrateClient";
import superjson from "superjson";
export const HydrateClient = createHydrateClient({
transformer: superjson,
});
+5
View File
@@ -0,0 +1,5 @@
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { createTRPCReact } from "@trpc/react-query";
export const trpc = createTRPCReact<AppRouter>({});
@@ -0,0 +1,21 @@
"use client";
import { type DehydratedState, Hydrate } from "@tanstack/react-query";
import { useMemo } from "react";
import type { DataTransformer } from "@trpc/server";
export function createHydrateClient(opts: { transformer?: DataTransformer }) {
return function HydrateClient(props: { children: React.ReactNode; state: DehydratedState }) {
const { state, children } = props;
const transformedState: DehydratedState = useMemo(() => {
if (opts.transformer) {
return opts.transformer.deserialize(state);
}
return state;
}, [state]);
return <Hydrate state={transformedState}>{children}</Hydrate>;
};
}
+128
View File
@@ -0,0 +1,128 @@
import { type DehydratedState, QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { HydrateClient } from "app/_trpc/HydrateClient";
import { trpc } from "app/_trpc/client";
import { useState } from "react";
import superjson from "superjson";
import { httpBatchLink } from "@calcom/trpc/client/links/httpBatchLink";
import { httpLink } from "@calcom/trpc/client/links/httpLink";
import { loggerLink } from "@calcom/trpc/client/links/loggerLink";
import { splitLink } from "@calcom/trpc/client/links/splitLink";
const ENDPOINTS = [
"admin",
"apiKeys",
"appRoutingForms",
"apps",
"auth",
"availability",
"appBasecamp3",
"bookings",
"deploymentSetup",
"eventTypes",
"features",
"insights",
"payments",
"public",
"saml",
"slots",
"teams",
"organizations",
"users",
"viewer",
"webhook",
"workflows",
"appsRouter",
"googleWorkspace",
] as const;
export type Endpoint = (typeof ENDPOINTS)[number];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resolveEndpoint = (links: any) => {
// TODO: Update our trpc routes so they are more clear.
// This function parses paths like the following and maps them
// to the correct API endpoints.
// - viewer.me - 2 segment paths like this are for logged in requests
// - viewer.public.i18n - 3 segments paths can be public or authed
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (ctx: any) => {
const parts = ctx.op.path.split(".");
let endpoint;
let path = "";
if (parts.length == 2) {
endpoint = parts[0] as keyof typeof links;
path = parts[1];
} else {
endpoint = parts[1] as keyof typeof links;
path = parts.splice(2, parts.length - 2).join(".");
}
return links[endpoint]({ ...ctx, op: { ...ctx.op, path } });
};
};
export const TrpcProvider: React.FC<{ children: React.ReactNode; dehydratedState?: DehydratedState }> = ({
children,
dehydratedState,
}) => {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: { queries: { staleTime: 5000 } },
})
);
const url =
typeof window !== "undefined"
? "/api/trpc"
: process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/api/trpc`
: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/trpc`;
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
// adds pretty logs to your console in development and logs errors in production
loggerLink({
enabled: (opts) =>
!!process.env.NEXT_PUBLIC_DEBUG || (opts.direction === "down" && opts.result instanceof Error),
}),
splitLink({
// check for context property `skipBatch`
condition: (op) => !!op.context.skipBatch,
// when condition is true, use normal request
true: (runtime) => {
const links = Object.fromEntries(
ENDPOINTS.map((endpoint) => [
endpoint,
httpLink({
url: `${url}/${endpoint}`,
})(runtime),
])
);
return resolveEndpoint(links);
},
// when condition is false, use batch request
false: (runtime) => {
const links = Object.fromEntries(
ENDPOINTS.map((endpoint) => [
endpoint,
httpBatchLink({
url: `${url}/${endpoint}`,
})(runtime),
])
);
return resolveEndpoint(links);
},
}),
],
transformer: superjson,
})
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{dehydratedState ? <HydrateClient state={dehydratedState}>{children}</HydrateClient> : children}
</QueryClientProvider>
</trpc.Provider>
);
};
+40
View File
@@ -0,0 +1,40 @@
import { type TFunction } from "i18next";
import { headers } from "next/headers";
import { constructGenericImage } from "@calcom/lib/OgImages";
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG } from "@calcom/lib/constants";
import { getFixedT } from "@calcom/lib/server/getFixedT";
import { preparePageMetadata } from "@lib/metadata";
export const _generateMetadata = async (
getTitle: (t: TFunction<string, undefined>) => string,
getDescription: (t: TFunction<string, undefined>) => string
) => {
const h = headers();
const canonical = h.get("x-pathname") ?? "";
const locale = h.get("x-locale") ?? "en";
const t = await getFixedT(locale, "common");
const title = getTitle(t);
const description = getDescription(t);
const metadataBase = new URL(IS_CALCOM ? "https://cal.com" : WEBAPP_URL);
const image =
SEO_IMG_OGIMG +
constructGenericImage({
title,
description,
});
return preparePageMetadata({
title,
canonical,
image,
description,
siteName: APP_NAME,
metadataBase,
});
};
+64
View File
@@ -0,0 +1,64 @@
"use client";
/**
* Typescript class based component for custom-error
* @link https://nextjs.org/docs/advanced-features/custom-error-page
*/
import type { NextPage } from "next";
import type { ErrorProps } from "next/error";
import React from "react";
import { HttpError } from "@calcom/lib/http-error";
import logger from "@calcom/lib/logger";
import { redactError } from "@calcom/lib/redactError";
import { ErrorPage } from "@components/error/error-page";
type NextError = Error & { digest?: string };
// Ref: https://nextjs.org/docs/app/api-reference/file-conventions/error#props
export type DefaultErrorProps = {
error: NextError;
reset: () => void; // A function to reset the error boundary
};
type AugmentedError = NextError | HttpError | null;
type CustomErrorProps = {
err?: AugmentedError;
statusCode?: number;
message?: string;
} & Omit<ErrorProps, "err" | "statusCode">;
const log = logger.getSubLogger({ prefix: ["[error]"] });
const CustomError: NextPage<DefaultErrorProps> = (props) => {
const { error } = props;
let errorObject: CustomErrorProps = {
message: error.message,
err: error,
};
if (error instanceof HttpError) {
const redactedError = redactError(error);
errorObject = {
statusCode: error.statusCode,
title: redactedError.name,
message: redactedError.message,
err: {
...redactedError,
...error,
},
};
}
// `error.digest` property contains an automatically generated hash of the error that can be used to match the corresponding error in server-side logs
log.debug(`${error?.toString() ?? JSON.stringify(error)}`);
log.info("errorObject: ", errorObject);
return (
<ErrorPage statusCode={errorObject.statusCode} error={errorObject.err} message={errorObject.message} />
);
};
export default CustomError;
@@ -0,0 +1,10 @@
import EventTypes from "@pages/event-types";
import { _generateMetadata } from "app/_utils";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("event_types_page_title"),
(t) => t("event_types_page_subtitle")
);
export default EventTypes;
@@ -0,0 +1,22 @@
// pages without layout (e.g., /availability/index.tsx) are supposed to go under (layout) folder
import { headers } from "next/headers";
import { type ReactElement } from "react";
import { getLayout } from "@calcom/features/MainLayoutAppDir";
import PageWrapper from "@components/PageWrapperAppDir";
type WrapperWithLayoutProps = {
children: ReactElement;
};
export default async function WrapperWithLayout({ children }: WrapperWithLayoutProps) {
const h = headers();
const nonce = h.get("x-nonce") ?? undefined;
return (
<PageWrapper getLayout={getLayout} requiresLicense={false} nonce={nonce} themeBasis={null}>
{children}
</PageWrapper>
);
}
+17
View File
@@ -0,0 +1,17 @@
"use client";
import { type NextPage } from "next";
import CustomError, { type DefaultErrorProps } from "./error";
export const GlobalError: NextPage<DefaultErrorProps> = (props) => {
return (
<html>
<body>
<CustomError {...props} />
</body>
</html>
);
};
export default GlobalError;
+34 -55
View File
@@ -1,84 +1,63 @@
import type { Metadata } from "next"; import { dir } from "i18next";
import { headers as nextHeaders, cookies as nextCookies } from "next/headers"; import { headers, cookies } from "next/headers";
import Script from "next/script"; import Script from "next/script";
import React from "react"; import React from "react";
import { getLocale } from "@calcom/features/auth/lib/getLocale"; import { getLocale } from "@calcom/features/auth/lib/getLocale";
import { IS_PRODUCTION } from "@calcom/lib/constants"; import { IS_PRODUCTION } from "@calcom/lib/constants";
import { prepareRootMetadata } from "@lib/metadata";
import "../styles/globals.css"; import "../styles/globals.css";
export const metadata: Metadata = { export const generateMetadata = () =>
icons: { prepareRootMetadata({
icon: [ twitterCreator: "@calcom",
{ twitterSite: "@calcom",
sizes: "32x32", robots: {
url: "/api/logo?type=favicon-32", index: false,
}, follow: false,
{
sizes: "16x16",
url: "/api/logo?type=favicon-16",
},
],
apple: {
sizes: "180x180",
url: "/api/logo?type=apple-touch-icon",
}, },
other: [ });
{
url: "/safari-pinned-tab.svg",
rel: "mask-icon",
},
],
},
manifest: "/site.webmanifest",
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#f9fafb" },
{ media: "(prefers-color-scheme: dark)", color: "#1C1C1C" },
],
other: {
"msapplication-TileColor": "#000000",
},
};
const getInitialProps = async ( const getInitialProps = async (url: string) => {
url: string,
headers: ReturnType<typeof nextHeaders>,
cookies: ReturnType<typeof nextCookies>
) => {
const { pathname, searchParams } = new URL(url); const { pathname, searchParams } = new URL(url);
const isEmbed = pathname.endsWith("/embed") || (searchParams?.get("embedType") ?? null) !== null; const isEmbed = pathname.endsWith("/embed") || (searchParams?.get("embedType") ?? null) !== null;
const embedColorScheme = searchParams?.get("ui.color-scheme"); const embedColorScheme = searchParams?.get("ui.color-scheme");
// @ts-expect-error we cannot access ctx.req in app dir, however headers and cookies are only properties needed to extract the locale const req = { headers: headers(), cookies: cookies() };
const newLocale = await getLocale({ headers, cookies }); const newLocale = await getLocale(req);
let direction = "ltr"; const direction = dir(newLocale);
try {
const intlLocale = new Intl.Locale(newLocale);
// @ts-expect-error INFO: Typescript does not know about the Intl.Locale textInfo attribute
direction = intlLocale.textInfo?.direction;
} catch (e) {
console.error(e);
}
return { isEmbed, embedColorScheme, locale: newLocale, direction }; return { isEmbed, embedColorScheme, locale: newLocale, direction };
}; };
const getFallbackProps = () => ({
locale: "en",
direction: "ltr",
isEmbed: false,
embedColorScheme: false,
});
export default async function RootLayout({ children }: { children: React.ReactNode }) { export default async function RootLayout({ children }: { children: React.ReactNode }) {
const headers = nextHeaders(); const h = headers();
const cookies = nextCookies();
const fullUrl = headers.get("x-url") ?? ""; const fullUrl = h.get("x-url") ?? "";
const nonce = headers.get("x-csp") ?? ""; const nonce = h.get("x-csp") ?? "";
const isSSG = !fullUrl;
const { locale, direction, isEmbed, embedColorScheme } = isSSG
? getFallbackProps()
: await getInitialProps(fullUrl);
const { locale, direction, isEmbed, embedColorScheme } = await getInitialProps(fullUrl, headers, cookies);
return ( return (
<html <html
lang={locale} lang={locale}
dir={direction} dir={direction}
style={embedColorScheme ? { colorScheme: embedColorScheme as string } : undefined}> style={embedColorScheme ? { colorScheme: embedColorScheme as string } : undefined}
data-nextjs-router="app">
<head nonce={nonce}> <head nonce={nonce}>
{!IS_PRODUCTION && process.env.VERCEL_ENV === "preview" && ( {!IS_PRODUCTION && process.env.VERCEL_ENV === "preview" && (
// eslint-disable-next-line @next/next/no-sync-scripts // eslint-disable-next-line @next/next/no-sync-scripts
+3 -2
View File
@@ -1,4 +1,4 @@
import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { z } from "zod"; import { z } from "zod";
@@ -6,6 +6,7 @@ import { z } from "zod";
import type { CredentialOwner } from "@calcom/app-store/types"; import type { CredentialOwner } from "@calcom/app-store/types";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage"; import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { Badge, ListItemText, Avatar } from "@calcom/ui"; import { Badge, ListItemText, Avatar } from "@calcom/ui";
@@ -56,7 +57,7 @@ export default function AppListCard(props: AppListCardProps) {
const router = useRouter(); const router = useRouter();
const [highlight, setHighlight] = useState(shouldHighlight && hl === slug); const [highlight, setHighlight] = useState(shouldHighlight && hl === slug);
const timeoutRef = useRef<NodeJS.Timeout | null>(null); const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
const pathname = usePathname(); const pathname = usePathname();
useEffect(() => { useEffect(() => {
+6 -5
View File
@@ -1,5 +1,6 @@
"use client"; "use client";
import { type DehydratedState } from "@tanstack/react-query";
import type { SSRConfig } from "next-i18next"; import type { SSRConfig } from "next-i18next";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import localFont from "next/font/local"; import localFont from "next/font/local";
@@ -10,7 +11,6 @@ import type { ReactNode } from "react";
import "@calcom/embed-core/src/embed-iframe"; import "@calcom/embed-core/src/embed-iframe";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired"; import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import { trpc } from "@calcom/trpc/react";
import type { AppProps } from "@lib/app-providers-app-dir"; import type { AppProps } from "@lib/app-providers-app-dir";
import AppProviders from "@lib/app-providers-app-dir"; import AppProviders from "@lib/app-providers-app-dir";
@@ -29,13 +29,14 @@ const calFont = localFont({
}); });
export type PageWrapperProps = Readonly<{ export type PageWrapperProps = Readonly<{
getLayout: (page: React.ReactElement) => ReactNode; getLayout: ((page: React.ReactElement) => ReactNode) | null;
children: React.ReactElement; children: React.ReactElement;
requiresLicense: boolean; requiresLicense: boolean;
isThemeSupported: boolean;
isBookingPage: boolean;
nonce: string | undefined; nonce: string | undefined;
themeBasis: string | null; themeBasis: string | null;
dehydratedState?: DehydratedState;
isThemeSupported?: boolean;
isBookingPage?: boolean;
i18n?: SSRConfig; i18n?: SSRConfig;
}>; }>;
@@ -85,4 +86,4 @@ function PageWrapper(props: PageWrapperProps) {
); );
} }
export default trpc.withTRPC(PageWrapper); export default PageWrapper;
+39 -18
View File
@@ -42,6 +42,7 @@ export type AppPageProps = {
disableInstall?: boolean; disableInstall?: boolean;
dependencies?: string[]; dependencies?: string[];
concurrentMeetings: AppType["concurrentMeetings"]; concurrentMeetings: AppType["concurrentMeetings"];
paid?: AppType["paid"];
}; };
export const AppPage = ({ export const AppPage = ({
@@ -67,6 +68,7 @@ export const AppPage = ({
isTemplate, isTemplate,
dependencies, dependencies,
concurrentMeetings, concurrentMeetings,
paid,
}: AppPageProps) => { }: AppPageProps) => {
const { t, i18n } = useLocale(); const { t, i18n } = useLocale();
const hasDescriptionItems = descriptionItems && descriptionItems.length > 0; const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
@@ -163,6 +165,19 @@ export const AppPage = ({
className="bg-subtle text-emphasis rounded-md p-1 text-xs capitalize"> className="bg-subtle text-emphasis rounded-md p-1 text-xs capitalize">
{categories[0]} {categories[0]}
</Link>{" "} </Link>{" "}
{paid && (
<>
<Badge className="mr-1">
{Intl.NumberFormat(i18n.language, {
style: "currency",
currency: "USD",
useGrouping: false,
maximumFractionDigits: 0,
}).format(paid.priceInUsd)}
/{t("month")}
</Badge>
</>
)}
•{" "} •{" "}
<a target="_blank" rel="noreferrer" href={website}> <a target="_blank" rel="noreferrer" href={website}>
{t("published_by", { author })} {t("published_by", { author })}
@@ -206,6 +221,7 @@ export const AppPage = ({
addAppMutationInput={{ type, variant, slug }} addAppMutationInput={{ type, variant, slug }}
multiInstall multiInstall
concurrentMeetings={concurrentMeetings} concurrentMeetings={concurrentMeetings}
paid={paid}
{...props} {...props}
/> />
); );
@@ -244,6 +260,7 @@ export const AppPage = ({
addAppMutationInput={{ type, variant, slug }} addAppMutationInput={{ type, variant, slug }}
credentials={appDbQuery.data?.credentials} credentials={appDbQuery.data?.credentials}
concurrentMeetings={concurrentMeetings} concurrentMeetings={concurrentMeetings}
paid={paid}
{...props} {...props}
/> />
); );
@@ -263,7 +280,7 @@ export const AppPage = ({
<SkeletonButton className="mt-6 h-20 grow" /> <SkeletonButton className="mt-6 h-20 grow" />
))} ))}
{price !== 0 && ( {price !== 0 && !paid && (
<span className="block text-right"> <span className="block text-right">
{feeType === "usage-based" ? `${commission}% + ${priceInDollar}/booking` : priceInDollar} {feeType === "usage-based" ? `${commission}% + ${priceInDollar}/booking` : priceInDollar}
{feeType === "monthly" && `/${t("month")}`} {feeType === "monthly" && `/${t("month")}`}
@@ -273,23 +290,27 @@ export const AppPage = ({
<div className="prose-sm prose prose-a:text-default prose-headings:text-emphasis prose-code:text-default prose-strong:text-default text-default mt-8"> <div className="prose-sm prose prose-a:text-default prose-headings:text-emphasis prose-code:text-default prose-strong:text-default text-default mt-8">
{body} {body}
</div> </div>
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4> {!paid && (
<span className="text-default"> <>
{teamsPlanRequired ? ( <h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
t("teams_plan_required") <span className="text-default">
) : price === 0 ? ( {teamsPlanRequired ? (
t("free_to_use_apps") t("teams_plan_required")
) : ( ) : price === 0 ? (
<> t("free_to_use_apps")
{Intl.NumberFormat(i18n.language, { ) : (
style: "currency", <>
currency: "USD", {Intl.NumberFormat(i18n.language, {
useGrouping: false, style: "currency",
}).format(price)} currency: "USD",
{feeType === "monthly" && `/${t("month")}`} useGrouping: false,
</> }).format(price)}
)} {feeType === "monthly" && `/${t("month")}`}
</span> </>
)}
</span>
</>
)}
<h4 className="text-emphasis mb-2 mt-8 font-semibold ">{t("contact")}</h4> <h4 className="text-emphasis mb-2 mt-8 font-semibold ">{t("contact")}</h4>
<ul className="prose-sm -ml-1 -mr-1 leading-5"> <ul className="prose-sm -ml-1 -mr-1 leading-5">
@@ -26,6 +26,7 @@ export const InstallAppButtonChild = ({
multiInstall, multiInstall,
credentials, credentials,
concurrentMeetings, concurrentMeetings,
paid,
...props ...props
}: { }: {
userAdminTeams?: UserAdminTeams; userAdminTeams?: UserAdminTeams;
@@ -34,6 +35,7 @@ export const InstallAppButtonChild = ({
multiInstall?: boolean; multiInstall?: boolean;
credentials?: RouterOutputs["viewer"]["appCredentialsByType"]["credentials"]; credentials?: RouterOutputs["viewer"]["appCredentialsByType"]["credentials"];
concurrentMeetings?: boolean; concurrentMeetings?: boolean;
paid?: AppFrontendPayload["paid"];
} & ButtonProps) => { } & ButtonProps) => {
const { t } = useLocale(); const { t } = useLocale();
@@ -46,8 +48,27 @@ export const InstallAppButtonChild = ({
if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error"); if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error");
}, },
}); });
const shouldDisableInstallation = !multiInstall ? !!(credentials && credentials.length) : false;
if (!userAdminTeams?.length || !doesAppSupportTeamInstall(appCategories, concurrentMeetings)) { // Paid apps don't support team installs at the moment
// Also, cal.ai(the only paid app at the moment) doesn't support team install either
if (paid) {
return (
<Button
data-testid="install-app-button"
{...props}
disabled={shouldDisableInstallation}
color="primary"
size="base">
{paid.trial ? t("start_paid_trial") : t("install_paid_app")}
</Button>
);
}
if (
!userAdminTeams?.length ||
!doesAppSupportTeamInstall({ appCategories, concurrentMeetings, isPaid: !!paid })
) {
return ( return (
<Button <Button
data-testid="install-app-button" data-testid="install-app-button"
@@ -55,6 +76,7 @@ export const InstallAppButtonChild = ({
// @TODO: Overriding color and size prevent us from // @TODO: Overriding color and size prevent us from
// having to duplicate InstallAppButton for now. // having to duplicate InstallAppButton for now.
color="primary" color="primary"
disabled={shouldDisableInstallation}
size="base"> size="base">
{multiInstall ? t("install_another") : t("install_app")} {multiInstall ? t("install_another") : t("install_app")}
</Button> </Button>
@@ -47,7 +47,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
}; };
}; };
const getAppDataSetter = (appId: EventTypeAppsList): SetAppData => { const getAppDataSetter = (appId: EventTypeAppsList, credentialId?: number): SetAppData => {
return function (key, value) { return function (key, value) {
// Always get latest data available in Form because consequent calls to setData would update the Form but not allAppsData(it would update during next render) // Always get latest data available in Form because consequent calls to setData would update the Form but not allAppsData(it would update during next render)
const allAppsDataFromForm = methods.getValues("metadata")?.apps || {}; const allAppsDataFromForm = methods.getValues("metadata")?.apps || {};
@@ -57,6 +57,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
[appId]: { [appId]: {
...appData, ...appData,
[key]: value, [key]: value,
credentialId,
}, },
}); });
}; };
@@ -76,7 +77,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
appCards.push( appCards.push(
<EventTypeAppCard <EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)} getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)} setAppData={getAppDataSetter(app.slug as EventTypeAppsList, app.userCredentialIds[0])}
key={app.slug} key={app.slug}
app={app} app={app}
eventType={eventType} eventType={eventType}
@@ -90,7 +91,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
appCards.push( appCards.push(
<EventTypeAppCard <EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)} getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)} setAppData={getAppDataSetter(app.slug as EventTypeAppsList, team.credentialId)}
key={app.slug + team?.credentialId} key={app.slug + team?.credentialId}
app={{ app={{
...app, ...app,
@@ -147,7 +148,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
return ( return (
<EventTypeAppCard <EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)} getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)} setAppData={getAppDataSetter(app.slug as EventTypeAppsList, app.userCredentialIds[0])}
key={app.slug} key={app.slug}
app={app} app={app}
eventType={eventType} eventType={eventType}
@@ -76,6 +76,8 @@ const ChildrenEventTypesList = ({
<div> <div>
<Label>{t("assign_to")}</Label> <Label>{t("assign_to")}</Label>
<ChildrenEventTypeSelect <ChildrenEventTypeSelect
aria-label="assignment-dropdown"
data-testid="assignment-dropdown"
onChange={(options) => { onChange={(options) => {
onChange && onChange &&
onChange( onChange(
+2 -2
View File
@@ -14,6 +14,6 @@ type UserAvatarProps = Omit<React.ComponentProps<typeof Avatar>, "alt" | "imageS
* It is aware of the user's organization to correctly show the avatar from the correct URL * It is aware of the user's organization to correctly show the avatar from the correct URL
*/ */
export function UserAvatar(props: UserAvatarProps) { export function UserAvatar(props: UserAvatarProps) {
const { user, previewSrc, ...rest } = props; const { user, previewSrc = getUserAvatarUrl(user), ...rest } = props;
return <Avatar {...rest} alt={user.name || ""} imageSrc={previewSrc ?? getUserAvatarUrl(user)} />; return <Avatar {...rest} alt={user.name || "Nameless User"} imageSrc={previewSrc} />;
} }
+23 -20
View File
@@ -1,4 +1,5 @@
import { TooltipProvider } from "@radix-ui/react-tooltip"; import { TooltipProvider } from "@radix-ui/react-tooltip";
import { TrpcProvider } from "app/_trpc/trpc-provider";
import { dir } from "i18next"; import { dir } from "i18next";
import type { Session } from "next-auth"; import type { Session } from "next-auth";
import { SessionProvider, useSession } from "next-auth/react"; import { SessionProvider, useSession } from "next-auth/react";
@@ -255,26 +256,28 @@ const AppProviders = (props: PageWrapperProps) => {
const isBookingPage = useIsBookingPage(); const isBookingPage = useIsBookingPage();
const RemainingProviders = ( const RemainingProviders = (
<EventCollectionProvider options={{ apiPath: "/api/collect-events" }}> <TrpcProvider dehydratedState={props.dehydratedState}>
<SessionProvider> <EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
<CustomI18nextProvider i18n={props.i18n}> <SessionProvider>
<TooltipProvider> <CustomI18nextProvider i18n={props.i18n}>
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */} <TooltipProvider>
<CalcomThemeProvider {/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
themeBasis={props.themeBasis} <CalcomThemeProvider
nonce={props.nonce} themeBasis={props.themeBasis}
isThemeSupported={props.isThemeSupported} nonce={props.nonce}
isBookingPage={props.isBookingPage || isBookingPage}> isThemeSupported={/* undefined gets treated as true */ props.isThemeSupported ?? true}
<FeatureFlagsProvider> isBookingPage={props.isBookingPage || isBookingPage}>
<OrgBrandProvider> <FeatureFlagsProvider>
<MetaProvider>{props.children}</MetaProvider> <OrgBrandProvider>
</OrgBrandProvider> <MetaProvider>{props.children}</MetaProvider>
</FeatureFlagsProvider> </OrgBrandProvider>
</CalcomThemeProvider> </FeatureFlagsProvider>
</TooltipProvider> </CalcomThemeProvider>
</CustomI18nextProvider> </TooltipProvider>
</SessionProvider> </CustomI18nextProvider>
</EventCollectionProvider> </SessionProvider>
</EventCollectionProvider>
</TrpcProvider>
); );
if (isBookingPage) { if (isBookingPage) {
+4 -2
View File
@@ -1,10 +1,12 @@
import { usePathname, useSearchParams } from "next/navigation"; import { usePathname } from "next/navigation";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
export default function useIsBookingPage(): boolean { export default function useIsBookingPage(): boolean {
const pathname = usePathname(); const pathname = usePathname();
const isBookingPage = ["/booking/", "/cancel", "/reschedule"].some((route) => pathname?.startsWith(route)); const isBookingPage = ["/booking/", "/cancel", "/reschedule"].some((route) => pathname?.startsWith(route));
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
const userParam = Boolean(searchParams?.get("user")); const userParam = Boolean(searchParams?.get("user"));
const teamParam = Boolean(searchParams?.get("team")); const teamParam = Boolean(searchParams?.get("team"));
+4 -2
View File
@@ -1,8 +1,10 @@
import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { useCallback } from "react"; import { useCallback } from "react";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
export default function useRouterQuery<T extends string>(name: T) { export default function useRouterQuery<T extends string>(name: T) {
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
const pathname = usePathname(); const pathname = usePathname();
const router = useRouter(); const router = useRouter();
+2 -2
View File
@@ -1,7 +1,7 @@
import { useSearchParams } from "next/navigation"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
export function useToggleQuery(name: string) { export function useToggleQuery(name: string) {
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
return { return {
isOn: searchParams?.get(name) === "1", isOn: searchParams?.get(name) === "1",
+82
View File
@@ -0,0 +1,82 @@
import type { Metadata } from "next";
import { truncateOnWord } from "@calcom/lib/text";
type RootMetadataRecipe = Readonly<{
twitterCreator: string;
twitterSite: string;
robots: {
index: boolean;
follow: boolean;
};
}>;
export type PageMetadataRecipe = Readonly<{
title: string;
canonical: string;
image: string;
description: string;
siteName: string;
metadataBase: URL;
}>;
export const prepareRootMetadata = (recipe: RootMetadataRecipe): Metadata => ({
icons: {
icon: "/favicon.icon",
apple: "/api/logo?type=apple-touch-icon",
other: [
{
rel: "icon-mask",
url: "/safari-pinned-tab.svg",
color: "#000000",
},
{
url: "/api/logo?type=favicon-16",
sizes: "16x16",
type: "image/png",
},
{
url: "/api/logo?type=favicon-32",
sizes: "32x32",
type: "image/png",
},
],
},
manifest: "/site.webmanifest",
viewport: "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0",
robots: recipe.robots,
other: {
"application-TileColor": "#ff0000",
},
themeColor: [
{
media: "(prefers-color-scheme: light)",
color: "#f9fafb",
},
{
media: "(prefers-color-scheme: dark)",
color: "#1C1C1C",
},
],
twitter: {
site: recipe.twitterSite,
creator: recipe.twitterCreator,
card: "summary_large_image",
},
});
export const preparePageMetadata = (recipe: PageMetadataRecipe): Metadata => ({
title: recipe.title,
alternates: {
canonical: recipe.canonical,
},
openGraph: {
description: truncateOnWord(recipe.description, 158),
url: recipe.canonical,
type: "website",
siteName: recipe.siteName,
title: recipe.title,
images: [recipe.image],
},
metadataBase: recipe.metadataBase,
});
+14 -3
View File
@@ -1,13 +1,16 @@
import { get } from "@vercel/edge-config"; import { get } from "@vercel/edge-config";
import { collectEvents } from "next-collect/server"; import { collectEvents } from "next-collect/server";
import type { NextMiddleware } from "next/server"; import type { NextRequest } from "next/server";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { getLocale } from "@calcom/features/auth/lib/getLocale";
import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry"; import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry";
import { csp } from "@lib/csp"; import { csp } from "@lib/csp";
const middleware: NextMiddleware = async (req) => { import { abTestMiddlewareFactory } from "./abTest/middlewareFactory";
const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
const url = req.nextUrl; const url = req.nextUrl;
const requestHeaders = new Headers(req.headers); const requestHeaders = new Headers(req.headers);
@@ -61,6 +64,12 @@ const middleware: NextMiddleware = async (req) => {
requestHeaders.set("x-csp-enforce", "true"); requestHeaders.set("x-csp-enforce", "true");
} }
requestHeaders.set("x-pathname", url.pathname);
const locale = await getLocale(req);
requestHeaders.set("x-locale", locale);
return NextResponse.next({ return NextResponse.next({
request: { request: {
headers: requestHeaders, headers: requestHeaders,
@@ -90,11 +99,13 @@ export const config = {
* Paths required by routingForms.handle * Paths required by routingForms.handle
*/ */
"/apps/routing_forms/:path*", "/apps/routing_forms/:path*",
"/event-types",
"/future/event-types/",
], ],
}; };
export default collectEvents({ export default collectEvents({
middleware, middleware: abTestMiddlewareFactory(middleware),
...nextCollectBasicSettings, ...nextCollectBasicSettings,
cookieName: "__clnds", cookieName: "__clnds",
extend: extendEventData, extend: extendEventData,
+4 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@calcom/web", "name": "@calcom/web",
"version": "3.4.9", "version": "3.5.2",
"private": true, "private": true,
"scripts": { "scripts": {
"analyze": "ANALYZE=true next build", "analyze": "ANALYZE=true next build",
@@ -45,7 +45,7 @@
"@hookform/resolvers": "^2.9.7", "@hookform/resolvers": "^2.9.7",
"@next-auth/prisma-adapter": "^1.0.4", "@next-auth/prisma-adapter": "^1.0.4",
"@next/bundle-analyzer": "^13.1.6", "@next/bundle-analyzer": "^13.1.6",
"@radix-ui/react-avatar": "^1.0.0", "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.0", "@radix-ui/react-collapsible": "^1.0.0",
"@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-dropdown-menu": "^2.0.5",
@@ -168,8 +168,9 @@
"module-alias": "^2.2.2", "module-alias": "^2.2.2",
"msw": "^0.42.3", "msw": "^0.42.3",
"node-html-parser": "^6.1.10", "node-html-parser": "^6.1.10",
"node-mocks-http": "^1.11.0",
"postcss": "^8.4.18", "postcss": "^8.4.18",
"tailwindcss": "^3.3.1", "tailwindcss": "^3.3.3",
"tailwindcss-animate": "^1.0.6", "tailwindcss-animate": "^1.0.6",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^4.9.4" "typescript": "^4.9.4"
+2 -2
View File
@@ -1,7 +1,7 @@
import Head from "next/head"; import Head from "next/head";
import { useSearchParams } from "next/navigation";
import { APP_NAME } from "@calcom/lib/constants"; import { APP_NAME } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, showToast } from "@calcom/ui"; import { Button, showToast } from "@calcom/ui";
import { Copy } from "@calcom/ui/components/icon"; import { Copy } from "@calcom/ui/components/icon";
@@ -9,7 +9,7 @@ import { Copy } from "@calcom/ui/components/icon";
import PageWrapper from "@components/PageWrapper"; import PageWrapper from "@components/PageWrapper";
export default function Error500() { export default function Error500() {
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
const { t } = useLocale(); const { t } = useLocale();
return ( return (
+5 -2
View File
@@ -82,7 +82,7 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
description={markdownStrippedBio} description={markdownStrippedBio}
meeting={{ meeting={{
title: markdownStrippedBio, title: markdownStrippedBio,
profile: { name: `${profile.name}`, image: null }, profile: { name: `${profile.name}`, image: user.avatarUrl || null },
users: [{ username: `${user.username}`, name: `${user.name}` }], users: [{ username: `${user.username}`, name: `${user.name}` }],
}} }}
nextSeoProps={{ nextSeoProps={{
@@ -245,7 +245,7 @@ export type UserPageProps = {
allowSEOIndexing: boolean; allowSEOIndexing: boolean;
username: string | null; username: string | null;
}; };
users: Pick<User, "away" | "name" | "username" | "bio" | "verified">[]; users: Pick<User, "away" | "name" | "username" | "bio" | "verified" | "avatarUrl">[];
themeBasis: string | null; themeBasis: string | null;
markdownStrippedBio: string; markdownStrippedBio: string;
safeBio: string; safeBio: string;
@@ -295,6 +295,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
metadata: true, metadata: true,
brandColor: true, brandColor: true,
darkBrandColor: true, darkBrandColor: true,
avatarUrl: true,
organizationId: true, organizationId: true,
organization: { organization: {
select: { select: {
@@ -363,6 +364,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
image: user.avatar, image: user.avatar,
theme: user.theme, theme: user.theme,
brandColor: user.brandColor, brandColor: user.brandColor,
avatarUrl: user.avatarUrl,
darkBrandColor: user.darkBrandColor, darkBrandColor: user.darkBrandColor,
allowSEOIndexing: user.allowSEOIndexing ?? true, allowSEOIndexing: user.allowSEOIndexing ?? true,
username: user.username, username: user.username,
@@ -397,6 +399,7 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
name: user.name, name: user.name,
username: user.username, username: user.username,
bio: user.bio, bio: user.bio,
avatarUrl: user.avatarUrl,
away: user.away, away: user.away,
verified: user.verified, verified: user.verified,
})), })),
+61
View File
@@ -0,0 +1,61 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { AVATAR_FALLBACK } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
const querySchema = z.object({
uuid: z.string().transform((objectKey) => objectKey.split(".")[0]),
});
const handleValidationError = (res: NextApiResponse, error: z.ZodError): void => {
const errors = error.errors.map((err) => ({
path: err.path.join("."),
errorCode: `error.validation.${err.code}`,
}));
res.status(400).json({
message: "VALIDATION_ERROR",
errors,
});
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const result = querySchema.safeParse(req.query);
if (!result.success) {
return handleValidationError(res, result.error);
}
const { uuid: objectKey } = result.data;
let img;
try {
const { data } = await prisma.avatar.findUniqueOrThrow({
where: {
objectKey,
},
select: {
data: true,
},
});
img = data;
} catch (e) {
// If anything goes wrong or avatar is not found, use default avatar
res.writeHead(302, {
Location: AVATAR_FALLBACK,
});
res.end();
return;
}
const decoded = img.toString().replace("data:image/png;base64,", "").replace("data:image/jpeg;base64,", "");
const imageResp = Buffer.from(decoded, "base64");
res.writeHead(200, {
"Content-Type": "image/png",
"Content-Length": imageResp.length,
});
res.end(imageResp);
}
+2 -1
View File
@@ -73,7 +73,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
redirectUrl = handler.redirect?.url || getInstalledAppPath(handler); redirectUrl = handler.redirect?.url || getInstalledAppPath(handler);
res.json({ url: redirectUrl, newTab: handler.redirect?.newTab }); res.json({ url: redirectUrl, newTab: handler.redirect?.newTab });
} }
return res.status(200); if (!res.writableEnded) return res.status(200);
return res;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
if (error instanceof HttpError) { if (error instanceof HttpError) {
@@ -0,0 +1,116 @@
import { buffer } from "micro";
import type { NextApiRequest, NextApiResponse } from "next";
import type Stripe from "stripe";
import stripe from "@calcom/app-store/stripepayment/lib/server";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { HttpError as HttpCode } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
export const config = {
api: {
bodyParser: false,
},
};
// This file is a catch-all for any integration related subscription/paid app.
const handleSubscriptionUpdate = async (event: Stripe.Event) => {
const subscription = event.data.object as Stripe.Subscription;
if (!subscription.id) throw new HttpCode({ statusCode: 400, message: "Subscription ID not found" });
const app = await prisma.credential.findFirst({
where: {
subscriptionId: subscription.id,
},
});
if (!app) {
throw new HttpCode({ statusCode: 202, message: "Received and discarded" });
}
await prisma.credential.update({
where: {
id: app.id,
},
data: {
paymentStatus: subscription.status,
},
});
};
const handleSubscriptionDeleted = async (event: Stripe.Event) => {
const subscription = event.data.object as Stripe.Subscription;
if (!subscription.id) throw new HttpCode({ statusCode: 400, message: "Subscription ID not found" });
const app = await prisma.credential.findFirst({
where: {
subscriptionId: subscription.id,
},
});
if (!app) {
throw new HttpCode({ statusCode: 202, message: "Received and discarded" });
}
// should we delete the credential here rather than marking as inactive?
await prisma.credential.update({
where: {
id: app.id,
},
data: {
paymentStatus: "inactive",
billingCycleStart: null,
},
});
};
type WebhookHandler = (event: Stripe.Event) => Promise<void>;
const webhookHandlers: Record<string, WebhookHandler | undefined> = {
"customer.subscription.updated": handleSubscriptionUpdate,
"customer.subscription.deleted": handleSubscriptionDeleted,
};
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
if (req.method !== "POST") {
throw new HttpCode({ statusCode: 405, message: "Method Not Allowed" });
}
const sig = req.headers["stripe-signature"];
if (!sig) {
throw new HttpCode({ statusCode: 400, message: "Missing stripe-signature" });
}
if (!process.env.STRIPE_WEBHOOK_SECRET) {
throw new HttpCode({ statusCode: 500, message: "Missing process.env.STRIPE_WEBHOOK_SECRET" });
}
const requestBuffer = await buffer(req);
const payload = requestBuffer.toString();
const event = stripe.webhooks.constructEvent(payload, sig, process.env.STRIPE_WEBHOOK_SECRET);
const handler = webhookHandlers[event.type];
if (handler) {
await handler(event);
} else {
/** Not really an error, just letting Stripe know that the webhook was received but unhandled */
throw new HttpCode({
statusCode: 202,
message: `Unhandled Stripe Webhook event type ${event.type}`,
});
}
} catch (_err) {
const err = getErrorFromUnknown(_err);
console.error(`Webhook Error: ${err.message}`);
res.status(err.statusCode ?? 500).send({
message: err.message,
stack: IS_PRODUCTION ? undefined : err.stack,
});
return;
}
// Return a response to acknowledge receipt of the event
res.json({ received: true });
}
+1
View File
@@ -79,6 +79,7 @@ function SingleAppPage(props: inferSSRProps<typeof getStaticProps>) {
isTemplate={data.isTemplate} isTemplate={data.isTemplate}
dependencies={data.dependencies} dependencies={data.dependencies}
concurrentMeetings={data.concurrentMeetings} concurrentMeetings={data.concurrentMeetings}
paid={data.paid}
// tos="https://zoom.us/terms" // tos="https://zoom.us/terms"
// privacy="https://zoom.us/privacy" // privacy="https://zoom.us/privacy"
body={ body={
+3 -2
View File
@@ -1,15 +1,16 @@
import type { InferGetServerSidePropsType } from "next"; import type { InferGetServerSidePropsType } from "next";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter } from "next/navigation";
import { AppSetupPage } from "@calcom/app-store/_pages/setup"; import { AppSetupPage } from "@calcom/app-store/_pages/setup";
import { getServerSideProps } from "@calcom/app-store/_pages/setup/_getServerSideProps"; import { getServerSideProps } from "@calcom/app-store/_pages/setup/_getServerSideProps";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { HeadSeo } from "@calcom/ui"; import { HeadSeo } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper"; import PageWrapper from "@components/PageWrapper";
export default function SetupInformation(props: InferGetServerSidePropsType<typeof getServerSideProps>) { export default function SetupInformation(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
const router = useRouter(); const router = useRouter();
const slug = searchParams?.get("slug") as string; const slug = searchParams?.get("slug") as string;
const { status } = useSession(); const { status } = useSession();
@@ -1,10 +1,10 @@
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import type { GetStaticPropsContext, InferGetStaticPropsType } from "next"; import type { GetStaticPropsContext, InferGetStaticPropsType } from "next";
import Link from "next/link"; import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { getAppRegistry } from "@calcom/app-store/_appRegistry"; import { getAppRegistry } from "@calcom/app-store/_appRegistry";
import Shell from "@calcom/features/shell/Shell"; import Shell from "@calcom/features/shell/Shell";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { AppCategories } from "@calcom/prisma/enums"; import { AppCategories } from "@calcom/prisma/enums";
@@ -13,7 +13,7 @@ import { AppCard, SkeletonText } from "@calcom/ui";
import PageWrapper from "@components/PageWrapper"; import PageWrapper from "@components/PageWrapper";
export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticProps>) { export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticProps>) {
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
const { t, isLocaleReady } = useLocale(); const { t, isLocaleReady } = useLocale();
const category = searchParams?.get("category"); const category = searchParams?.get("category");
+2 -2
View File
@@ -1,8 +1,8 @@
import { useSearchParams } from "next/navigation";
import { useReducer } from "react"; import { useReducer } from "react";
import { z } from "zod"; import { z } from "zod";
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal"; import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { AppCategories } from "@calcom/prisma/enums"; import { AppCategories } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
@@ -122,7 +122,7 @@ type ModalState = {
}; };
export default function InstalledApps() { export default function InstalledApps() {
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
const { t } = useLocale(); const { t } = useLocale();
const category = searchParams?.get("category") as querySchemaType["category"]; const category = searchParams?.get("category") as querySchemaType["category"];
const categoryList: AppCategories[] = Object.values(AppCategories).filter((category) => { const categoryList: AppCategories[] = Object.values(AppCategories).filter((category) => {
+2 -2
View File
@@ -1,8 +1,8 @@
import type { GetStaticPropsContext } from "next"; import type { GetStaticPropsContext } from "next";
import Link from "next/link"; import Link from "next/link";
import { useSearchParams } from "next/navigation";
import z from "zod"; import z from "zod";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui"; import { Button } from "@calcom/ui";
import { X } from "@calcom/ui/components/icon"; import { X } from "@calcom/ui/components/icon";
@@ -18,7 +18,7 @@ const querySchema = z.object({
export default function Error() { export default function Error() {
const { t } = useLocale(); const { t } = useLocale();
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
const { error } = querySchema.parse(searchParams); const { error } = querySchema.parse(searchParams);
const isTokenVerificationError = error?.toLowerCase() === "verification"; const isTokenVerificationError = error?.toLowerCase() === "verification";
const errorMsg = isTokenVerificationError ? t("token_invalid_expired") : t("error_during_login"); const errorMsg = isTokenVerificationError ? t("token_invalid_expired") : t("error_during_login");
+22 -3
View File
@@ -4,7 +4,7 @@ import { jwtVerify } from "jose";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import { getCsrfToken, signIn } from "next-auth/react"; import { getCsrfToken, signIn } from "next-auth/react";
import Link from "next/link"; import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter } from "next/navigation";
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
import { useState } from "react"; import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
@@ -17,6 +17,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml"; import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
import { WEBAPP_URL, WEBSITE_URL, HOSTED_CAL_FEATURES } from "@calcom/lib/constants"; import { WEBAPP_URL, WEBSITE_URL, HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl"; import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
@@ -53,7 +54,7 @@ export default function Login({
totpEmail, totpEmail,
}: // eslint-disable-next-line @typescript-eslint/ban-types }: // eslint-disable-next-line @typescript-eslint/ban-types
inferSSRProps<typeof _getServerSideProps> & WithNonceProps<{}>) { inferSSRProps<typeof _getServerSideProps> & WithNonceProps<{}>) {
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
const { t } = useLocale(); const { t } = useLocale();
const router = useRouter(); const router = useRouter();
const formSchema = z const formSchema = z
@@ -278,7 +279,7 @@ inferSSRProps<typeof _getServerSideProps> & WithNonceProps<{}>) {
// TODO: Once we understand how to retrieve prop types automatically from getServerSideProps, remove this temporary variable // TODO: Once we understand how to retrieve prop types automatically from getServerSideProps, remove this temporary variable
const _getServerSideProps = async function getServerSideProps(context: GetServerSidePropsContext) { const _getServerSideProps = async function getServerSideProps(context: GetServerSidePropsContext) {
const { req, res } = context; const { req, res, query } = context;
const session = await getServerSession({ req, res }); const session = await getServerSession({ req, res });
const ssr = await ssrInit(context); const ssr = await ssrInit(context);
@@ -318,6 +319,24 @@ const _getServerSideProps = async function getServerSideProps(context: GetServer
} }
if (session) { if (session) {
const { callbackUrl } = query;
if (callbackUrl) {
try {
const destination = getSafeRedirectUrl(callbackUrl as string);
if (destination) {
return {
redirect: {
destination,
permanent: false,
},
};
}
} catch (e) {
console.warn(e);
}
}
return { return {
redirect: { redirect: {
destination: "/", destination: "/",
+2 -2
View File
@@ -1,9 +1,9 @@
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useSearchParams } from "next/navigation";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { APP_NAME } from "@calcom/lib/constants"; import { APP_NAME } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Avatar, Button, Select } from "@calcom/ui"; import { Avatar, Button, Select } from "@calcom/ui";
@@ -16,7 +16,7 @@ export default function Authorize() {
const { status } = useSession(); const { status } = useSession();
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
const client_id = searchParams?.get("client_id") as string; const client_id = searchParams?.get("client_id") as string;
const state = searchParams?.get("state") as string; const state = searchParams?.get("state") as string;
+3 -2
View File
@@ -1,12 +1,13 @@
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import { useSearchParams } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import PageWrapper from "@components/PageWrapper"; import PageWrapper from "@components/PageWrapper";
// To handle the IdP initiated login flow callback // To handle the IdP initiated login flow callback
export default function Page() { export default function Page() {
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
useEffect(() => { useEffect(() => {
const code = searchParams?.get("code"); const code = searchParams?.get("code");
+3 -2
View File
@@ -1,11 +1,12 @@
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import AdminAppsList from "@calcom/features/apps/AdminAppsList"; import AdminAppsList from "@calcom/features/apps/AdminAppsList";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getDeploymentKey } from "@calcom/features/ee/deployment/lib/getDeploymentKey"; import { getDeploymentKey } from "@calcom/features/ee/deployment/lib/getDeploymentKey";
import { APP_NAME } from "@calcom/lib/constants"; import { APP_NAME } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { UserPermissionRole } from "@calcom/prisma/enums"; import { UserPermissionRole } from "@calcom/prisma/enums";
@@ -21,7 +22,7 @@ import { ssrInit } from "@server/lib/ssr";
function useSetStep() { function useSetStep() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
const pathname = usePathname(); const pathname = usePathname();
const setStep = (newStep = 1) => { const setStep = (newStep = 1) => {
const _searchParams = new URLSearchParams(searchParams ?? undefined); const _searchParams = new URLSearchParams(searchParams ?? undefined);
+3 -2
View File
@@ -1,6 +1,6 @@
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils"; import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
@@ -9,6 +9,7 @@ import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomain
import stripe from "@calcom/features/ee/payments/server/stripe"; import stripe from "@calcom/features/ee/payments/server/stripe";
import { hostedCal, isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml"; import { hostedCal, isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
import { ssoTenantProduct } from "@calcom/features/ee/sso/lib/sso"; import { ssoTenantProduct } from "@calcom/features/ee/sso/lib/sso";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { checkUsername } from "@calcom/lib/server/checkUsername"; import { checkUsername } from "@calcom/lib/server/checkUsername";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
@@ -22,7 +23,7 @@ import { ssrInit } from "@server/lib/ssr";
export type SSOProviderPageProps = inferSSRProps<typeof getServerSideProps>; export type SSOProviderPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Provider(props: SSOProviderPageProps) { export default function Provider(props: SSOProviderPageProps) {
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
+3 -2
View File
@@ -1,10 +1,11 @@
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import Head from "next/head"; import Head from "next/head";
import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import z from "zod"; import z from "zod";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants"; import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Button, showToast } from "@calcom/ui"; import { Button, showToast } from "@calcom/ui";
@@ -54,7 +55,7 @@ const querySchema = z.object({
}); });
export default function Verify() { export default function Verify() {
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
const pathname = usePathname(); const pathname = usePathname();
const router = useRouter(); const router = useRouter();
const routerQuery = useRouterQuery(); const routerQuery = useRouterQuery();
+3 -2
View File
@@ -1,4 +1,4 @@
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
@@ -8,6 +8,7 @@ import Schedule from "@calcom/features/schedules/components/Schedule";
import Shell from "@calcom/features/shell/Shell"; import Shell from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib"; import { classNames } from "@calcom/lib";
import { availabilityAsString } from "@calcom/lib/availability"; import { availabilityAsString } from "@calcom/lib/availability";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error"; import { HttpError } from "@calcom/lib/http-error";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
@@ -83,7 +84,7 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
}; };
export default function Availability() { export default function Availability() {
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
const { t, i18n } = useLocale(); const { t, i18n } = useLocale();
const router = useRouter(); const router = useRouter();
const utils = trpc.useContext(); const utils = trpc.useContext();
+3 -2
View File
@@ -1,11 +1,12 @@
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useRouter, useSearchParams, usePathname } from "next/navigation"; import { useRouter, usePathname } from "next/navigation";
import { useCallback } from "react"; import { useCallback } from "react";
import { getLayout } from "@calcom/features/MainLayout"; import { getLayout } from "@calcom/features/MainLayout";
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules"; import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
import { ShellMain } from "@calcom/features/shell/Shell"; import { ShellMain } from "@calcom/features/shell/Shell";
import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable"; import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error"; import { HttpError } from "@calcom/lib/http-error";
import type { RouterOutputs } from "@calcom/trpc/react"; import type { RouterOutputs } from "@calcom/trpc/react";
@@ -131,7 +132,7 @@ const WithQuery = withQuery(trpc.viewer.availability.list as any);
export default function AvailabilityPage() { export default function AvailabilityPage() {
const { t } = useLocale(); const { t } = useLocale();
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
+11 -130
View File
@@ -1,139 +1,20 @@
import dayjs from "@calcom/dayjs"; import { Troubleshooter } from "@calcom/features/troubleshooter/Troubleshooter";
import Shell from "@calcom/features/shell/Shell"; import { getLayout } from "@calcom/features/troubleshooter/layout";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react"; import { HeadSeo } from "@calcom/ui";
import { trpc } from "@calcom/trpc/react";
import { SkeletonText } from "@calcom/ui";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import PageWrapper from "@components/PageWrapper"; import PageWrapper from "@components/PageWrapper";
type User = RouterOutputs["viewer"]["me"]; function TroubleshooterPage() {
export interface IBusySlot {
start: string | Date;
end: string | Date;
title?: string;
source?: string | null;
}
const AvailabilityView = ({ user }: { user: User }) => {
const { t } = useLocale();
const { date, setQuery: setSelectedDate } = useRouterQuery("date");
const selectedDate = dayjs(date);
const formattedSelectedDate = selectedDate.format("YYYY-MM-DD");
const { data, isLoading } = trpc.viewer.availability.user.useQuery(
{
username: user.username || "",
dateFrom: selectedDate.startOf("day").utc().format(),
dateTo: selectedDate.endOf("day").utc().format(),
withSource: true,
},
{
enabled: !!user.username,
}
);
const overrides =
data?.dateOverrides.reduce((acc, override) => {
if (
formattedSelectedDate !== dayjs(override.start).format("YYYY-MM-DD") &&
formattedSelectedDate !== dayjs(override.end).format("YYYY-MM-DD")
)
return acc;
acc.push({ ...override, source: "Date override" });
return acc;
}, [] as IBusySlot[]) || [];
return (
<div className="bg-default max-w-xl overflow-hidden rounded-md shadow">
<div className="px-4 py-5 sm:p-6">
{t("overview_of_day")}{" "}
<input
type="date"
className="inline h-8 border-none bg-inherit p-0"
defaultValue={formattedSelectedDate}
onChange={(e) => {
if (e.target.value) setSelectedDate(e.target.value);
}}
/>
<small className="text-muted block">{t("hover_over_bold_times_tip")}</small>
<div className="mt-4 space-y-4">
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
</div>
</div>
{(() => {
if (isLoading)
return (
<>
<SkeletonText className="block h-16 w-full" />
<SkeletonText className="block h-16 w-full" />
</>
);
if (data && (data.busy.length > 0 || overrides.length > 0))
return [...data.busy, ...overrides]
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
.map((slot: IBusySlot) => (
<div
key={dayjs(slot.start).format("HH:mm")}
className="bg-subtle overflow-hidden rounded-md"
data-testid="troubleshooter-busy-time">
<div className="text-emphasis px-4 py-5 sm:p-6">
{t("calendar_shows_busy_between")}{" "}
<span className="text-default font-medium" title={dayjs(slot.start).format("HH:mm")}>
{dayjs(slot.start).format("HH:mm")}
</span>{" "}
{t("and")}{" "}
<span className="text-default font-medium" title={dayjs(slot.end).format("HH:mm")}>
{dayjs(slot.end).format("HH:mm")}
</span>{" "}
{t("on")} {dayjs(slot.start).format("D")}{" "}
{t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
{slot.title && ` - (${slot.title})`}
{slot.source && <small>{` - (source: ${slot.source})`}</small>}
</div>
</div>
));
return (
<div className="bg-subtle overflow-hidden rounded-md">
<div className="text-emphasis px-4 py-5 sm:p-6">{t("calendar_no_busy_slots")}</div>
</div>
);
})()}
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
{t("your_day_ends_at")} {convertMinsToHrsMins(user.endTime)}
</div>
</div>
</div>
</div>
</div>
);
};
export default function Troubleshoot() {
const { data, isLoading } = trpc.viewer.me.useQuery();
const { t } = useLocale(); const { t } = useLocale();
return ( return (
<div> <>
<Shell heading={t("troubleshoot")} hideHeadingOnMobile subtitle={t("troubleshoot_description")}> <HeadSeo title={t("troubleshoot")} description={t("troubleshoot_availability")} />
{!isLoading && data && <AvailabilityView user={data} />} <Troubleshooter month={null} />
</Shell> </>
</div>
); );
} }
Troubleshoot.PageWrapper = PageWrapper;
function convertMinsToHrsMins(mins: number) { TroubleshooterPage.getLayout = getLayout;
const h = Math.floor(mins / 60); TroubleshooterPage.PageWrapper = PageWrapper;
const m = mins % 60; export default TroubleshooterPage;
const hs = h < 10 ? `0${h}` : h;
const ms = m < 10 ? `0${m}` : m;
return `${hs}:${ms}`;
}
+3 -2
View File
@@ -4,7 +4,7 @@ import { createEvent } from "ics";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import Link from "next/link"; import Link from "next/link";
import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { RRule } from "rrule"; import { RRule } from "rrule";
import { z } from "zod"; import { z } from "zod";
@@ -36,6 +36,7 @@ import {
} from "@calcom/lib/date-fns"; } from "@calcom/lib/date-fns";
import { getDefaultEvent } from "@calcom/lib/defaultEvents"; import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import useGetBrandingColours from "@calcom/lib/getBrandColours"; import useGetBrandingColours from "@calcom/lib/getBrandColours";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import useTheme from "@calcom/lib/hooks/useTheme"; import useTheme from "@calcom/lib/hooks/useTheme";
@@ -98,7 +99,7 @@ export default function Success(props: SuccessProps) {
const router = useRouter(); const router = useRouter();
const routerQuery = useRouterQuery(); const routerQuery = useRouterQuery();
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
const { const {
allRemainingBookings, allRemainingBookings,
isSuccessBookingPage, isSuccessBookingPage,
+7 -4
View File
@@ -1,8 +1,10 @@
"use client";
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { User } from "@prisma/client"; import type { User } from "@prisma/client";
import { Trans } from "next-i18next"; import { Trans } from "next-i18next";
import Link from "next/link"; import Link from "next/link";
import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import type { FC } from "react"; import type { FC } from "react";
import { memo, useEffect, useState } from "react"; import { memo, useEffect, useState } from "react";
import { z } from "zod"; import { z } from "zod";
@@ -19,6 +21,7 @@ import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsF
import { ShellMain } from "@calcom/features/shell/Shell"; import { ShellMain } from "@calcom/features/shell/Shell";
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants"; import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl"; import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery"; import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
@@ -208,7 +211,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
const { t } = useLocale(); const { t } = useLocale();
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
const orgBranding = useOrgBranding(); const orgBranding = useOrgBranding();
const [parent] = useAutoAnimate<HTMLUListElement>(); const [parent] = useAutoAnimate<HTMLUListElement>();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@@ -840,7 +843,7 @@ const Main = ({
filters: ReturnType<typeof getTeamsFiltersFromQuery>; filters: ReturnType<typeof getTeamsFiltersFromQuery>;
}) => { }) => {
const isMobile = useMediaQuery("(max-width: 768px)"); const isMobile = useMediaQuery("(max-width: 768px)");
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
const orgBranding = useOrgBranding(); const orgBranding = useOrgBranding();
if (!data || status === "loading") { if (!data || status === "loading") {
@@ -904,7 +907,7 @@ const Main = ({
const EventTypesPage = () => { const EventTypesPage = () => {
const { t } = useLocale(); const { t } = useLocale();
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
const { open } = useIntercom(); const { open } = useIntercom();
const { data: user } = useMeQuery(); const { data: user } = useMeQuery();
const [showProfileBanner, setShowProfileBanner] = useState(false); const [showProfileBanner, setShowProfileBanner] = useState(false);
@@ -250,10 +250,7 @@ const AppearanceView = ({
/> />
{lightModeError ? ( {lightModeError ? (
<div className="mt-4"> <div className="mt-4">
<Alert <Alert severity="warning" message={t("light_theme_contrast_error")} />
severity="warning"
message="Light Theme color doesn't pass contrast check. We recommend you change this colour so your buttons will be more visible."
/>
</div> </div>
) : null} ) : null}
</div> </div>
@@ -282,10 +279,7 @@ const AppearanceView = ({
/> />
{darkModeError ? ( {darkModeError ? (
<div className="mt-4"> <div className="mt-4">
<Alert <Alert severity="warning" message={t("dark_theme_contrast_error")} />
severity="warning"
message="Dark Theme color doesn't pass contrast check. We recommend you change this colour so your buttons will be more visible."
/>
</div> </div>
) : null} ) : null}
</div> </div>
+12 -31
View File
@@ -10,7 +10,7 @@ import OrganizationMemberAvatar from "@calcom/features/ee/organizations/componen
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions"; import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants"; import { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
import { AVATAR_FALLBACK } from "@calcom/lib/constants"; import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt"; import { md } from "@calcom/lib/markdownIt";
import turndown from "@calcom/lib/turndownService"; import turndown from "@calcom/lib/turndownService";
@@ -72,37 +72,22 @@ interface DeleteAccountValues {
type FormValues = { type FormValues = {
username: string; username: string;
avatar: string | null; avatar: string;
name: string; name: string;
email: string; email: string;
bio: string; bio: string;
}; };
const checkIfItFallbackImage = (fetchedImgSrc?: string) => {
return !fetchedImgSrc || fetchedImgSrc.endsWith(AVATAR_FALLBACK);
};
const ProfileView = () => { const ProfileView = () => {
const { t } = useLocale(); const { t } = useLocale();
const utils = trpc.useContext(); const utils = trpc.useContext();
const { update } = useSession(); const { update } = useSession();
const { data: user, isLoading } = trpc.viewer.me.useQuery();
const [fetchedImgSrc, setFetchedImgSrc] = useState<string | undefined>(undefined); const { data: avatarData } = trpc.viewer.avatar.useQuery(undefined, {
enabled: !isLoading && !user?.avatarUrl,
const { data: user, isLoading } = trpc.viewer.me.useQuery(undefined, {
onSuccess: async (userData) => {
try {
if (!userData.organization) {
const res = await fetch(userData.avatar);
if (res.url) setFetchedImgSrc(res.url);
} else {
setFetchedImgSrc("");
}
} catch (err) {
setFetchedImgSrc("");
}
},
}); });
const updateProfileMutation = trpc.viewer.updateProfile.useMutation({ const updateProfileMutation = trpc.viewer.updateProfile.useMutation({
onSuccess: async (res) => { onSuccess: async (res) => {
await update(res); await update(res);
@@ -234,7 +219,7 @@ const ProfileView = () => {
const defaultValues = { const defaultValues = {
username: user.username || "", username: user.username || "",
avatar: user.avatar || "", avatar: getUserAvatarUrl(user),
name: user.name || "", name: user.name || "",
email: user.email || "", email: user.email || "",
bio: user.bio || "", bio: user.bio || "",
@@ -251,8 +236,7 @@ const ProfileView = () => {
key={JSON.stringify(defaultValues)} key={JSON.stringify(defaultValues)}
defaultValues={defaultValues} defaultValues={defaultValues}
isLoading={updateProfileMutation.isLoading} isLoading={updateProfileMutation.isLoading}
isFallbackImg={checkIfItFallbackImage(fetchedImgSrc)} isFallbackImg={!user.avatarUrl && !avatarData?.avatar}
userAvatar={user.avatar}
user={user} user={user}
userOrganization={user.organization} userOrganization={user.organization}
onSubmit={(values) => { onSubmit={(values) => {
@@ -398,7 +382,6 @@ const ProfileForm = ({
extraField, extraField,
isLoading = false, isLoading = false,
isFallbackImg, isFallbackImg,
userAvatar,
user, user,
userOrganization, userOrganization,
}: { }: {
@@ -407,7 +390,6 @@ const ProfileForm = ({
extraField?: React.ReactNode; extraField?: React.ReactNode;
isLoading: boolean; isLoading: boolean;
isFallbackImg: boolean; isFallbackImg: boolean;
userAvatar: string;
user: RouterOutputs["viewer"]["me"]; user: RouterOutputs["viewer"]["me"];
userOrganization: RouterOutputs["viewer"]["me"]["organization"]; userOrganization: RouterOutputs["viewer"]["me"]["organization"];
}) => { }) => {
@@ -416,7 +398,7 @@ const ProfileForm = ({
const profileFormSchema = z.object({ const profileFormSchema = z.object({
username: z.string(), username: z.string(),
avatar: z.string().nullable(), avatar: z.string(),
name: z name: z
.string() .string()
.trim() .trim()
@@ -438,7 +420,6 @@ const ProfileForm = ({
} = formMethods; } = formMethods;
const isDisabled = isSubmitting || !isDirty; const isDisabled = isSubmitting || !isDirty;
return ( return (
<Form form={formMethods} handleSubmit={onSubmit}> <Form form={formMethods} handleSubmit={onSubmit}>
<div className="border-subtle border-x px-4 pb-10 pt-8 sm:px-6"> <div className="border-subtle border-x px-4 pb-10 pt-8 sm:px-6">
@@ -447,7 +428,7 @@ const ProfileForm = ({
control={formMethods.control} control={formMethods.control}
name="avatar" name="avatar"
render={({ field: { value } }) => { render={({ field: { value } }) => {
const showRemoveAvatarButton = !isFallbackImg || (value && userAvatar !== value); const showRemoveAvatarButton = value === null ? false : !isFallbackImg;
const organization = const organization =
userOrganization && userOrganization.id userOrganization && userOrganization.id
? { ? {
@@ -474,7 +455,7 @@ const ProfileForm = ({
handleAvatarChange={(newAvatar) => { handleAvatarChange={(newAvatar) => {
formMethods.setValue("avatar", newAvatar, { shouldDirty: true }); formMethods.setValue("avatar", newAvatar, { shouldDirty: true });
}} }}
imageSrc={value || undefined} imageSrc={value}
triggerButtonColor={showRemoveAvatarButton ? "secondary" : "primary"} triggerButtonColor={showRemoveAvatarButton ? "secondary" : "primary"}
/> />
@@ -482,7 +463,7 @@ const ProfileForm = ({
<Button <Button
color="secondary" color="secondary"
onClick={() => { onClick={() => {
formMethods.setValue("avatar", null, { shouldDirty: true }); formMethods.setValue("avatar", "", { shouldDirty: true });
}}> }}>
{t("remove")} {t("remove")}
</Button> </Button>
+4 -4
View File
@@ -1,7 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import { useSearchParams } from "next/navigation";
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
import type { SubmitHandler } from "react-hook-form"; import type { SubmitHandler } from "react-hook-form";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
@@ -13,6 +12,7 @@ import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { useFlagMap } from "@calcom/features/flags/context/provider"; import { useFlagMap } from "@calcom/features/flags/context/provider";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils"; import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants"; import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import slugify from "@calcom/lib/slugify"; import slugify from "@calcom/lib/slugify";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
@@ -37,7 +37,7 @@ type SignupProps = inferSSRProps<typeof getServerSideProps>;
const checkValidEmail = (email: string) => z.string().email().safeParse(email).success; const checkValidEmail = (email: string) => z.string().email().safeParse(email).success;
const getOrgUsernameFromEmail = (email: string, autoAcceptEmailDomain: string) => { const getOrgUsernameFromEmail = (email: string, autoAcceptEmailDomain: string) => {
const [emailUser, emailDomain] = email.split("@"); const [emailUser, emailDomain = ""] = email.split("@");
const username = const username =
emailDomain === autoAcceptEmailDomain emailDomain === autoAcceptEmailDomain
? slugify(emailUser) ? slugify(emailUser)
@@ -53,7 +53,7 @@ function addOrUpdateQueryParam(url: string, key: string, value: string) {
} }
export default function Signup({ prepopulateFormValues, token, orgSlug, orgAutoAcceptEmail }: SignupProps) { export default function Signup({ prepopulateFormValues, token, orgSlug, orgAutoAcceptEmail }: SignupProps) {
const searchParams = useSearchParams(); const searchParams = useCompatSearchParams();
const telemetry = useTelemetry(); const telemetry = useTelemetry();
const { t, i18n } = useLocale(); const { t, i18n } = useLocale();
const flags = useFlagMap(); const flags = useFlagMap();
@@ -143,7 +143,7 @@ export default function Signup({ prepopulateFormValues, token, orgSlug, orgAutoA
methods.clearErrors("apiError"); methods.clearErrors("apiError");
} }
if (methods.getValues().username === undefined && isOrgInviteByLink && orgAutoAcceptEmail) { if (!methods.getValues().username && isOrgInviteByLink && orgAutoAcceptEmail) {
methods.setValue( methods.setValue(
"username", "username",
getOrgUsernameFromEmail(methods.getValues().email, orgAutoAcceptEmail) getOrgUsernameFromEmail(methods.getValues().email, orgAutoAcceptEmail)
+23 -12
View File
@@ -11,10 +11,6 @@ let pages = (exports.pages = glob
) )
.filter((v, i, self) => self.indexOf(v) === i && !v.startsWith("[user]"))); .filter((v, i, self) => self.indexOf(v) === i && !v.startsWith("[user]")));
// Following routes don't exist but they work by doing rewrite. Thus they need to be excluded from matching the orgRewrite patterns
// Make sure to keep it upto date as more nonExistingRouteRewrites are added.
const otherNonExistingRoutePrefixes = ["forms", "router", "success", "cancel"];
// .* matches / as well(Note: *(i.e wildcard) doesn't match / but .*(i.e. RegExp) does) // .* matches / as well(Note: *(i.e wildcard) doesn't match / but .*(i.e. RegExp) does)
// It would match /free/30min but not /bookings/upcoming because 'bookings' is an item in pages // It would match /free/30min but not /bookings/upcoming because 'bookings' is an item in pages
// It would also not match /free/30min/embed because we are ensuring just two slashes // It would also not match /free/30min/embed because we are ensuring just two slashes
@@ -27,11 +23,26 @@ let subdomainRegExp = (exports.subdomainRegExp = getSubdomainRegExp(
)); ));
exports.orgHostPath = `^(?<orgSlug>${subdomainRegExp})\\.(?!vercel\.app).*`; exports.orgHostPath = `^(?<orgSlug>${subdomainRegExp})\\.(?!vercel\.app).*`;
let beforeRewriteExcludePages = pages.concat(otherNonExistingRoutePrefixes); /**
exports.orgUserRoutePath = `/:user((?!${beforeRewriteExcludePages.join("|")}|_next|public)[a-zA-Z0-9\-_]+)`; * Returns a regex that matches all existing routes, virtual routes (like /forms, /router, /success etc) and nextjs special paths (_next, public)
exports.orgUserTypeRoutePath = `/:user((?!${beforeRewriteExcludePages.join( */
"/|" function getRegExpMatchingAllReservedRoutes(suffix) {
)}|_next/|public/)[^/]+)/:type((?!avatar\.png)[^/]+)`; // Following routes don't exist but they work by doing rewrite. Thus they need to be excluded from matching the orgRewrite patterns
exports.orgUserTypeEmbedRoutePath = `/:user((?!${beforeRewriteExcludePages.join( // Make sure to keep it upto date as more nonExistingRouteRewrites are added.
"/|" const otherNonExistingRoutePrefixes = ["forms", "router", "success", "cancel"];
)}|_next/|public/)[^/]+)/:type/embed`; const nextJsSpecialPaths = ["_next", "public"];
let beforeRewriteExcludePages = pages.concat(otherNonExistingRoutePrefixes).concat(nextJsSpecialPaths);
return beforeRewriteExcludePages.join(`${suffix}|`) + suffix;
}
// To handle /something
exports.orgUserRoutePath = `/:user((?!${getRegExpMatchingAllReservedRoutes("/?$")})[a-zA-Z0-9\-_]+)`;
// To handle /something/somethingelse
exports.orgUserTypeRoutePath = `/:user((?!${getRegExpMatchingAllReservedRoutes(
"/"
)})[^/]+)/:type((?!avatar\.png)[^/]+)`;
// To handle /something/somethingelse/embed
exports.orgUserTypeEmbedRoutePath = `/:user((?!${getRegExpMatchingAllReservedRoutes("/")})[^/]+)/:type/embed`;
+1
View File
@@ -40,6 +40,7 @@ test.describe("Availablity tests", () => {
const date = json[0].result.data.json.schedule.availability.find((a) => !!a.date); const date = json[0].result.data.json.schedule.availability.find((a) => !!a.date);
const troubleshooterURL = `/availability/troubleshoot?date=${dayjs(date.date).format("YYYY-MM-DD")}`; const troubleshooterURL = `/availability/troubleshoot?date=${dayjs(date.date).format("YYYY-MM-DD")}`;
await page.goto(troubleshooterURL); await page.goto(troubleshooterURL);
await page.waitForLoadState("networkidle");
await expect(page.locator('[data-testid="troubleshooter-busy-time"]')).toHaveCount(1); await expect(page.locator('[data-testid="troubleshooter-busy-time"]')).toHaveCount(1);
}); });
}); });
+91
View File
@@ -1,6 +1,8 @@
import { expect } from "@playwright/test"; import { expect } from "@playwright/test";
import { randomString } from "@calcom/lib/random"; import { randomString } from "@calcom/lib/random";
import { SchedulingType } from "@calcom/prisma/client";
import type { Schedule, TimeRange } from "@calcom/types/schedule";
import { test } from "./lib/fixtures"; import { test } from "./lib/fixtures";
import { import {
@@ -342,3 +344,92 @@ test.describe("Booking on different layouts", () => {
await expect(page.locator("[data-testid=success-page]")).toBeVisible(); await expect(page.locator("[data-testid=success-page]")).toBeVisible();
}); });
}); });
test.describe("Booking round robin event", () => {
test.beforeEach(async ({ page, users }) => {
const teamMatesObj = [{ name: "teammate-1" }];
const dateRanges: TimeRange = {
start: new Date(new Date().setUTCHours(10, 0, 0, 0)), //one hour after default schedule (teammate-1's schedule)
end: new Date(new Date().setUTCHours(17, 0, 0, 0)),
};
const schedule: Schedule = [[], [dateRanges], [dateRanges], [dateRanges], [dateRanges], [dateRanges], []];
const testUser = await users.create(
{ username: "test-user", name: "Test User", email: "testuser@example.com", schedule },
{
hasTeam: true,
schedulingType: SchedulingType.ROUND_ROBIN,
teamEventLength: 120,
teammates: teamMatesObj,
}
);
const team = await testUser.getFirstTeam();
await page.goto(`/team/${team.team.slug}`);
});
test("Does not book round robin host outside availability with date override", async ({ page, users }) => {
const [testUser] = users.get();
testUser.apiLogin();
const team = await testUser.getFirstTeam();
// Click first event type (round robin)
await page.click('[data-testid="event-type-link"]');
await page.click('[data-testid="incrementMonth"]');
// books 9AM slots for 120 minutes (test-user is not available at this time, availability starts at 10)
await page.locator('[data-testid="time"]').nth(0).click();
await page.waitForLoadState("networkidle");
await page.locator('[name="name"]').fill("Test name");
await page.locator('[name="email"]').fill(`${randomString(4)}@example.com`);
await page.click('[data-testid="confirm-book-button"]');
await page.waitForURL((url) => {
return url.pathname.startsWith("/booking");
});
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
const host = await page.locator('[data-testid="booking-host-name"]');
const hostName = await host.innerText();
//expect teammate-1 to be booked, test-user is not available at this time
expect(hostName).toBe("teammate-1");
// make another booking to see if also for the second booking teammate-1 is booked
await page.goto(`/team/${team.team.slug}`);
await page.click('[data-testid="event-type-link"]');
await page.click('[data-testid="incrementMonth"]');
await page.click('[data-testid="incrementMonth"]');
// Again book a 9AM slot for 120 minutes where test-user is not available
await page.locator('[data-testid="time"]').nth(0).click();
await page.waitForLoadState("networkidle");
await page.locator('[name="name"]').fill("Test name");
await page.locator('[name="email"]').fill(`${randomString(4)}@example.com`);
await page.click('[data-testid="confirm-book-button"]');
await page.waitForURL((url) => {
return url.pathname.startsWith("/booking");
});
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
const hostSecondBooking = await page.locator('[data-testid="booking-host-name"]');
const hostNameSecondBooking = await hostSecondBooking.innerText();
expect(hostNameSecondBooking).toBe("teammate-1"); // teammate-1 should be booked again
});
});
+32 -1
View File
@@ -5,16 +5,47 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
import { randomString } from "@calcom/lib/random"; import { randomString } from "@calcom/lib/random";
import { test } from "./lib/fixtures"; import { test } from "./lib/fixtures";
import { testBothFutureAndLegacyRoutes } from "./lib/future-legacy-routes";
import { bookTimeSlot, createNewEventType, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils"; import { bookTimeSlot, createNewEventType, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
test.describe.configure({ mode: "parallel" }); test.describe.configure({ mode: "parallel" });
test.describe("Event Types A/B tests", () => {
test("should point to the /future/event-types page", async ({ page, users, context }) => {
await context.addCookies([
{
name: "x-calcom-future-routes-override",
value: "1",
url: "http://localhost:3000",
},
]);
const user = await users.create();
await user.apiLogin();
await page.goto("/event-types");
await page.waitForLoadState();
const dataNextJsRouter = await page.evaluate(() =>
window.document.documentElement.getAttribute("data-nextjs-router")
);
expect(dataNextJsRouter).toEqual("app");
const locator = page.getByRole("heading", { name: "Event Types" });
await expect(locator).toBeVisible();
});
});
test.describe("Event Types tests", () => { test.describe("Event Types tests", () => {
test.describe("user", () => { testBothFutureAndLegacyRoutes.describe("user", () => {
test.beforeEach(async ({ page, users }) => { test.beforeEach(async ({ page, users }) => {
const user = await users.create(); const user = await users.create();
await user.apiLogin(); await user.apiLogin();
await page.goto("/event-types"); await page.goto("/event-types");
// We wait until loading is finished // We wait until loading is finished
await page.waitForSelector('[data-testid="event-types"]'); await page.waitForSelector('[data-testid="event-types"]');
}); });
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

+34
View File
@@ -0,0 +1,34 @@
import type { Page } from "@playwright/test";
declare global {
interface Window {
E2E_CLIPBOARD_VALUE?: string;
}
}
export type Window = typeof window;
// creates the single server fixture
export const createClipboardFixture = (page: Page) => {
return {
reset: async () => {
await page.evaluate(() => {
delete window.E2E_CLIPBOARD_VALUE;
});
},
get: async () => {
return getClipboardValue({ page });
},
};
};
function getClipboardValue({ page }: { page: Page }) {
return page.evaluate(() => {
return new Promise<string>((resolve, reject) => {
setInterval(() => {
if (!window.E2E_CLIPBOARD_VALUE) return;
resolve(window.E2E_CLIPBOARD_VALUE);
}, 500);
setTimeout(() => reject(new Error("Timeout")), 1000);
});
});
}
@@ -204,7 +204,7 @@ export function createBookingPageFixture(page: Page) {
placeholder?: string placeholder?: string
) => { ) => {
await page.getByTestId("add-field").click(); await page.getByTestId("add-field").click();
await page.locator("#test-field-type > .bg-default > div > div:nth-child(2)").first().click(); await page.getByTestId("test-field-type").click();
await page.getByTestId(`select-option-${questionType}`).click(); await page.getByTestId(`select-option-${questionType}`).click();
await page.getByLabel("Identifier").dblclick(); await page.getByLabel("Identifier").dblclick();
await page.getByLabel("Identifier").fill(identifier); await page.getByLabel("Identifier").fill(identifier);
+41 -7
View File
@@ -10,6 +10,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
import { prisma } from "@calcom/prisma"; import { prisma } from "@calcom/prisma";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums"; import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import type { Schedule } from "@calcom/types/schedule";
import { selectFirstAvailableTimeSlotNextMonth, teamEventSlug, teamEventTitle } from "../lib/testUtils"; import { selectFirstAvailableTimeSlotNextMonth, teamEventSlug, teamEventTitle } from "../lib/testUtils";
import { TimeZoneEnum } from "./types"; import { TimeZoneEnum } from "./types";
@@ -46,6 +47,7 @@ const createTeamEventType = async (
schedulingType?: SchedulingType; schedulingType?: SchedulingType;
teamEventTitle?: string; teamEventTitle?: string;
teamEventSlug?: string; teamEventSlug?: string;
teamEventLength?: number;
} }
) => { ) => {
return await prisma.eventType.create({ return await prisma.eventType.create({
@@ -65,10 +67,16 @@ const createTeamEventType = async (
id: user.id, id: user.id,
}, },
}, },
hosts: {
create: {
userId: user.id,
isFixed: scenario?.schedulingType === SchedulingType.COLLECTIVE ? true : false,
},
},
schedulingType: scenario?.schedulingType ?? SchedulingType.COLLECTIVE, schedulingType: scenario?.schedulingType ?? SchedulingType.COLLECTIVE,
title: scenario?.teamEventTitle ?? `${teamEventTitle}-team-id-${team.id}`, title: scenario?.teamEventTitle ?? `${teamEventTitle}-team-id-${team.id}`,
slug: scenario?.teamEventSlug ?? `${teamEventSlug}-team-id-${team.id}`, slug: scenario?.teamEventSlug ?? `${teamEventSlug}-team-id-${team.id}`,
length: 30, length: scenario?.teamEventLength ?? 30,
}, },
}); });
}; };
@@ -78,12 +86,14 @@ const createTeamAndAddUser = async (
user, user,
isUnpublished, isUnpublished,
isOrg, isOrg,
isOrgVerified,
hasSubteam, hasSubteam,
organizationId, organizationId,
}: { }: {
user: { id: number; username: string | null; role?: MembershipRole }; user: { id: number; email: string; username: string | null; role?: MembershipRole };
isUnpublished?: boolean; isUnpublished?: boolean;
isOrg?: boolean; isOrg?: boolean;
isOrgVerified?: boolean;
hasSubteam?: true; hasSubteam?: true;
organizationId?: number | null; organizationId?: number | null;
}, },
@@ -95,7 +105,14 @@ const createTeamAndAddUser = async (
}; };
data.metadata = { data.metadata = {
...(isUnpublished ? { requestedSlug: slug } : {}), ...(isUnpublished ? { requestedSlug: slug } : {}),
...(isOrg ? { isOrganization: true } : {}), ...(isOrg
? {
isOrganization: true,
isOrganizationVerified: !!isOrgVerified,
orgAutoAcceptEmail: user.email.split("@")[1],
isOrganizationConfigured: false,
}
: {}),
}; };
data.slug = !isUnpublished ? slug : undefined; data.slug = !isUnpublished ? slug : undefined;
if (isOrg && hasSubteam) { if (isOrg && hasSubteam) {
@@ -135,7 +152,9 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn
schedulingType?: SchedulingType; schedulingType?: SchedulingType;
teamEventTitle?: string; teamEventTitle?: string;
teamEventSlug?: string; teamEventSlug?: string;
teamEventLength?: number;
isOrg?: boolean; isOrg?: boolean;
isOrgVerified?: boolean;
hasSubteam?: true; hasSubteam?: true;
isUnpublished?: true; isUnpublished?: true;
} = {} } = {}
@@ -283,9 +302,10 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn
if (scenario.hasTeam) { if (scenario.hasTeam) {
const team = await createTeamAndAddUser( const team = await createTeamAndAddUser(
{ {
user: { id: user.id, username: user.username, role: "OWNER" }, user: { id: user.id, email: user.email, username: user.username, role: "OWNER" },
isUnpublished: scenario.isUnpublished, isUnpublished: scenario.isUnpublished,
isOrg: scenario.isOrg, isOrg: scenario.isOrg,
isOrgVerified: scenario.isOrgVerified,
hasSubteam: scenario.hasSubteam, hasSubteam: scenario.hasSubteam,
organizationId: opts?.organizationId, organizationId: opts?.organizationId,
}, },
@@ -376,6 +396,15 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn
await prisma.user.delete({ where: { id } }); await prisma.user.delete({ where: { id } });
store.users = store.users.filter((b) => b.id !== id); store.users = store.users.filter((b) => b.id !== id);
}, },
set: async (email: string) => {
const user = await prisma.user.findUniqueOrThrow({
where: { email },
include: userIncludes,
});
const userFixture = createUserFixture(user, store.page);
store.users.push(userFixture);
return userFixture;
},
}; };
}; };
@@ -400,7 +429,11 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
eventTypes: user.eventTypes, eventTypes: user.eventTypes,
routingForms: user.routingForms, routingForms: user.routingForms,
self, self,
apiLogin: async () => apiLogin({ ...(await self()), password: user.username }, store.page), apiLogin: async (password?: string) =>
apiLogin({ ...(await self()), password: password || user.username }, store.page),
/**
* @deprecated use apiLogin instead
*/
login: async () => login({ ...(await self()), password: user.username }, store.page), login: async () => login({ ...(await self()), password: user.username }, store.page),
logout: async () => { logout: async () => {
await page.goto("/auth/logout"); await page.goto("/auth/logout");
@@ -489,6 +522,7 @@ type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & {
// ignores adding the worker-index after username // ignores adding the worker-index after username
useExactUsername?: boolean; useExactUsername?: boolean;
roleInOrganization?: MembershipRole; roleInOrganization?: MembershipRole;
schedule?: Schedule;
}; };
// creates the actual user in the db. // creates the actual user in the db.
@@ -520,7 +554,7 @@ const createUser = (
timeZone: opts?.timeZone ?? TimeZoneEnum.UK, timeZone: opts?.timeZone ?? TimeZoneEnum.UK,
availability: { availability: {
createMany: { createMany: {
data: getAvailabilityFromSchedule(DEFAULT_SCHEDULE), data: getAvailabilityFromSchedule(opts?.schedule ?? DEFAULT_SCHEDULE),
}, },
}, },
}, },
@@ -641,7 +675,7 @@ export async function apiLogin(
export async function setupEventWithPrice(eventType: Pick<Prisma.EventType, "id">, page: Page) { export async function setupEventWithPrice(eventType: Pick<Prisma.EventType, "id">, page: Page) {
await page.goto(`/event-types/${eventType?.id}?tabName=apps`); await page.goto(`/event-types/${eventType?.id}?tabName=apps`);
await page.locator("div > .ml-auto").first().click(); await page.locator("[data-testid='app-switch']").first().click();
await page.getByPlaceholder("Price").fill("100"); await page.getByPlaceholder("Price").fill("100");
await page.getByTestId("update-eventtype").click(); await page.getByTestId("update-eventtype").click();
} }
+94 -1
View File
@@ -1,6 +1,10 @@
import { expect } from "@playwright/test"; import { expect } from "@playwright/test";
import type Prisma from "@prisma/client"; import type Prisma from "@prisma/client";
import prisma from "@calcom/prisma";
import { SchedulingType } from "@calcom/prisma/enums";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { test } from "./lib/fixtures"; import { test } from "./lib/fixtures";
import type { Fixtures } from "./lib/fixtures"; import type { Fixtures } from "./lib/fixtures";
import { todo, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils"; import { todo, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
@@ -34,6 +38,95 @@ test.describe("Stripe integration", () => {
}); });
}); });
test("when enabling Stripe, credentialId is included", async ({ page, users }) => {
const user = await users.create();
await user.apiLogin();
await page.goto("/apps/installed");
await user.getPaymentCredential();
const eventType = user.eventTypes.find((e) => e.slug === "paid") as Prisma.EventType;
await user.setupEventWithPrice(eventType);
// Need to wait for the DB to be updated with the metadata
await page.waitForResponse((res) => res.url().includes("update") && res.status() === 200);
// Check event type metadata to see if credentialId is included
const eventTypeMetadata = await prisma.eventType.findFirst({
where: {
id: eventType.id,
},
select: {
metadata: true,
},
});
const metadata = EventTypeMetaDataSchema.parse(eventTypeMetadata?.metadata);
const stripeAppMetadata = metadata?.apps?.stripe;
expect(stripeAppMetadata).toHaveProperty("credentialId");
expect(typeof stripeAppMetadata?.credentialId).toBe("number");
});
test("when enabling Stripe, team credentialId is included", async ({ page, users }) => {
const ownerObj = { username: "pro-user", name: "pro-user" };
const teamMatesObj = [
{ name: "teammate-1" },
{ name: "teammate-2" },
{ name: "teammate-3" },
{ name: "teammate-4" },
];
const owner = await users.create(ownerObj, {
hasTeam: true,
teammates: teamMatesObj,
schedulingType: SchedulingType.COLLECTIVE,
});
await owner.apiLogin();
const { team } = await owner.getFirstTeam();
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
const teamEvent = await owner.getFirstTeamEvent(team.id);
await page.goto("/apps/stripe");
/** We start the Stripe flow */
await Promise.all([
page.waitForURL("https://connect.stripe.com/oauth/v2/authorize?*"),
page.click('[data-testid="install-app-button"]'),
page.click('[data-testid="anything else"]'),
]);
await Promise.all([
page.waitForURL("/apps/installed/payment?hl=stripe"),
/** We skip filling Stripe forms (testing mode only) */
page.click('[id="skip-account-app"]'),
]);
await owner.setupEventWithPrice(teamEvent);
// Need to wait for the DB to be updated with the metadata
await page.waitForResponse((res) => res.url().includes("update") && res.status() === 200);
// Check event type metadata to see if credentialId is included
const eventTypeMetadata = await prisma.eventType.findFirst({
where: {
id: teamEvent.id,
},
select: {
metadata: true,
},
});
const metadata = EventTypeMetaDataSchema.parse(eventTypeMetadata?.metadata);
const stripeAppMetadata = metadata?.apps?.stripe;
expect(stripeAppMetadata).toHaveProperty("credentialId");
expect(typeof stripeAppMetadata?.credentialId).toBe("number");
});
test("Can book a paid booking", async ({ page, users }) => { test("Can book a paid booking", async ({ page, users }) => {
const user = await users.create(); const user = await users.create();
const eventType = user.eventTypes.find((e) => e.slug === "paid") as Prisma.EventType; const eventType = user.eventTypes.find((e) => e.slug === "paid") as Prisma.EventType;
@@ -174,7 +267,7 @@ test.describe("Stripe integration", () => {
await page.getByTestId("price-input-stripe").fill("200"); await page.getByTestId("price-input-stripe").fill("200");
// Select currency in dropdown // Select currency in dropdown
await page.locator(".text-black > .bg-default > div > div:nth-child(2)").first().click(); await page.getByTestId("stripe-currency-select").click();
await page.locator("#react-select-2-input").fill("mexi"); await page.locator("#react-select-2-input").fill("mexi");
await page.locator("#react-select-2-option-81").click(); await page.locator("#react-select-2-option-81").click();
+9
View File
@@ -4,10 +4,12 @@ import type { API } from "mailhog";
import mailhog from "mailhog"; import mailhog from "mailhog";
import { IS_MAILHOG_ENABLED } from "@calcom/lib/constants"; import { IS_MAILHOG_ENABLED } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import type { ExpectedUrlDetails } from "../../../../playwright.config"; import type { ExpectedUrlDetails } from "../../../../playwright.config";
import { createBookingsFixture } from "../fixtures/bookings"; import { createBookingsFixture } from "../fixtures/bookings";
import { createClipboardFixture } from "../fixtures/clipboard";
import { createEmbedsFixture } from "../fixtures/embeds"; import { createEmbedsFixture } from "../fixtures/embeds";
import { createOrgsFixture } from "../fixtures/orgs"; import { createOrgsFixture } from "../fixtures/orgs";
import { createPaymentsFixture } from "../fixtures/payments"; import { createPaymentsFixture } from "../fixtures/payments";
@@ -28,6 +30,7 @@ export interface Fixtures {
emails?: API; emails?: API;
routingForms: ReturnType<typeof createRoutingFormsFixture>; routingForms: ReturnType<typeof createRoutingFormsFixture>;
bookingPage: ReturnType<typeof createBookingPageFixture>; bookingPage: ReturnType<typeof createBookingPageFixture>;
clipboard: ReturnType<typeof createClipboardFixture>;
} }
declare global { declare global {
@@ -85,6 +88,8 @@ export const test = base.extend<Fixtures>({
const mailhogAPI = mailhog(); const mailhogAPI = mailhog();
await use(mailhogAPI); await use(mailhogAPI);
} else { } else {
//FIXME: Ideally we should error out here. If someone is running tests with mailhog disabled, they should be aware of it
logger.warn("Mailhog is not enabled - Skipping Emails verification");
await use(undefined); await use(undefined);
} }
}, },
@@ -92,4 +97,8 @@ export const test = base.extend<Fixtures>({
const bookingPage = createBookingPageFixture(page); const bookingPage = createBookingPageFixture(page);
await use(bookingPage); await use(bookingPage);
}, },
clipboard: async ({ page }, use) => {
const clipboard = createClipboardFixture(page);
await use(clipboard);
},
}); });
@@ -0,0 +1,33 @@
import { test } from "./fixtures";
export type RouteVariant = "future" | "legacy";
const routeVariants = ["future", "legacy"];
/**
* Small wrapper around test.describe().
* When using testbothFutureLegacyRoutes.describe() instead of test.describe(), this will run the specified
* tests twice. One with the pages route, and one with the new app dir "future" route. It will also add the route variant
* name to the test name for easier debugging.
* Finally it also adds a parameter routeVariant to your testBothFutureAndLegacyRoutes.describe() callback, which
* can be used to do any conditional rendering in the test for a specific route variant (should be as little
* as possible).
*
* See apps/web/playwright/event-types.e2e.ts for an example.
*/
export const testBothFutureAndLegacyRoutes = {
describe: (testName: string, testFn: (routeVariant: RouteVariant) => void) => {
routeVariants.forEach((routeVariant) => {
test.describe(`${testName} -- ${routeVariant}`, () => {
if (routeVariant === "future") {
test.beforeEach(({ context }) => {
context.addCookies([
{ name: "x-calcom-future-routes-override", value: "1", url: "http://localhost:3000" },
]);
});
}
testFn(routeVariant as RouteVariant);
});
});
},
};
+11
View File
@@ -1,11 +1,13 @@
import type { Frame, Page } from "@playwright/test"; import type { Frame, Page } from "@playwright/test";
import { expect } from "@playwright/test"; import { expect } from "@playwright/test";
import { createHash } from "crypto";
import EventEmitter from "events"; import EventEmitter from "events";
import type { IncomingMessage, ServerResponse } from "http"; import type { IncomingMessage, ServerResponse } from "http";
import { createServer } from "http"; import { createServer } from "http";
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import { noop } from "lodash"; import { noop } from "lodash";
import type { API, Messages } from "mailhog"; import type { API, Messages } from "mailhog";
import { totp } from "otplib";
import type { Prisma } from "@calcom/prisma/client"; import type { Prisma } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums"; import { BookingStatus } from "@calcom/prisma/enums";
@@ -278,3 +280,12 @@ export async function createUserWithSeatedEventAndAttendees(
}); });
return { user, eventType, booking }; return { user, eventType, booking };
} }
export function generateTotpCode(email: string) {
const secret = createHash("md5")
.update(email + process.env.CALENDSO_ENCRYPTION_KEY)
.digest("hex");
totp.options = { step: 90 };
return totp.generate(secret);
}
+2 -2
View File
@@ -24,7 +24,7 @@ test.describe("2FA Tests", async () => {
const user = await users.create(); const user = await users.create();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const userPassword = user.username!; const userPassword = user.username!;
await user.login(); await user.apiLogin();
// expects the home page for an authorized user // expects the home page for an authorized user
await page.goto("/settings/security/two-factor-auth"); await page.goto("/settings/security/two-factor-auth");
@@ -94,7 +94,7 @@ test.describe("2FA Tests", async () => {
const user = await users.create(); const user = await users.create();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const userPassword = user.username!; const userPassword = user.username!;
await user.login(); await user.apiLogin();
// expects the home page for an authorized user // expects the home page for an authorized user
await page.goto("/settings/security/two-factor-auth"); await page.goto("/settings/security/two-factor-auth");
+30 -2
View File
@@ -1,6 +1,9 @@
import { expect } from "@playwright/test"; import { expect } from "@playwright/test";
import type { Page } from "@playwright/test";
import { test } from "./lib/fixtures"; import { test } from "./lib/fixtures";
import { selectFirstAvailableTimeSlotNextMonth, bookTimeSlot } from "./lib/testUtils";
import { localize } from "./lib/testUtils";
test.afterEach(({ users }) => users.deleteAll()); test.afterEach(({ users }) => users.deleteAll());
@@ -69,15 +72,34 @@ test.describe("Managed Event Types tests", () => {
await page.goto("/event-types"); await page.goto("/event-types");
await page.getByTestId("event-types").locator('a[title="managed"]').click(); await page.getByTestId("event-types").locator('a[title="managed"]').click();
await page.getByTestId("vertical-tab-assignment").click(); await page.getByTestId("vertical-tab-assignment").click();
await page.locator('[class$="control"]').filter({ hasText: "Select..." }).click(); await page.getByTestId("assignment-dropdown").click();
await page.getByTestId(`select-option-${memberUser.id}`).click(); await page.getByTestId(`select-option-${memberUser.id}`).click();
await page.locator('[type="submit"]').click(); await page.locator('[type="submit"]').click();
await page.getByTestId("toast-success").waitFor(); await page.getByTestId("toast-success").waitFor();
});
await adminUser.logout(); await test.step("Managed event type can use Organizer's default app as location", async () => {
await page.getByTestId("vertical-tab-event_setup_tab_title").click();
await page.locator("#location-select").click();
const optionText = (await localize("en"))("organizer_default_conferencing_app");
await page.locator(`text=${optionText}`).click();
await page.locator("[data-testid=update-eventtype]").click();
await page.getByTestId("toast-success").waitFor();
await page.waitForLoadState("networkidle");
await page.getByTestId("vertical-tab-assignment").click();
await gotoBookingPage(page);
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);
await expect(page.getByTestId("success-page")).toBeVisible();
}); });
await test.step("Managed event type has locked fields for added member", async () => { await test.step("Managed event type has locked fields for added member", async () => {
await adminUser.logout();
// Coming back as member user to see if there is a managed event present after assignment // Coming back as member user to see if there is a managed event present after assignment
await memberUser.apiLogin(); await memberUser.apiLogin();
await page.goto("/event-types"); await page.goto("/event-types");
@@ -91,3 +113,9 @@ test.describe("Managed Event Types tests", () => {
}); });
}); });
}); });
async function gotoBookingPage(page: Page) {
const previewLink = await page.getByTestId("preview-button").getAttribute("href");
await page.goto(previewLink ?? "");
}
@@ -0,0 +1,28 @@
import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { JSDOM } from "jsdom";
// eslint-disable-next-line no-restricted-imports
import type { API, Messages } from "mailhog";
import { getEmailsReceivedByUser } from "../lib/testUtils";
export async function expectInvitationEmailToBeReceived(
page: Page,
emails: API | undefined,
userEmail: string,
subject: string,
returnLink?: string
) {
if (!emails) return null;
// We need to wait for the email to go through, otherwise it will fail
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(5000);
const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail });
expect(receivedEmails?.total).toBe(1);
const [firstReceivedEmail] = (receivedEmails as Messages).items;
expect(firstReceivedEmail.subject).toBe(subject);
if (!returnLink) return;
const dom = new JSDOM(firstReceivedEmail.html);
const anchor = dom.window.document.querySelector(`a[href*="${returnLink}"]`);
return anchor?.getAttribute("href");
}
@@ -0,0 +1,143 @@
import { expect } from "@playwright/test";
import path from "path";
import { test } from "../lib/fixtures";
import { generateTotpCode } from "../lib/testUtils";
import { expectInvitationEmailToBeReceived } from "./expects";
test.afterAll(({ users, emails }) => {
users.deleteAll();
emails?.deleteAll();
});
function capitalize(text: string) {
if (!text) {
return text;
}
return text.charAt(0).toUpperCase() + text.slice(1);
}
test.describe("Organization", () => {
test("should be able to create an organization and complete onboarding", async ({
page,
users,
emails,
}) => {
const orgOwner = await users.create();
const orgDomain = `${orgOwner.username}-org`;
const orgName = capitalize(`${orgOwner.username}-org`);
await orgOwner.apiLogin();
await page.goto("/settings/organizations/new");
await page.waitForLoadState("networkidle");
await test.step("Basic info", async () => {
// Check required fields
await page.locator("button[type=submit]").click();
await expect(page.locator(".text-red-700")).toHaveCount(3);
// Happy path
await page.locator("input[name=adminEmail]").fill(`john@${orgDomain}.com`);
expect(await page.locator("input[name=name]").inputValue()).toEqual(orgName);
expect(await page.locator("input[name=slug]").inputValue()).toEqual(orgDomain);
await page.locator("button[type=submit]").click();
await page.waitForLoadState("networkidle");
// Check admin email about code verification
await expectInvitationEmailToBeReceived(
page,
emails,
`john@${orgOwner.username}-org.com`,
"Verify your email to create an organization"
);
await test.step("Verification", async () => {
// Code verification
await expect(page.locator("#modal-title")).toBeVisible();
await page.locator("input[name='2fa1']").fill(generateTotpCode(`john@${orgDomain}.com`));
await page.locator("button:text('Verify')").click();
// Check admin email about DNS pending action
await expectInvitationEmailToBeReceived(
page,
emails,
"admin@example.com",
"New organization created: pending action"
);
// Waiting to be in next step URL
await page.waitForURL("/settings/organizations/*/set-password");
});
});
await test.step("Admin password", async () => {
// Check required fields
await page.locator("button[type=submit]").click();
await expect(page.locator(".text-red-700")).toHaveCount(3); // 3 password hints
// Happy path
await page.locator("input[name='password']").fill("ADMIN_user2023$");
await page.locator("button[type=submit]").click();
// Waiting to be in next step URL
await page.waitForURL("/settings/organizations/*/about");
});
await test.step("About the organization", async () => {
// Choosing an avatar
await page.locator('button:text("Upload")').click();
const fileChooserPromise = page.waitForEvent("filechooser");
await page.getByText("Choose a file...").click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, "../../public/apple-touch-icon.png"));
await page.locator('button:text("Save")').click();
// About text
await page.locator('textarea[name="about"]').fill("This is a testing org");
await page.locator("button[type=submit]").click();
// Waiting to be in next step URL
await page.waitForURL("/settings/organizations/*/onboard-admins");
});
await test.step("On-board administrators", async () => {
// Required field
await page.locator("button[type=submit]").click();
// Happy path
await page.locator('textarea[name="emails"]').fill(`rick@${orgDomain}.com`);
await page.locator("button[type=submit]").click();
// Check if invited admin received the invitation email
await expectInvitationEmailToBeReceived(
page,
emails,
`rick@${orgDomain}.com`,
`${orgName}'s admin invited you to join the organization ${orgName} on Cal.com`
);
// Waiting to be in next step URL
await page.waitForURL("/settings/organizations/*/add-teams");
});
await test.step("Create teams", async () => {
// Initial state
await expect(page.locator('input[name="teams.0.name"]')).toHaveCount(1);
await expect(page.locator('button:text("Continue")')).toBeDisabled();
// Filling one team
await page.locator('input[name="teams.0.name"]').fill("Marketing");
await expect(page.locator('button:text("Continue")')).toBeEnabled();
// Adding another team
await page.locator('button:text("Add a team")').click();
await expect(page.locator('button:text("Continue")')).toBeDisabled();
await expect(page.locator('input[name="teams.1.name"]')).toHaveCount(1);
await page.locator('input[name="teams.1.name"]').fill("Sales");
await expect(page.locator('button:text("Continue")')).toBeEnabled();
// Finishing the creation wizard
await page.locator('button:text("Continue")').click();
await page.waitForURL("/event-types");
});
});
});
@@ -0,0 +1,119 @@
import { expect } from "@playwright/test";
import { test } from "../lib/fixtures";
import { expectInvitationEmailToBeReceived } from "./expects";
test.describe.configure({ mode: "parallel" });
test.afterEach(async ({ users, emails, clipboard }) => {
clipboard.reset();
await users.deleteAll();
emails?.deleteAll();
});
test.describe("Organization", () => {
test("Invitation (non verified)", async ({ browser, page, users, emails, clipboard }) => {
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true });
const { team: org } = await orgOwner.getOrg();
await orgOwner.apiLogin();
await page.goto("/settings/organizations/members");
await page.waitForLoadState("networkidle");
await test.step("To the organization by email (external user)", async () => {
const invitedUserEmail = `rick@domain-${Date.now()}.com`;
await page.locator('button:text("Add")').click();
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
await page.locator('button:text("Send invite")').click();
await page.waitForLoadState("networkidle");
const inviteLink = await expectInvitationEmailToBeReceived(
page,
emails,
invitedUserEmail,
`${org.name}'s admin invited you to join the organization ${org.name} on Cal.com`,
"signup?token"
);
// Check newly invited member exists and is pending
await expect(
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
).toHaveCount(1);
// eslint-disable-next-line playwright/no-conditional-in-test
if (!inviteLink) return null;
// Follow invite link in new window
const context = await browser.newContext();
const newPage = await context.newPage();
newPage.goto(inviteLink);
await newPage.waitForLoadState("networkidle");
// Check required fields
await newPage.locator("button[type=submit]").click();
await expect(newPage.locator(".text-red-700")).toHaveCount(3); // 3 password hints
await newPage.locator("input[name=password]").fill(`P4ssw0rd!`);
await newPage.locator("button[type=submit]").click();
await newPage.waitForURL("/getting-started?from=signup");
await context.close();
await newPage.close();
// Check newly invited member is not pending anymore
await page.bringToFront();
await page.goto("/settings/organizations/members");
page.locator(`[data-testid="login-form"]`);
await expect(
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
).toHaveCount(0);
});
await test.step("To the organization by invite link", async () => {
// Get the invite link
await page.locator('button:text("Add")').click();
await page.locator(`[data-testid="copy-invite-link-button"]`).click();
const inviteLink = await clipboard.get();
await page.waitForLoadState("networkidle");
// Follow invite link in new window
const context = await browser.newContext();
const inviteLinkPage = await context.newPage();
await inviteLinkPage.goto(inviteLink);
await inviteLinkPage.waitForLoadState("networkidle");
// Check required fields
await inviteLinkPage.locator("button[type=submit]").click();
await expect(inviteLinkPage.locator(".text-red-700")).toHaveCount(4); // email + 3 password hints
// Happy path
await inviteLinkPage.locator("input[name=email]").fill(`rick@domain-${Date.now()}.com`);
await inviteLinkPage.locator("input[name=password]").fill(`P4ssw0rd!`);
await inviteLinkPage.locator("button[type=submit]").click();
await inviteLinkPage.waitForURL("/getting-started");
});
});
test("Invitation (verified)", async ({ browser, page, users, emails }) => {
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, isOrgVerified: true });
const { team: org } = await orgOwner.getOrg();
await orgOwner.apiLogin();
await page.goto("/settings/organizations/members");
await page.waitForLoadState("networkidle");
await test.step("To the organization by email (internal user)", async () => {
const invitedUserEmail = `rick@example.com`;
await page.locator('button:text("Add")').click();
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
await page.locator('button:text("Send invite")').click();
await page.waitForLoadState("networkidle");
await expectInvitationEmailToBeReceived(
page,
emails,
invitedUserEmail,
`${org.name}'s admin invited you to join the organization ${org.name} on Cal.com`
);
// Check newly invited member exists and is pending
await expect(
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
).toHaveCount(0);
});
});
});
+3 -3
View File
@@ -77,7 +77,7 @@ test.describe("Payment app", () => {
await page.goto(`event-types/${paymentEvent.id}?tabName=apps`); await page.goto(`event-types/${paymentEvent.id}?tabName=apps`);
await page.locator("#event-type-form").getByRole("switch").click(); await page.locator("#event-type-form").getByRole("switch").click();
await page.locator(".text-black > .bg-default > div > div:nth-child(2)").first().click(); await page.getByTestId("stripe-currency-select").click();
await page.getByTestId("select-option-usd").click(); await page.getByTestId("select-option-usd").click();
await page.getByTestId("price-input-stripe").click(); await page.getByTestId("price-input-stripe").click();
@@ -123,10 +123,10 @@ test.describe("Payment app", () => {
await page.getByPlaceholder("Price").click(); await page.getByPlaceholder("Price").click();
await page.getByPlaceholder("Price").fill("150"); await page.getByPlaceholder("Price").fill("150");
await page.locator(".text-black > .bg-default > div > div:nth-child(2)").first().click(); await page.getByTestId("paypal-currency-select").click();
await page.locator("#react-select-2-option-13").click(); await page.locator("#react-select-2-option-13").click();
await page.locator(".mb-1 > .bg-default > div > div:nth-child(2)").first().click(); await page.getByTestId("paypal-payment-option-select").click();
await page.getByText("$MXNCurrencyMexican pesoPayment option").click(); await page.getByText("$MXNCurrencyMexican pesoPayment option").click();
await page.getByTestId("update-eventtype").click(); await page.getByTestId("update-eventtype").click();
@@ -0,0 +1,56 @@
import { expect } from "@playwright/test";
import path from "path";
import { prisma } from "@calcom/prisma";
import { test } from "../lib/fixtures";
test.describe("UploadAvatar", async () => {
test("can upload an image", async ({ page, users }) => {
const user = await users.create({});
await user.apiLogin();
await test.step("Can upload an initial picture", async () => {
await page.goto("/settings/my-account/profile");
await page.getByTestId("open-upload-avatar-dialog").click();
const [fileChooser] = await Promise.all([
// It is important to call waitForEvent before click to set up waiting.
page.waitForEvent("filechooser"),
// Opens the file chooser.
page.getByTestId("open-upload-image-filechooser").click(),
]);
await fileChooser.setFiles(`${path.dirname(__filename)}/../fixtures/cal.png`);
await page.getByTestId("upload-avatar").click();
await page.locator("input[name='name']").fill(user.email);
await page.getByText("Update").click();
await page.waitForSelector("text=Settings updated successfully");
const response = await prisma.avatar.findUniqueOrThrow({
where: {
teamId_userId: {
userId: user.id,
teamId: 0,
},
},
});
// todo: remove this; ideally the organization-avatar is updated the moment
// 'Settings updated succesfully' is saved.
await page.waitForLoadState("networkidle");
await expect(await page.getByTestId("organization-avatar").innerHTML()).toContain(response.objectKey);
const urlResponse = await page.request.get(`/api/avatar/${response.objectKey}.png`, {
maxRedirects: 0,
});
await expect(urlResponse?.status()).toBe(200);
});
});
});
+29
View File
@@ -0,0 +1,29 @@
import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { JSDOM } from "jsdom";
import type { API, Messages } from "mailhog";
import { getEmailsReceivedByUser } from "../lib/testUtils";
export async function expectInvitationEmailToBeReceived(
page: Page,
emails: API | undefined,
userEmail: string,
subject: string,
returnLink?: string
) {
if (!emails) return null;
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(10000);
const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail });
expect(receivedEmails?.total).toBe(1);
const [firstReceivedEmail] = (receivedEmails as Messages).items;
expect(firstReceivedEmail.subject).toBe(subject);
if (!returnLink) return;
const dom = new JSDOM(firstReceivedEmail.html);
const anchor = dom.window.document.querySelector(`a[href*="${returnLink}"]`);
return anchor?.getAttribute("href");
}
@@ -0,0 +1,124 @@
import { expect } from "@playwright/test";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { test } from "../lib/fixtures";
import { localize } from "../lib/testUtils";
import { expectInvitationEmailToBeReceived } from "./expects";
test.describe.configure({ mode: "parallel" });
test.afterEach(async ({ users, emails, clipboard }) => {
clipboard.reset();
await users.deleteAll();
emails?.deleteAll();
});
test.describe("Team", () => {
test("Invitation (non verified)", async ({ browser, page, users, emails, clipboard }) => {
const t = await localize("en");
const teamOwner = await users.create(undefined, { hasTeam: true });
const { team } = await teamOwner.getFirstTeam();
await teamOwner.apiLogin();
await page.goto(`/settings/teams/${team.id}/members`);
await page.waitForLoadState("networkidle");
await test.step("To the team by email (external user)", async () => {
const invitedUserEmail = `rick_${Date.now()}@domain-${Date.now()}.com`;
await page.locator(`button:text("${t("add")}")`).click();
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
await page.locator(`button:text("${t("send_invite")}")`).click();
await page.waitForLoadState("networkidle");
const inviteLink = await expectInvitationEmailToBeReceived(
page,
emails,
invitedUserEmail,
`${team.name}'s admin invited you to join the team ${team.name} on Cal.com`,
"signup?token"
);
//Check newly invited member exists and is pending
await expect(
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
).toHaveCount(1);
// eslint-disable-next-line playwright/no-conditional-in-test
if (!inviteLink) return null;
// Follow invite link to new window
const context = await browser.newContext();
const newPage = await context.newPage();
await newPage.goto(inviteLink);
await newPage.waitForLoadState("networkidle");
// Check required fields
await newPage.locator("button[type=submit]").click();
await expect(newPage.locator('[data-testid="hint-error"]')).toHaveCount(3);
await newPage.locator("input[name=password]").fill(`P4ssw0rd!`);
await newPage.locator("button[type=submit]").click();
await newPage.waitForURL("/getting-started?from=signup");
await newPage.close();
await context.close();
// Check newly invited member is not pending anymore
await page.bringToFront();
await page.goto(`/settings/teams/${team.id}/members`);
await page.waitForLoadState("networkidle");
await expect(
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
).toHaveCount(0);
});
await test.step("To the team by invite link", async () => {
const user = await users.create({
email: `user-invite-${Date.now()}@domain.com`,
password: "P4ssw0rd!",
});
await page.locator(`button:text("${t("add")}")`).click();
await page.locator(`[data-testid="copy-invite-link-button"]`).click();
const inviteLink = await clipboard.get();
await page.waitForLoadState("networkidle");
const context = await browser.newContext();
const inviteLinkPage = await context.newPage();
await inviteLinkPage.goto(inviteLink);
await inviteLinkPage.waitForLoadState("domcontentloaded");
await inviteLinkPage.locator("button[type=submit]").click();
await expect(inviteLinkPage.locator('[data-testid="field-error"]')).toHaveCount(2);
await inviteLinkPage.locator("input[name=email]").fill(user.email);
await inviteLinkPage.locator("input[name=password]").fill(user.username || "P4ssw0rd!");
await inviteLinkPage.locator("button[type=submit]").click();
await inviteLinkPage.waitForURL(`${WEBAPP_URL}/teams**`);
});
});
test("Invitation (verified)", async ({ browser, page, users, emails }) => {
const t = await localize("en");
const teamOwner = await users.create({ name: `team-owner-${Date.now()}` }, { hasTeam: true });
const { team } = await teamOwner.getFirstTeam();
await teamOwner.apiLogin();
await page.goto(`/settings/teams/${team.id}/members`);
await page.waitForLoadState("networkidle");
await test.step("To the organization by email (internal user)", async () => {
const invitedUserEmail = `rick@example.com`;
await page.locator(`button:text("${t("add")}")`).click();
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
await page.locator(`button:text("${t("send_invite")}")`).click();
await page.waitForLoadState("networkidle");
await expectInvitationEmailToBeReceived(
page,
emails,
invitedUserEmail,
`${teamOwner.name} invited you to join the team ${team.name} on Cal.com`
);
await expect(
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
).toHaveCount(1);
});
});
});
+4 -4
View File
@@ -134,7 +134,7 @@ test.describe("Teams - NonOrg", () => {
// Anyone of the teammates could be the Host of the booking. // Anyone of the teammates could be the Host of the booking.
const chosenUser = await page.getByTestId("booking-host-name").textContent(); const chosenUser = await page.getByTestId("booking-host-name").textContent();
expect(chosenUser).not.toBeNull(); expect(chosenUser).not.toBeNull();
expect(teamMatesObj.some(({ name }) => name === chosenUser)).toBe(true); expect(teamMatesObj.concat([{ name: ownerObj.name }]).some(({ name }) => name === chosenUser)).toBe(true);
// TODO: Assert whether the user received an email // TODO: Assert whether the user received an email
}); });
@@ -351,7 +351,7 @@ test.describe("Teams - Org", () => {
await page.goto(`/team/${team.slug}/${teamEventSlug}`); await page.goto(`/team/${team.slug}/${teamEventSlug}`);
await expect(page.locator('[data-testid="404-page"]')).toBeVisible(); await expect(page.locator("text=This page could not be found")).toBeVisible();
await doOnOrgDomain( await doOnOrgDomain(
{ {
orgSlug: org.slug, orgSlug: org.slug,
@@ -370,7 +370,7 @@ test.describe("Teams - Org", () => {
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName); await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
// All the teammates should be in the booking // All the teammates should be in the booking
for (const teammate of teamMatesObj) { for (const teammate of teamMatesObj.concat([{ name: owner.name || "" }])) {
await expect(page.getByText(teammate.name, { exact: true })).toBeVisible(); await expect(page.getByText(teammate.name, { exact: true })).toBeVisible();
} }
} }
@@ -412,7 +412,7 @@ test.describe("Teams - Org", () => {
// Anyone of the teammates could be the Host of the booking. // Anyone of the teammates could be the Host of the booking.
const chosenUser = await page.getByTestId("booking-host-name").textContent(); const chosenUser = await page.getByTestId("booking-host-name").textContent();
expect(chosenUser).not.toBeNull(); expect(chosenUser).not.toBeNull();
expect(teamMatesObj.some(({ name }) => name === chosenUser)).toBe(true); expect(teamMatesObj.concat([{ name: ownerObj.name }]).some(({ name }) => name === chosenUser)).toBe(true);
// TODO: Assert whether the user received an email // TODO: Assert whether the user received an email
}); });
}); });
+3 -4
View File
@@ -103,12 +103,11 @@ test.describe("BOOKING_CREATED", async () => {
body.payload.videoCallData = dynamic; body.payload.videoCallData = dynamic;
body.payload.appsStatus = dynamic; body.payload.appsStatus = dynamic;
body.payload.metadata.videoCallUrl = dynamic; body.payload.metadata.videoCallUrl = dynamic;
expect(body).toMatchObject({ expect(body).toMatchObject({
triggerEvent: "BOOKING_CREATED", triggerEvent: "BOOKING_CREATED",
createdAt: "[redacted/dynamic]", createdAt: "[redacted/dynamic]",
payload: { payload: {
type: "30 min", type: "30-min",
title: "30 min between Nameless and Test Testson", title: "30 min between Nameless and Test Testson",
description: "", description: "",
additionalNotes: "", additionalNotes: "",
@@ -236,7 +235,7 @@ test.describe("BOOKING_REJECTED", async () => {
triggerEvent: "BOOKING_REJECTED", triggerEvent: "BOOKING_REJECTED",
createdAt: "[redacted/dynamic]", createdAt: "[redacted/dynamic]",
payload: { payload: {
type: "Opt in", type: "opt-in",
title: "Opt in between Nameless and Test Testson", title: "Opt in between Nameless and Test Testson",
customInputs: {}, customInputs: {},
startTime: "[redacted/dynamic]", startTime: "[redacted/dynamic]",
@@ -357,7 +356,7 @@ test.describe("BOOKING_REQUESTED", async () => {
triggerEvent: "BOOKING_REQUESTED", triggerEvent: "BOOKING_REQUESTED",
createdAt: "[redacted/dynamic]", createdAt: "[redacted/dynamic]",
payload: { payload: {
type: "Opt in", type: "opt-in",
title: "Opt in between Nameless and Test Testson", title: "Opt in between Nameless and Test Testson",
customInputs: {}, customInputs: {},
startTime: "[redacted/dynamic]", startTime: "[redacted/dynamic]",
+1 -1
View File
@@ -17,7 +17,7 @@ test.describe("Workflow tests", () => {
async ({ page, users }) => { async ({ page, users }) => {
const user = await users.create(); const user = await users.create();
const [eventType] = user.eventTypes; const [eventType] = user.eventTypes;
await user.login(); await user.apiLogin();
await page.goto(`/workflows`); await page.goto(`/workflows`);
await page.click('[data-testid="create-button"]'); await page.click('[data-testid="create-button"]');
@@ -849,6 +849,7 @@
"next_step": "تخطي الخطوة", "next_step": "تخطي الخطوة",
"prev_step": "الخطوة السابقة", "prev_step": "الخطوة السابقة",
"install": "تثبيت", "install": "تثبيت",
"install_paid_app": "اشتراك",
"installed": "تم التثبيت", "installed": "تم التثبيت",
"active_install_one": "{{count}} تثبيت نشط", "active_install_one": "{{count}} تثبيت نشط",
"active_install_other": "{{count}} تثبيت نشط", "active_install_other": "{{count}} تثبيت نشط",
@@ -1097,6 +1098,7 @@
"developer_documentation": "مستندات المطور", "developer_documentation": "مستندات المطور",
"get_in_touch": "تواصل معنا", "get_in_touch": "تواصل معنا",
"contact_support": "الاتصال بالدعم", "contact_support": "الاتصال بالدعم",
"community_support": "الدعم المجتمعي",
"feedback": "الملاحظات", "feedback": "الملاحظات",
"submitted_feedback": "نشكرك على ملاحظاتك!", "submitted_feedback": "نشكرك على ملاحظاتك!",
"feedback_error": "حدث خطأ عند إرسال الملاحظات", "feedback_error": "حدث خطأ عند إرسال الملاحظات",
@@ -268,6 +268,7 @@
"set_availability": "Nastavte, jak jste dostupní", "set_availability": "Nastavte, jak jste dostupní",
"availability_settings": "Nastavení dostupnosti", "availability_settings": "Nastavení dostupnosti",
"continue_without_calendar": "Pokračovat bez kalendáře", "continue_without_calendar": "Pokračovat bez kalendáře",
"continue_with": "Pokračovat přes {{appName}}",
"connect_your_calendar": "Připojit svůj kalendář", "connect_your_calendar": "Připojit svůj kalendář",
"connect_your_video_app": "Propojte své video aplikace", "connect_your_video_app": "Propojte své video aplikace",
"connect_your_video_app_instructions": "Propojte své video aplikace a používejte je ve svých typech událostí.", "connect_your_video_app_instructions": "Propojte své video aplikace a používejte je ve svých typech událostí.",
@@ -288,6 +289,8 @@
"when": "Kdy", "when": "Kdy",
"where": "Kde", "where": "Kde",
"add_to_calendar": "Přidat do kalendáře", "add_to_calendar": "Přidat do kalendáře",
"add_to_calendar_description": "Vyberte, kam se mají přidávat události, pokud jste zarezervováni.",
"add_events_to": "Přidat události do",
"add_another_calendar": "Přidat další kalendář", "add_another_calendar": "Přidat další kalendář",
"other": "Ostatní", "other": "Ostatní",
"email_sign_in_subject": "Váš přihlašovací odkaz pro {{appName}}", "email_sign_in_subject": "Váš přihlašovací odkaz pro {{appName}}",
@@ -422,6 +425,7 @@
"booking_created": "Rezervace vytvořena", "booking_created": "Rezervace vytvořena",
"booking_rejected": "Rezervace byla zamítnuta", "booking_rejected": "Rezervace byla zamítnuta",
"booking_requested": "Váš požadavek na rezervaci byl úspěšně odeslán", "booking_requested": "Váš požadavek na rezervaci byl úspěšně odeslán",
"booking_payment_initiated": "Platba za rezervaci zahájena",
"meeting_ended": "Schůzka skončila", "meeting_ended": "Schůzka skončila",
"form_submitted": "Formulář byl odeslán", "form_submitted": "Formulář byl odeslán",
"booking_paid": "Rezervace uhrazena", "booking_paid": "Rezervace uhrazena",
@@ -456,6 +460,7 @@
"no_event_types_have_been_setup": "Tento uživatel zatím nezaložil žádné typy událostí.", "no_event_types_have_been_setup": "Tento uživatel zatím nezaložil žádné typy událostí.",
"edit_logo": "Upravit logo", "edit_logo": "Upravit logo",
"upload_a_logo": "Nahrát logo", "upload_a_logo": "Nahrát logo",
"upload_logo": "Nahrát logo",
"remove_logo": "Odstranit logo", "remove_logo": "Odstranit logo",
"enable": "Povolit", "enable": "Povolit",
"code": "Kód", "code": "Kód",
@@ -568,6 +573,7 @@
"your_team_name": "Název vašeho týmu", "your_team_name": "Název vašeho týmu",
"team_updated_successfully": "Tým byl úspěšně aktualizován", "team_updated_successfully": "Tým byl úspěšně aktualizován",
"your_team_updated_successfully": "Váš tým byl úspěšně aktualizován.", "your_team_updated_successfully": "Váš tým byl úspěšně aktualizován.",
"your_org_updated_successfully": "Vaše organizace byla aktualizována.",
"about": "O aplikaci", "about": "O aplikaci",
"team_description": "Pár vět o vašem týmu. Objeví se na URL stránce vašeho týmu.", "team_description": "Pár vět o vašem týmu. Objeví se na URL stránce vašeho týmu.",
"org_description": "Několik vět o vaší organizaci. Zobrazí se na adrese URL vaší organizace.", "org_description": "Několik vět o vaší organizaci. Zobrazí se na adrese URL vaší organizace.",
@@ -599,6 +605,7 @@
"hide_book_a_team_member": "Skrýt tlačítko Rezervovat člena týmu", "hide_book_a_team_member": "Skrýt tlačítko Rezervovat člena týmu",
"hide_book_a_team_member_description": "Skrýt tlačítko Rezervovat člena týmu na veřejných stránkách.", "hide_book_a_team_member_description": "Skrýt tlačítko Rezervovat člena týmu na veřejných stránkách.",
"danger_zone": "Nebezpečná zóna", "danger_zone": "Nebezpečná zóna",
"account_deletion_cannot_be_undone": "Pozor. Odstranění účtu nelze vrátit.",
"back": "Zpět", "back": "Zpět",
"cancel": "Zrušit", "cancel": "Zrušit",
"cancel_all_remaining": "Zrušit vše zbývající", "cancel_all_remaining": "Zrušit vše zbývající",
@@ -688,6 +695,7 @@
"people": "Lidé", "people": "Lidé",
"your_email": "Váš e-mail", "your_email": "Váš e-mail",
"change_avatar": "Změnit avatar", "change_avatar": "Změnit avatar",
"upload_avatar": "Nahrát avatar",
"language": "Jazyk", "language": "Jazyk",
"timezone": "Časová zóna", "timezone": "Časová zóna",
"first_day_of_week": "První den v týdnu", "first_day_of_week": "První den v týdnu",
@@ -778,6 +786,7 @@
"disable_guests": "Zakázat hosty", "disable_guests": "Zakázat hosty",
"disable_guests_description": "Zakázat přidávání dalších hostů během rezervace.", "disable_guests_description": "Zakázat přidávání dalších hostů během rezervace.",
"private_link": "Vygenerovat soukromou adresu URL", "private_link": "Vygenerovat soukromou adresu URL",
"enable_private_url": "Zapnout soukromou adresu URL",
"private_link_label": "Soukromý odkaz", "private_link_label": "Soukromý odkaz",
"private_link_hint": "Váš soukromý odkaz se po každém použití obnoví", "private_link_hint": "Váš soukromý odkaz se po každém použití obnoví",
"copy_private_link": "Zkopírovat soukromý odkaz", "copy_private_link": "Zkopírovat soukromý odkaz",
@@ -840,6 +849,7 @@
"next_step": "Přeskočit krok", "next_step": "Přeskočit krok",
"prev_step": "Předchozí krok", "prev_step": "Předchozí krok",
"install": "Nainstalovat", "install": "Nainstalovat",
"install_paid_app": "Odebírat",
"installed": "Nainstalováno", "installed": "Nainstalováno",
"active_install_one": "Aktivní instalace: {{count}}", "active_install_one": "Aktivní instalace: {{count}}",
"active_install_other": "Aktivní instalace: {{count}}", "active_install_other": "Aktivní instalace: {{count}}",
@@ -1088,6 +1098,7 @@
"developer_documentation": "Dokumentace vývojáře", "developer_documentation": "Dokumentace vývojáře",
"get_in_touch": "Kontaktujte nás", "get_in_touch": "Kontaktujte nás",
"contact_support": "Kontaktujte podporu", "contact_support": "Kontaktujte podporu",
"community_support": "Podpora komunity",
"feedback": "Zpětná vazba", "feedback": "Zpětná vazba",
"submitted_feedback": "Děkujeme za vaši zpětnou vazbu!", "submitted_feedback": "Děkujeme za vaši zpětnou vazbu!",
"feedback_error": "Chyba při odesílání zpětné vazby", "feedback_error": "Chyba při odesílání zpětné vazby",
@@ -1213,6 +1224,7 @@
"organizer_name_variable": "Jméno organizátora", "organizer_name_variable": "Jméno organizátora",
"app_upgrade_description": "Pokud chcete použít tuto funkci, musíte provést aktualizaci na účet Pro.", "app_upgrade_description": "Pokud chcete použít tuto funkci, musíte provést aktualizaci na účet Pro.",
"invalid_number": "Neplatné telefonní číslo", "invalid_number": "Neplatné telefonní číslo",
"invalid_url_error_message": "Neplatná adresa URL pro {{label}}. Příklad URL: {{sampleUrl}}",
"navigate": "Navigace", "navigate": "Navigace",
"open": "Otevřít", "open": "Otevřít",
"close": "Zavřít", "close": "Zavřít",
@@ -1276,6 +1288,7 @@
"personal_cal_url": "Moje osobní adresa URL {{appName}}", "personal_cal_url": "Moje osobní adresa URL {{appName}}",
"bio_hint": "Několik vět o vás. Obsah se zobrazí se na vaší osobní stránce URL.", "bio_hint": "Několik vět o vás. Obsah se zobrazí se na vaší osobní stránce URL.",
"user_has_no_bio": "Tento uživatel zatím nepřidal životopis.", "user_has_no_bio": "Tento uživatel zatím nepřidal životopis.",
"bio": "Bio",
"delete_account_modal_title": "Odstranit účet", "delete_account_modal_title": "Odstranit účet",
"confirm_delete_account_modal": "Opravdu chcete odstranit svůj účet {{appName}}?", "confirm_delete_account_modal": "Opravdu chcete odstranit svůj účet {{appName}}?",
"delete_my_account": "Odstranit můj účet", "delete_my_account": "Odstranit můj účet",
@@ -1286,6 +1299,7 @@
"select_calendars": "Vyberte kalendáře, u kterých chcete kontrolovat konflikty v zájmu prevence dvojích rezervací.", "select_calendars": "Vyberte kalendáře, u kterých chcete kontrolovat konflikty v zájmu prevence dvojích rezervací.",
"check_for_conflicts": "Zkontrolovat konflikty", "check_for_conflicts": "Zkontrolovat konflikty",
"view_recordings": "Zobrazit nahrávky", "view_recordings": "Zobrazit nahrávky",
"check_for_recordings": "Zkontrolovat nahrávky",
"adding_events_to": "Přidání událostí do:", "adding_events_to": "Přidání událostí do:",
"follow_system_preferences": "Řídit se předvolbami systému", "follow_system_preferences": "Řídit se předvolbami systému",
"custom_brand_colors": "Vlastní barvy značky", "custom_brand_colors": "Vlastní barvy značky",
@@ -1530,6 +1544,7 @@
"problem_registering_domain": "Při registraci subdomény došlo k problému, zkuste to prosím znovu nebo kontaktujte správce", "problem_registering_domain": "Při registraci subdomény došlo k problému, zkuste to prosím znovu nebo kontaktujte správce",
"team_publish": "Zveřejnit tým", "team_publish": "Zveřejnit tým",
"number_text_notifications": "Telefonní číslo (SMS oznámení)", "number_text_notifications": "Telefonní číslo (SMS oznámení)",
"number_sms_notifications": "Telefonní číslo (SMS oznámení)",
"attendee_email_variable": "E-mail účastníka", "attendee_email_variable": "E-mail účastníka",
"attendee_email_info": "E-mail osoby provádějící rezervaci", "attendee_email_info": "E-mail osoby provádějící rezervaci",
"kbar_search_placeholder": "Zadejte příkaz nebo vyhledejte...", "kbar_search_placeholder": "Zadejte příkaz nebo vyhledejte...",
@@ -1594,6 +1609,7 @@
"options": "Možnosti", "options": "Možnosti",
"enter_option": "Zadejte možnost {{index}}", "enter_option": "Zadejte možnost {{index}}",
"add_an_option": "Přidat možnost", "add_an_option": "Přidat možnost",
"location_already_exists": "Toto místo již existuje. Vyberte prosím nové místo",
"radio": "Přepínač", "radio": "Přepínač",
"google_meet_warning": "Abyste mohli používat službu Google Meet, musíte jako cílový kalendář nastavit Kalendář Google", "google_meet_warning": "Abyste mohli používat službu Google Meet, musíte jako cílový kalendář nastavit Kalendář Google",
"individual": "Jedinec", "individual": "Jedinec",
@@ -1613,6 +1629,7 @@
"date_overrides_mark_all_day_unavailable_other": "Zadat nedostupnost ve vybraných datech", "date_overrides_mark_all_day_unavailable_other": "Zadat nedostupnost ve vybraných datech",
"date_overrides_add_btn": "Přidat změnu", "date_overrides_add_btn": "Přidat změnu",
"date_overrides_update_btn": "Aktualizovat změnu", "date_overrides_update_btn": "Aktualizovat změnu",
"date_successfully_added": "Přidána změna dnů",
"event_type_duplicate_copy_text": "{{slug}} – kopie", "event_type_duplicate_copy_text": "{{slug}} – kopie",
"set_as_default": "Nastavit jako výchozí", "set_as_default": "Nastavit jako výchozí",
"hide_eventtype_details": "Skrýt podrobnosti o typu události", "hide_eventtype_details": "Skrýt podrobnosti o typu události",
@@ -1639,6 +1656,7 @@
"minimum_round_robin_hosts_count": "Počet hostitelů, kteří se musí zúčastnit", "minimum_round_robin_hosts_count": "Počet hostitelů, kteří se musí zúčastnit",
"hosts": "Hostitelé", "hosts": "Hostitelé",
"upgrade_to_enable_feature": "K povolení této funkce je třeba vytvořit tým. Tým vytvoříte kliknutím.", "upgrade_to_enable_feature": "K povolení této funkce je třeba vytvořit tým. Tým vytvoříte kliknutím.",
"orgs_upgrade_to_enable_feature": "Pokud chcete zapnout tuto funkci, musíte upgradovat na náš tarif Enterprise.",
"new_attendee": "Nový účastník", "new_attendee": "Nový účastník",
"awaiting_approval": "Čeká na schválení", "awaiting_approval": "Čeká na schválení",
"requires_google_calendar": "Tato aplikace vyžaduje připojení ke Kalendáři Google", "requires_google_calendar": "Tato aplikace vyžaduje připojení ke Kalendáři Google",
@@ -1743,6 +1761,7 @@
"show_on_booking_page": "Zobrazit na stránce rezervace", "show_on_booking_page": "Zobrazit na stránce rezervace",
"get_started_zapier_templates": "Začněte používat šablony Zapier", "get_started_zapier_templates": "Začněte používat šablony Zapier",
"team_is_unpublished": "Tým {{team}} není zveřejněn", "team_is_unpublished": "Tým {{team}} není zveřejněn",
"org_is_unpublished_description": "Tento odkaz organizace není v současné době k dispozici. Kontaktujte prosím vlastníka organizace nebo ho požádejte o jeho zveřejnění.",
"team_is_unpublished_description": "Tento odkaz subjektu ({{entity}}) není v současné době k dispozici. Kontaktujte prosím vlastníka subjektu ({{entity}}) nebo ho požádejte o jeho zveřejnění.", "team_is_unpublished_description": "Tento odkaz subjektu ({{entity}}) není v současné době k dispozici. Kontaktujte prosím vlastníka subjektu ({{entity}}) nebo ho požádejte o jeho zveřejnění.",
"team_member": "Člen týmu", "team_member": "Člen týmu",
"a_routing_form": "Směrovací formulář", "a_routing_form": "Směrovací formulář",
@@ -1877,6 +1896,7 @@
"edit_invite_link": "Upravit nastavení odkazu", "edit_invite_link": "Upravit nastavení odkazu",
"invite_link_copied": "Odkaz pozvánky byl zkopírován", "invite_link_copied": "Odkaz pozvánky byl zkopírován",
"invite_link_deleted": "Odkaz pozvánky byl odstraněn", "invite_link_deleted": "Odkaz pozvánky byl odstraněn",
"api_key_deleted": "Klíč API odstraněn",
"invite_link_updated": "Nastavení odkazu pozvánky bylo uloženo", "invite_link_updated": "Nastavení odkazu pozvánky bylo uloženo",
"link_expires_after": "Platnost odkazů vyprší za...", "link_expires_after": "Platnost odkazů vyprší za...",
"one_day": "1 den", "one_day": "1 den",
@@ -2009,7 +2029,13 @@
"attendee_last_name_variable": "Příjmení účastníka", "attendee_last_name_variable": "Příjmení účastníka",
"attendee_first_name_info": "Jméno rezervující osoby", "attendee_first_name_info": "Jméno rezervující osoby",
"attendee_last_name_info": "Příjmení rezervující osoby", "attendee_last_name_info": "Příjmení rezervující osoby",
"your_monthly_digest": "Váš měsíční přehled",
"member_name": "Jméno člena",
"most_popular_events": "Nejoblíbenější události",
"summary_of_events_for_your_team_for_the_last_30_days": "Zde je přehled oblíbených událostí vašeho týmu {{teamName}} za posledních 30 dní",
"me": "Já", "me": "Já",
"monthly_digest_email": "E-mail s měsíčním přehledem",
"monthly_digest_email_for_teams": "Měsíční e-mail s přehledem pro týmy",
"verify_team_tooltip": "Proveďte ověření svého týmu a zapněte odesílání zpráv účastníkům", "verify_team_tooltip": "Proveďte ověření svého týmu a zapněte odesílání zpráv účastníkům",
"member_removed": "Člen odstraněn", "member_removed": "Člen odstraněn",
"my_availability": "Moje dostupnost", "my_availability": "Moje dostupnost",
@@ -2039,13 +2065,41 @@
"team_no_event_types": "Tento tým nemá žádné typy událostí", "team_no_event_types": "Tento tým nemá žádné typy událostí",
"seat_options_doesnt_multiple_durations": "Volba místa nepodporuje více dob trvání", "seat_options_doesnt_multiple_durations": "Volba místa nepodporuje více dob trvání",
"include_calendar_event": "Zahrnout událost kalendáře", "include_calendar_event": "Zahrnout událost kalendáře",
"oAuth": "OAuth",
"recently_added": "Nedávno přidáno", "recently_added": "Nedávno přidáno",
"no_members_found": "Nenalezeni žádní členové", "no_members_found": "Nenalezeni žádní členové",
"event_setup_length_error": "Nastavení události: Doba trvání musí být alespoň 1 minuta.", "event_setup_length_error": "Nastavení události: Doba trvání musí být alespoň 1 minuta.",
"availability_schedules": "Plány dostupnosti", "availability_schedules": "Plány dostupnosti",
"unauthorized": "Neautorizováno",
"access_cal_account": "{{clientName}} žádá o přístup k vašemu účtu {{appName}}",
"select_account_team": "Vyberte účet nebo tým",
"allow_client_to": "To umožní klientovi {{clientName}}",
"associate_with_cal_account": "Přiřadit vám vaše osobní údaje z klienta {{clientName}}",
"see_personal_info": "Zobrazit vaše osobní údaje, včetně všech osobních údajů, které jste zveřejnili",
"see_primary_email_address": "Zobrazit vaši primární e-mailovou adresu",
"connect_installed_apps": "Připojit se k vašim nainstalovaným aplikacím",
"access_event_type": "Číst, upravovat a odstraňovat vaše typy událostí",
"access_availability": "Číst, upravovat a odstraňovat vaši dostupnost",
"access_bookings": "Číst, upravovat a odstraňovat rezervace",
"allow_client_to_do": "Chcete povolit klientovi {{clientName}} provádět uvedené akce?",
"oauth_access_information": "Kliknutím na tlačítko Povolit této aplikaci umožníte používat vaše údaje v souladu s podmínkami služby a zásadami ochrany osobních údajů. Přístup k aplikaci {{appName}} můžete odebrat přes App Store.",
"allow": "Povolit",
"view_only_edit_availability_not_onboarded": "Tento uživatel ještě nedokončil onboarding. Dokud nedokončí onboarding, dostupnost nebude možné nastavit.", "view_only_edit_availability_not_onboarded": "Tento uživatel ještě nedokončil onboarding. Dokud nedokončí onboarding, dostupnost nebude možné nastavit.",
"view_only_edit_availability": "Právě máte zobrazenou dostupnost tohoto uživatele. Upravovat lze pouze vlastní dostupnost.", "view_only_edit_availability": "Právě máte zobrazenou dostupnost tohoto uživatele. Upravovat lze pouze vlastní dostupnost.",
"you_can_override_calendar_in_advanced_tab": "Tuto možnost můžete zrušit pro každou událost zvlášť v pokročilém nastavení jednotlivých typů událostí.",
"edit_users_availability": "Upravte dostupnost uživatele: {{username}}", "edit_users_availability": "Upravte dostupnost uživatele: {{username}}",
"resend_invitation": "Znovu odeslat pozvánku",
"invitation_resent": "Pozvánka byla odeslána znovu.",
"add_client": "Přidat klienta",
"copy_client_secret_info": "Po zkopírování již nebude možné tajný klíč zobrazit",
"add_new_client": "Přidat nového klienta",
"this_app_is_not_setup_already": "Tato aplikace ještě nebyla nastavena",
"as_csv": "jako CSV",
"overlay_my_calendar": "Překryv mého kalendáře",
"overlay_my_calendar_toc": "Připojením ke svému kalendáři přijímáte naše zásady ochrany osobních údajů a podmínky používání. Přístup můžete kdykoli odvolat.",
"view_overlay_calendar_events": "Zobrazení událostí v kalendáři, aby se zabránilo kolidujícím rezervacím.",
"lock_timezone_toggle_on_booking_page": "Uzamčení časového pásma na stránce rezervace",
"description_lock_timezone_toggle_on_booking_page": "Uzamčení časového pásma na stránce rezervace (užitečné pro osobní události).",
"extensive_whitelabeling": "Vyhrazená podpora zaškolovací a inženýrská podpora", "extensive_whitelabeling": "Vyhrazená podpora zaškolovací a inženýrská podpora",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
} }
@@ -717,6 +717,7 @@
"next_step": "Spring trin over", "next_step": "Spring trin over",
"prev_step": "Forrige trin", "prev_step": "Forrige trin",
"install": "Installér", "install": "Installér",
"install_paid_app": "Abonnér",
"installed": "Installeret", "installed": "Installeret",
"active_install_one": "{{count}} aktiv installation", "active_install_one": "{{count}} aktiv installation",
"active_install_other": "{{count}} aktive installationer", "active_install_other": "{{count}} aktive installationer",
@@ -268,6 +268,7 @@
"set_availability": "Verfügbarkeit festlegen", "set_availability": "Verfügbarkeit festlegen",
"availability_settings": "Verfügbarkeitseinstellungen", "availability_settings": "Verfügbarkeitseinstellungen",
"continue_without_calendar": "Ohne Kalender fortfahren", "continue_without_calendar": "Ohne Kalender fortfahren",
"continue_with": "Mit {{appName}} fortfahren",
"connect_your_calendar": "Kalender verbinden", "connect_your_calendar": "Kalender verbinden",
"connect_your_video_app": "Verbinden Sie Ihre Video-Apps", "connect_your_video_app": "Verbinden Sie Ihre Video-Apps",
"connect_your_video_app_instructions": "Verbinden Sie Ihre Video-Apps, um sie für Ihre Termintypen zu verwenden.", "connect_your_video_app_instructions": "Verbinden Sie Ihre Video-Apps, um sie für Ihre Termintypen zu verwenden.",
@@ -289,6 +290,7 @@
"where": "Wo", "where": "Wo",
"add_to_calendar": "Zum Kalender hinzufügen", "add_to_calendar": "Zum Kalender hinzufügen",
"add_to_calendar_description": "Legen Sie fest, wo neue Termine hinzugefügt werden sollen, wenn Sie gebucht werden.", "add_to_calendar_description": "Legen Sie fest, wo neue Termine hinzugefügt werden sollen, wenn Sie gebucht werden.",
"add_events_to": "Termine hinzufügen zu",
"add_another_calendar": "Einen weiteren Kalender hinzufügen", "add_another_calendar": "Einen weiteren Kalender hinzufügen",
"other": "Sonstige", "other": "Sonstige",
"email_sign_in_subject": "Ihr Anmelde-Link für {{appName}}", "email_sign_in_subject": "Ihr Anmelde-Link für {{appName}}",
@@ -423,6 +425,7 @@
"booking_created": "Termin erstellt", "booking_created": "Termin erstellt",
"booking_rejected": "Termin abgelehnt", "booking_rejected": "Termin abgelehnt",
"booking_requested": "Buchung angefragt", "booking_requested": "Buchung angefragt",
"booking_payment_initiated": "Buchungszahlung eingeleitet",
"meeting_ended": "Meeting beendet", "meeting_ended": "Meeting beendet",
"form_submitted": "Formular gesendet", "form_submitted": "Formular gesendet",
"booking_paid": "Buchung bezahlt", "booking_paid": "Buchung bezahlt",
@@ -457,6 +460,7 @@
"no_event_types_have_been_setup": "Dieser Benutzer hat noch keine Termintypen eingerichtet.", "no_event_types_have_been_setup": "Dieser Benutzer hat noch keine Termintypen eingerichtet.",
"edit_logo": "Logo bearbeiten", "edit_logo": "Logo bearbeiten",
"upload_a_logo": "Logo hochladen", "upload_a_logo": "Logo hochladen",
"upload_logo": "Logo hochladen",
"remove_logo": "Logos entfernen", "remove_logo": "Logos entfernen",
"enable": "Aktivieren", "enable": "Aktivieren",
"code": "Code", "code": "Code",
@@ -569,6 +573,7 @@
"your_team_name": "Ihr Teamname", "your_team_name": "Ihr Teamname",
"team_updated_successfully": "Team erfolgreich aktualisiert", "team_updated_successfully": "Team erfolgreich aktualisiert",
"your_team_updated_successfully": "Ihr Team wurde erfolgreich aktualisiert.", "your_team_updated_successfully": "Ihr Team wurde erfolgreich aktualisiert.",
"your_org_updated_successfully": "Ihre Org wurde erfolgreich aktualisiert.",
"about": "Beschreibung", "about": "Beschreibung",
"team_description": "Ein paar Sätze über Ihr Team auf der öffentlichen Teamseite.", "team_description": "Ein paar Sätze über Ihr Team auf der öffentlichen Teamseite.",
"org_description": "Ein paar Sätze zu Ihrer Organisation. Dies wird auf der URL-Seite Ihrer Organisation erscheinen.", "org_description": "Ein paar Sätze zu Ihrer Organisation. Dies wird auf der URL-Seite Ihrer Organisation erscheinen.",
@@ -781,6 +786,7 @@
"disable_guests": "Gäste deaktivieren", "disable_guests": "Gäste deaktivieren",
"disable_guests_description": "Das Hinzufügen zusätzlicher Gäste deaktivieren.", "disable_guests_description": "Das Hinzufügen zusätzlicher Gäste deaktivieren.",
"private_link": "Privaten Link generieren", "private_link": "Privaten Link generieren",
"enable_private_url": "Private URL aktivieren",
"private_link_label": "Privater Link", "private_link_label": "Privater Link",
"private_link_hint": "Ihr privater Link wird nach jeder Nutzung neu generiert", "private_link_hint": "Ihr privater Link wird nach jeder Nutzung neu generiert",
"copy_private_link": "Privaten Link kopieren", "copy_private_link": "Privaten Link kopieren",
@@ -843,6 +849,7 @@
"next_step": "Schritt überspringen", "next_step": "Schritt überspringen",
"prev_step": "Vorheriger Schritt", "prev_step": "Vorheriger Schritt",
"install": "Installieren", "install": "Installieren",
"install_paid_app": "Abonnieren",
"installed": "Installiert", "installed": "Installiert",
"active_install_one": "{{count}} aktive Installation", "active_install_one": "{{count}} aktive Installation",
"active_install_other": "{{count}} aktive Installationen", "active_install_other": "{{count}} aktive Installationen",
@@ -1091,6 +1098,7 @@
"developer_documentation": "Entwickler-Dokumentation", "developer_documentation": "Entwickler-Dokumentation",
"get_in_touch": "Kontakt aufnehmen", "get_in_touch": "Kontakt aufnehmen",
"contact_support": "Support kontaktieren", "contact_support": "Support kontaktieren",
"community_support": "Community Support",
"feedback": "Feedback", "feedback": "Feedback",
"submitted_feedback": "Vielen Dank für Ihr Feedback!", "submitted_feedback": "Vielen Dank für Ihr Feedback!",
"feedback_error": "Fehler beim Senden des Feedbacks", "feedback_error": "Fehler beim Senden des Feedbacks",
@@ -1216,6 +1224,7 @@
"organizer_name_variable": "Organisator Name", "organizer_name_variable": "Organisator Name",
"app_upgrade_description": "Um diese Funktion nutzen zu können, müssen Sie ein Upgrade auf einen Pro-Account durchführen.", "app_upgrade_description": "Um diese Funktion nutzen zu können, müssen Sie ein Upgrade auf einen Pro-Account durchführen.",
"invalid_number": "Ungültige Telefonnummer", "invalid_number": "Ungültige Telefonnummer",
"invalid_url_error_message": "Ungültige URL für {{label}}. Beispiel-URL: {{sampleUrl}}",
"navigate": "Navigieren", "navigate": "Navigieren",
"open": "Öffnen", "open": "Öffnen",
"close": "Schließen", "close": "Schließen",
@@ -1290,6 +1299,7 @@
"select_calendars": "Wählen Sie aus, in welchen Kalendern Sie nach Konflikten suchen wollen, um Doppelbuchungen zu vermeiden.", "select_calendars": "Wählen Sie aus, in welchen Kalendern Sie nach Konflikten suchen wollen, um Doppelbuchungen zu vermeiden.",
"check_for_conflicts": "Auf Konflikte prüfen", "check_for_conflicts": "Auf Konflikte prüfen",
"view_recordings": "Aufnahmen anzeigen", "view_recordings": "Aufnahmen anzeigen",
"check_for_recordings": "Nach Aufnahmen suchen",
"adding_events_to": "Termine hinzufügen zu", "adding_events_to": "Termine hinzufügen zu",
"follow_system_preferences": "Systemeinstellungen folgen", "follow_system_preferences": "Systemeinstellungen folgen",
"custom_brand_colors": "Eigene Marken-Farben", "custom_brand_colors": "Eigene Marken-Farben",
@@ -1599,6 +1609,7 @@
"options": "Optionen", "options": "Optionen",
"enter_option": "Option {{index}} eingeben", "enter_option": "Option {{index}} eingeben",
"add_an_option": "Option hinzufügen", "add_an_option": "Option hinzufügen",
"location_already_exists": "Dieser Standort existiert bereits. Bitte wählen Sie einen neuen Standort",
"radio": "Radio", "radio": "Radio",
"google_meet_warning": "Um Google Meet nutzen zu können, müssen Sie Ihren Zielkalender zu einem Google Calendar ändern", "google_meet_warning": "Um Google Meet nutzen zu können, müssen Sie Ihren Zielkalender zu einem Google Calendar ändern",
"individual": "Person", "individual": "Person",
@@ -1618,6 +1629,7 @@
"date_overrides_mark_all_day_unavailable_other": "An ausgewählten Daten als „nicht verfügbar“ markieren", "date_overrides_mark_all_day_unavailable_other": "An ausgewählten Daten als „nicht verfügbar“ markieren",
"date_overrides_add_btn": "Überschreibung hinzufügen", "date_overrides_add_btn": "Überschreibung hinzufügen",
"date_overrides_update_btn": "Überschreiben aktualisieren", "date_overrides_update_btn": "Überschreiben aktualisieren",
"date_successfully_added": "Datumsüberbrückung erfolgreich hinzugefügt",
"event_type_duplicate_copy_text": "{{slug}}-Kopie", "event_type_duplicate_copy_text": "{{slug}}-Kopie",
"set_as_default": "Als Standard festlegen", "set_as_default": "Als Standard festlegen",
"hide_eventtype_details": "Ereignistyp-Einzelheiten ausblenden", "hide_eventtype_details": "Ereignistyp-Einzelheiten ausblenden",
@@ -2062,6 +2074,7 @@
"access_cal_account": "{{clientName}} möchte auf Ihr {{appName}} Konto zugreifen", "access_cal_account": "{{clientName}} möchte auf Ihr {{appName}} Konto zugreifen",
"select_account_team": "Konto oder Team auswählen", "select_account_team": "Konto oder Team auswählen",
"allow_client_to": "Dies wird {{clientName}} erlauben", "allow_client_to": "Dies wird {{clientName}} erlauben",
"associate_with_cal_account": "Verknüpfen Sie sich mit Ihren persönlichen Daten von {{clientName}}",
"see_personal_info": "Ihre persönlichen Daten einzusehen, einschließlich persönlicher Informationen, die Sie öffentlich zugänglich gemacht haben", "see_personal_info": "Ihre persönlichen Daten einzusehen, einschließlich persönlicher Informationen, die Sie öffentlich zugänglich gemacht haben",
"see_primary_email_address": "Ihre primäre E-Mail-Adresse einzusehen", "see_primary_email_address": "Ihre primäre E-Mail-Adresse einzusehen",
"connect_installed_apps": "Sich mit Ihren installierten Apps zu verbinden", "connect_installed_apps": "Sich mit Ihren installierten Apps zu verbinden",
@@ -2069,13 +2082,24 @@
"access_availability": "Lesen, Bearbeiten, Löschen Ihrer Verfügbarkeiten", "access_availability": "Lesen, Bearbeiten, Löschen Ihrer Verfügbarkeiten",
"access_bookings": "Lesen, Bearbeiten, Löschen Ihrer Termine", "access_bookings": "Lesen, Bearbeiten, Löschen Ihrer Termine",
"allow_client_to_do": "{{clientName}} zulassen, dies zu tun?", "allow_client_to_do": "{{clientName}} zulassen, dies zu tun?",
"oauth_access_information": "Indem Sie auf „Erlauben“ klicken, erlauben Sie dieser App, Ihre Informationen gemäß ihrer Nutzungsbedingungen und Datenschutzrichtlinien zu verwenden. Sie können den Zugriff im {{appName}} App Store aufheben.",
"allow": "Zulassen", "allow": "Zulassen",
"view_only_edit_availability_not_onboarded": "Dieser Benutzer hat das Onboarding noch nicht abgeschlossen. Sie können seine Verfügbarkeit erst festlegen, wenn er das Onboarding abgeschlossen hat.", "view_only_edit_availability_not_onboarded": "Dieser Benutzer hat das Onboarding noch nicht abgeschlossen. Sie können seine Verfügbarkeit erst festlegen, wenn er das Onboarding abgeschlossen hat.",
"view_only_edit_availability": "Sie sehen die Verfügbarkeit dieses Benutzers. Sie können nur Ihre eigene Verfügbarkeit bearbeiten.", "view_only_edit_availability": "Sie sehen die Verfügbarkeit dieses Benutzers. Sie können nur Ihre eigene Verfügbarkeit bearbeiten.",
"you_can_override_calendar_in_advanced_tab": "Sie können dies in den erweiterten Einstellungen pro Termin für jeden Termintyp überschreiben.",
"edit_users_availability": "Benutzerverfügbarkeit bearbeiten: {{username}}", "edit_users_availability": "Benutzerverfügbarkeit bearbeiten: {{username}}",
"resend_invitation": "Einladung erneut senden", "resend_invitation": "Einladung erneut senden",
"invitation_resent": "Die Einladung wurde erneut gesendet.", "invitation_resent": "Die Einladung wurde erneut gesendet.",
"add_client": "Kunde hinzufügen",
"copy_client_secret_info": "Nach dem Kopieren des Geheimnisses können Sie es nicht mehr ansehen",
"add_new_client": "Neuen Kunden hinzufügen",
"this_app_is_not_setup_already": "Diese App wurde noch nicht eingerichtet", "this_app_is_not_setup_already": "Diese App wurde noch nicht eingerichtet",
"as_csv": "als CSV",
"overlay_my_calendar": "Meinen Kalender überlagern",
"overlay_my_calendar_toc": "Durch das Verbinden mit Ihrem Kalender akzeptieren Sie unsere Datenschutzerklärung und Nutzungsbedingungen. Sie können den Zugriff jederzeit widerrufen.",
"view_overlay_calendar_events": "Sehen Sie sich Ihre Kalendertermine an, um Buchungskonflikte zu vermeiden.",
"lock_timezone_toggle_on_booking_page": "Zeitzone auf der Buchungsseite sperren",
"description_lock_timezone_toggle_on_booking_page": "Um die Zeitzone auf der Buchungsseite zu sperren, nützlich für Termine in Person.",
"extensive_whitelabeling": "Dedizierte Onboarding- und Engineeringsupport", "extensive_whitelabeling": "Dedizierte Onboarding- und Engineeringsupport",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
} }
@@ -56,6 +56,17 @@
"a_refund_failed": "A refund failed", "a_refund_failed": "A refund failed",
"awaiting_payment_subject": "Awaiting Payment: {{title}} on {{date}}", "awaiting_payment_subject": "Awaiting Payment: {{title}} on {{date}}",
"meeting_awaiting_payment": "Your meeting is awaiting payment", "meeting_awaiting_payment": "Your meeting is awaiting payment",
"dark_theme_contrast_error":"Dark Theme color doesn't pass contrast check. We recommend you change this colour so your buttons will be more visible.",
"light_theme_contrast_error":"Light Theme color doesn't pass contrast check. We recommend you change this colour so your buttons will be more visible.",
"payment_not_created_error": "Payment could not be created",
"couldnt_charge_card_error": "Could not charge card for Payment",
"no_available_users_found_error": "No available users found. Could you try another time slot?",
"request_body_end_time_internal_error": "Internal Error. Request body does not contain end time",
"create_calendar_event_error": "Unable to create Calendar event in Organizer's calendar",
"update_calendar_event_error": "Unable to update Calendar event.",
"delete_calendar_event_error": "Unable to delete Calendar event.",
"already_signed_up_for_this_booking_error": "You are already signed up for this booking.",
"hosts_unavailable_for_booking": "Some of the hosts are unavailable for booking.",
"help": "Help", "help": "Help",
"price": "Price", "price": "Price",
"paid": "Paid", "paid": "Paid",
@@ -606,6 +617,7 @@
"hide_book_a_team_member_description": "Hide Book a Team Member Button from your public pages.", "hide_book_a_team_member_description": "Hide Book a Team Member Button from your public pages.",
"danger_zone": "Danger zone", "danger_zone": "Danger zone",
"account_deletion_cannot_be_undone":"Be Careful. Account deletion cannot be undone.", "account_deletion_cannot_be_undone":"Be Careful. Account deletion cannot be undone.",
"team_deletion_cannot_be_undone":"Be Careful. Team deletion cannot be undone",
"back": "Back", "back": "Back",
"cancel": "Cancel", "cancel": "Cancel",
"cancel_all_remaining": "Cancel all remaining", "cancel_all_remaining": "Cancel all remaining",
@@ -849,6 +861,8 @@
"next_step": "Skip step", "next_step": "Skip step",
"prev_step": "Prev step", "prev_step": "Prev step",
"install": "Install", "install": "Install",
"install_paid_app": "Subscribe",
"start_paid_trial": "Start free Trial",
"installed": "Installed", "installed": "Installed",
"active_install_one": "{{count}} active install", "active_install_one": "{{count}} active install",
"active_install_other": "{{count}} active installs", "active_install_other": "{{count}} active installs",
@@ -1097,6 +1111,8 @@
"developer_documentation": "Developer Documentation", "developer_documentation": "Developer Documentation",
"get_in_touch": "Get in touch", "get_in_touch": "Get in touch",
"contact_support": "Contact Support", "contact_support": "Contact Support",
"premium_support": "Premium Support",
"community_support": "Community Support",
"feedback": "Feedback", "feedback": "Feedback",
"submitted_feedback": "Thank you for your feedback!", "submitted_feedback": "Thank you for your feedback!",
"feedback_error": "Error sending feedback", "feedback_error": "Error sending feedback",
@@ -1361,6 +1377,7 @@
"event_name_info": "The event type name", "event_name_info": "The event type name",
"event_date_info": "The event date", "event_date_info": "The event date",
"event_time_info": "The event start time", "event_time_info": "The event start time",
"event_type_not_found": "EventType not Found",
"location_info": "The location of the event", "location_info": "The location of the event",
"additional_notes_info": "The additional notes of booking", "additional_notes_info": "The additional notes of booking",
"attendee_name_info": "The person booking's name", "attendee_name_info": "The person booking's name",
@@ -1401,6 +1418,7 @@
"slot_length": "Slot length", "slot_length": "Slot length",
"booking_appearance": "Booking Appearance", "booking_appearance": "Booking Appearance",
"appearance_team_description": "Manage settings for your team's booking appearance", "appearance_team_description": "Manage settings for your team's booking appearance",
"appearance_org_description": "Manage settings for your organization's booking appearance",
"only_owner_change": "Only the owner of this team can make changes to the team's booking ", "only_owner_change": "Only the owner of this team can make changes to the team's booking ",
"team_disable_cal_branding_description": "Removes any {{appName}} related brandings, i.e. 'Powered by {{appName}}'", "team_disable_cal_branding_description": "Removes any {{appName}} related brandings, i.e. 'Powered by {{appName}}'",
"invited_by_team": "{{teamName}} has invited you to join their team as a {{role}}", "invited_by_team": "{{teamName}} has invited you to join their team as a {{role}}",
@@ -2096,8 +2114,14 @@
"overlay_my_calendar":"Overlay my calendar", "overlay_my_calendar":"Overlay my calendar",
"overlay_my_calendar_toc":"By connecting to your calendar, you accept our privacy policy and terms of use. You may revoke access at any time.", "overlay_my_calendar_toc":"By connecting to your calendar, you accept our privacy policy and terms of use. You may revoke access at any time.",
"view_overlay_calendar_events":"View your calendar events to prevent clashed booking.", "view_overlay_calendar_events":"View your calendar events to prevent clashed booking.",
"troubleshooting":"Troubleshooting",
"calendars_were_checking_for_conflicts":"Calendars we’re checking for conflicts",
"availabilty_schedules":"Availability schedules",
"manage_calendars":"Manage calendars",
"manage_availability_schedules":"Manage availability schedules",
"lock_timezone_toggle_on_booking_page": "Lock timezone on booking page", "lock_timezone_toggle_on_booking_page": "Lock timezone on booking page",
"description_lock_timezone_toggle_on_booking_page" : "To lock the timezone on booking page, useful for in-person events.", "description_lock_timezone_toggle_on_booking_page" : "To lock the timezone on booking page, useful for in-person events.",
"install_calendar":"Install Calendar",
"branded_subdomain": "Branded Subdomain", "branded_subdomain": "Branded Subdomain",
"branded_subdomain_description": "Get your own branded subdomain, such as acme.cal.com", "branded_subdomain_description": "Get your own branded subdomain, such as acme.cal.com",
"org_insights": "Organization-wide Insights", "org_insights": "Organization-wide Insights",
@@ -268,6 +268,7 @@
"set_availability": "Establecer Disponibilidad", "set_availability": "Establecer Disponibilidad",
"availability_settings": "Configuración de disponibilidad", "availability_settings": "Configuración de disponibilidad",
"continue_without_calendar": "Continuar sin calendario", "continue_without_calendar": "Continuar sin calendario",
"continue_with": "Continuar con {{appName}}",
"connect_your_calendar": "Conecta tu calendario", "connect_your_calendar": "Conecta tu calendario",
"connect_your_video_app": "Conecte sus aplicaciones favoritas", "connect_your_video_app": "Conecte sus aplicaciones favoritas",
"connect_your_video_app_instructions": "Conecte sus aplicaciones de vídeo para usarlas en sus tipos de eventos.", "connect_your_video_app_instructions": "Conecte sus aplicaciones de vídeo para usarlas en sus tipos de eventos.",
@@ -288,6 +289,8 @@
"when": "Cuándo", "when": "Cuándo",
"where": "Donde", "where": "Donde",
"add_to_calendar": "Añadir al Calendario", "add_to_calendar": "Añadir al Calendario",
"add_to_calendar_description": "Seleccione dónde se añadirán los eventos cuando haya reservado.",
"add_events_to": "Agregar eventos a",
"add_another_calendar": "Añadir otro calendario", "add_another_calendar": "Añadir otro calendario",
"other": "Otro", "other": "Otro",
"email_sign_in_subject": "Su enlace de inicio de sesión para {{appName}}", "email_sign_in_subject": "Su enlace de inicio de sesión para {{appName}}",
@@ -422,6 +425,7 @@
"booking_created": "Reserva Creada", "booking_created": "Reserva Creada",
"booking_rejected": "Reserva rechazada", "booking_rejected": "Reserva rechazada",
"booking_requested": "Reserva solicitada", "booking_requested": "Reserva solicitada",
"booking_payment_initiated": "Pago de la reserva iniciado",
"meeting_ended": "Reunión finalizada", "meeting_ended": "Reunión finalizada",
"form_submitted": "Formulario enviado", "form_submitted": "Formulario enviado",
"booking_paid": "Reserva pagada", "booking_paid": "Reserva pagada",
@@ -456,6 +460,7 @@
"no_event_types_have_been_setup": "Este usuario aún no ha configurado ningún tipo de evento.", "no_event_types_have_been_setup": "Este usuario aún no ha configurado ningún tipo de evento.",
"edit_logo": "Cambiar la marca", "edit_logo": "Cambiar la marca",
"upload_a_logo": "Subir una marca", "upload_a_logo": "Subir una marca",
"upload_logo": "Cargar logo",
"remove_logo": "Quitar logo", "remove_logo": "Quitar logo",
"enable": "Habilitar", "enable": "Habilitar",
"code": "Código", "code": "Código",
@@ -568,6 +573,7 @@
"your_team_name": "Nombre de tu Equipo", "your_team_name": "Nombre de tu Equipo",
"team_updated_successfully": "Equipo Actualizado Correctamente", "team_updated_successfully": "Equipo Actualizado Correctamente",
"your_team_updated_successfully": "Tu equipo ha sido actualizado con éxito.", "your_team_updated_successfully": "Tu equipo ha sido actualizado con éxito.",
"your_org_updated_successfully": "Su organización se ha actualizado correctamente.",
"about": "Acerca de", "about": "Acerca de",
"team_description": "Comentarios sobre tu equipo. Esta información aparecerá en la página de la URL de tu equipo.", "team_description": "Comentarios sobre tu equipo. Esta información aparecerá en la página de la URL de tu equipo.",
"org_description": "Algunas frases sobre su organización. Esto aparecerá en la página de la URL de su organización.", "org_description": "Algunas frases sobre su organización. Esto aparecerá en la página de la URL de su organización.",
@@ -599,6 +605,7 @@
"hide_book_a_team_member": "Ocultar el botón Reservar un miembro del equipo", "hide_book_a_team_member": "Ocultar el botón Reservar un miembro del equipo",
"hide_book_a_team_member_description": "Ocultar el botón Reservar un miembro del equipo de sus páginas públicas.", "hide_book_a_team_member_description": "Ocultar el botón Reservar un miembro del equipo de sus páginas públicas.",
"danger_zone": "Paso definitivo", "danger_zone": "Paso definitivo",
"account_deletion_cannot_be_undone": "Tenga cuidado. La eliminación de la cuenta no se puede deshacer.",
"back": "Atrás", "back": "Atrás",
"cancel": "Cancelar", "cancel": "Cancelar",
"cancel_all_remaining": "Cancelar todos los restantes", "cancel_all_remaining": "Cancelar todos los restantes",
@@ -688,6 +695,7 @@
"people": "Personas", "people": "Personas",
"your_email": "Tu Email", "your_email": "Tu Email",
"change_avatar": "Cambiar Avatar", "change_avatar": "Cambiar Avatar",
"upload_avatar": "Cargar avatar",
"language": "Lenguaje", "language": "Lenguaje",
"timezone": "Zona Horaria", "timezone": "Zona Horaria",
"first_day_of_week": "Primer dia de la semana", "first_day_of_week": "Primer dia de la semana",
@@ -778,6 +786,7 @@
"disable_guests": "Desactivar Invitados", "disable_guests": "Desactivar Invitados",
"disable_guests_description": "Desactiva agregar invitados adicionales al hacer la reserva.", "disable_guests_description": "Desactiva agregar invitados adicionales al hacer la reserva.",
"private_link": "Generar una URL privada", "private_link": "Generar una URL privada",
"enable_private_url": "Activar URL privada",
"private_link_label": "Enlace privado", "private_link_label": "Enlace privado",
"private_link_hint": "Su enlace privado se regenera después de cada uso", "private_link_hint": "Su enlace privado se regenera después de cada uso",
"copy_private_link": "Copiar enlace privado", "copy_private_link": "Copiar enlace privado",
@@ -840,6 +849,7 @@
"next_step": "Saltar paso", "next_step": "Saltar paso",
"prev_step": "Paso anterior", "prev_step": "Paso anterior",
"install": "Instalar", "install": "Instalar",
"install_paid_app": "Suscribirse",
"installed": "Instalado", "installed": "Instalado",
"active_install_one": "{{count}} instalación activa", "active_install_one": "{{count}} instalación activa",
"active_install_other": "{{count}} instalaciones activas", "active_install_other": "{{count}} instalaciones activas",
@@ -1088,6 +1098,7 @@
"developer_documentation": "Documentación del desarrollador", "developer_documentation": "Documentación del desarrollador",
"get_in_touch": "Póngase en contacto", "get_in_touch": "Póngase en contacto",
"contact_support": "Contactar con Soporte", "contact_support": "Contactar con Soporte",
"community_support": "Soporte comunitario",
"feedback": "Comentarios", "feedback": "Comentarios",
"submitted_feedback": "¡Gracias por sus comentarios!", "submitted_feedback": "¡Gracias por sus comentarios!",
"feedback_error": "Error al enviar comentarios", "feedback_error": "Error al enviar comentarios",
@@ -1213,6 +1224,7 @@
"organizer_name_variable": "Nombre del organizador", "organizer_name_variable": "Nombre del organizador",
"app_upgrade_description": "Para poder usar esta función, necesita actualizarse a una cuenta Pro.", "app_upgrade_description": "Para poder usar esta función, necesita actualizarse a una cuenta Pro.",
"invalid_number": "Número de teléfono no válido", "invalid_number": "Número de teléfono no válido",
"invalid_url_error_message": "URL no válida para {{label}}. URL de ejemplo: {{sampleUrl}}",
"navigate": "Navegar", "navigate": "Navegar",
"open": "Abrir", "open": "Abrir",
"close": "Cerrar", "close": "Cerrar",
@@ -1276,6 +1288,7 @@
"personal_cal_url": "Mi URL personal de {{appName}}", "personal_cal_url": "Mi URL personal de {{appName}}",
"bio_hint": "Algunas frases sobre usted, esta información aparecerá en la página de su URL personal.", "bio_hint": "Algunas frases sobre usted, esta información aparecerá en la página de su URL personal.",
"user_has_no_bio": "Este usuario no ha añadido una biografía todavía.", "user_has_no_bio": "Este usuario no ha añadido una biografía todavía.",
"bio": "Biografía",
"delete_account_modal_title": "Eliminar cuenta", "delete_account_modal_title": "Eliminar cuenta",
"confirm_delete_account_modal": "¿Está seguro que desea eliminar su cuenta de {{appName}}?", "confirm_delete_account_modal": "¿Está seguro que desea eliminar su cuenta de {{appName}}?",
"delete_my_account": "Eliminar mi cuenta", "delete_my_account": "Eliminar mi cuenta",
@@ -1286,6 +1299,7 @@
"select_calendars": "Seleccione los calendarios en los que desee comprobar conflictos para evitar reservas dobles.", "select_calendars": "Seleccione los calendarios en los que desee comprobar conflictos para evitar reservas dobles.",
"check_for_conflicts": "Comprobar conflictos", "check_for_conflicts": "Comprobar conflictos",
"view_recordings": "Ver grabaciones", "view_recordings": "Ver grabaciones",
"check_for_recordings": "Comprobar si hay grabaciones",
"adding_events_to": "Agregando eventos a", "adding_events_to": "Agregando eventos a",
"follow_system_preferences": "Siga las preferencias del sistema", "follow_system_preferences": "Siga las preferencias del sistema",
"custom_brand_colors": "Colores de marca personalizados", "custom_brand_colors": "Colores de marca personalizados",
@@ -1530,6 +1544,7 @@
"problem_registering_domain": "Hubo un problema con el registro del subdominio, intente nuevamente o comuníquese con un administrador", "problem_registering_domain": "Hubo un problema con el registro del subdominio, intente nuevamente o comuníquese con un administrador",
"team_publish": "Publicar equipo", "team_publish": "Publicar equipo",
"number_text_notifications": "Número de teléfono (Notificaciones de texto)", "number_text_notifications": "Número de teléfono (Notificaciones de texto)",
"number_sms_notifications": "Número de teléfono (notificaciones SMS)",
"attendee_email_variable": "Correo electrónico del asistente", "attendee_email_variable": "Correo electrónico del asistente",
"attendee_email_info": "El correo electrónico de la persona que reserva", "attendee_email_info": "El correo electrónico de la persona que reserva",
"kbar_search_placeholder": "Escriba un comando o búsqueda...", "kbar_search_placeholder": "Escriba un comando o búsqueda...",
@@ -1594,6 +1609,7 @@
"options": "Opciones", "options": "Opciones",
"enter_option": "Introduzca la opción {{index}}", "enter_option": "Introduzca la opción {{index}}",
"add_an_option": "Agregue una opción", "add_an_option": "Agregue una opción",
"location_already_exists": "Esta ubicación ya existe. Seleccione una ubicación nueva",
"radio": "Botón radial", "radio": "Botón radial",
"google_meet_warning": "Para usar Google Meet, debe establecer un Google Calendar como calendario de destino", "google_meet_warning": "Para usar Google Meet, debe establecer un Google Calendar como calendario de destino",
"individual": "Individuo", "individual": "Individuo",
@@ -1613,6 +1629,7 @@
"date_overrides_mark_all_day_unavailable_other": "Marcar como no disponible en las fechas seleccionadas", "date_overrides_mark_all_day_unavailable_other": "Marcar como no disponible en las fechas seleccionadas",
"date_overrides_add_btn": "Agregar anulación", "date_overrides_add_btn": "Agregar anulación",
"date_overrides_update_btn": "Actualizar anulación", "date_overrides_update_btn": "Actualizar anulación",
"date_successfully_added": "Sustitución de fechas añadida correctamente",
"event_type_duplicate_copy_text": "{{slug}}-copia", "event_type_duplicate_copy_text": "{{slug}}-copia",
"set_as_default": "Establecer como predeterminado", "set_as_default": "Establecer como predeterminado",
"hide_eventtype_details": "Ocultar detalles del tipo de evento", "hide_eventtype_details": "Ocultar detalles del tipo de evento",
@@ -1639,6 +1656,7 @@
"minimum_round_robin_hosts_count": "Número de anfitriones requeridos para asistir", "minimum_round_robin_hosts_count": "Número de anfitriones requeridos para asistir",
"hosts": "Anfitriones", "hosts": "Anfitriones",
"upgrade_to_enable_feature": "Debe crear un equipo para activar esta función. Haga clic para crear un equipo.", "upgrade_to_enable_feature": "Debe crear un equipo para activar esta función. Haga clic para crear un equipo.",
"orgs_upgrade_to_enable_feature": "Debe pasarse a nuestro plan Enterprise para habilitar esta función.",
"new_attendee": "Nuevo asistente", "new_attendee": "Nuevo asistente",
"awaiting_approval": "En espera de aprobación", "awaiting_approval": "En espera de aprobación",
"requires_google_calendar": "Esta aplicación requiere una conexión con Google Calendar", "requires_google_calendar": "Esta aplicación requiere una conexión con Google Calendar",
@@ -1743,6 +1761,7 @@
"show_on_booking_page": "Mostrar en la página de reserva", "show_on_booking_page": "Mostrar en la página de reserva",
"get_started_zapier_templates": "Comience con las plantillas de Zapier", "get_started_zapier_templates": "Comience con las plantillas de Zapier",
"team_is_unpublished": "{{team}} no está publicado", "team_is_unpublished": "{{team}} no está publicado",
"org_is_unpublished_description": "El enlace de esta organización no está disponible actualmente. Comuníquese con el propietario de la organización o pídale que lo publique.",
"team_is_unpublished_description": "Este enlace de {{entity}} no está disponible actualmente. Póngase en contacto con el propietario de {{entity}} o pídale que lo publique.", "team_is_unpublished_description": "Este enlace de {{entity}} no está disponible actualmente. Póngase en contacto con el propietario de {{entity}} o pídale que lo publique.",
"team_member": "Miembro del equipo", "team_member": "Miembro del equipo",
"a_routing_form": "Un formulario de enrutamiento", "a_routing_form": "Un formulario de enrutamiento",
@@ -1877,6 +1896,7 @@
"edit_invite_link": "Editar ajustes de enlace", "edit_invite_link": "Editar ajustes de enlace",
"invite_link_copied": "Enlace de invitación copiado", "invite_link_copied": "Enlace de invitación copiado",
"invite_link_deleted": "Enlace de invitación eliminado", "invite_link_deleted": "Enlace de invitación eliminado",
"api_key_deleted": "Clave API eliminada",
"invite_link_updated": "Configuración de enlace de invitación guardada", "invite_link_updated": "Configuración de enlace de invitación guardada",
"link_expires_after": "Enlaces establecidos para expirar después de...", "link_expires_after": "Enlaces establecidos para expirar después de...",
"one_day": "1 día", "one_day": "1 día",
@@ -2009,7 +2029,13 @@
"attendee_last_name_variable": "Apellido del asistente", "attendee_last_name_variable": "Apellido del asistente",
"attendee_first_name_info": "Nombre de la persona que reserva", "attendee_first_name_info": "Nombre de la persona que reserva",
"attendee_last_name_info": "Apellido de la persona que reserva", "attendee_last_name_info": "Apellido de la persona que reserva",
"your_monthly_digest": "Su resumen mensual",
"member_name": "Nombre del miembro",
"most_popular_events": "Eventos más populares",
"summary_of_events_for_your_team_for_the_last_30_days": "Este es el resumen de los eventos populares de su equipo {{teamName}} durante los últimos 30 días",
"me": "Yo", "me": "Yo",
"monthly_digest_email": "Correo electrónico del resumen mensual",
"monthly_digest_email_for_teams": "Correo electrónico de resumen mensual para equipos",
"verify_team_tooltip": "Verifique su equipo para activar el envío de mensajes a los asistentes", "verify_team_tooltip": "Verifique su equipo para activar el envío de mensajes a los asistentes",
"member_removed": "Miembro eliminado", "member_removed": "Miembro eliminado",
"my_availability": "Mi disponibilidad", "my_availability": "Mi disponibilidad",
@@ -2039,13 +2065,41 @@
"team_no_event_types": "Este equipo no tiene tipos de eventos", "team_no_event_types": "Este equipo no tiene tipos de eventos",
"seat_options_doesnt_multiple_durations": "La opción Cupo no soporta múltiples duraciones", "seat_options_doesnt_multiple_durations": "La opción Cupo no soporta múltiples duraciones",
"include_calendar_event": "Incluir evento del calendario", "include_calendar_event": "Incluir evento del calendario",
"oAuth": "OAuth",
"recently_added": "Añadido recientemente", "recently_added": "Añadido recientemente",
"no_members_found": "No se encontraron miembros", "no_members_found": "No se encontraron miembros",
"event_setup_length_error": "Configuración del evento: la duración debe ser de al menos 1 minuto.", "event_setup_length_error": "Configuración del evento: la duración debe ser de al menos 1 minuto.",
"availability_schedules": "Horarios de disponibilidad", "availability_schedules": "Horarios de disponibilidad",
"unauthorized": "Sin autorización",
"access_cal_account": "{{clientName}} quiere acceder a su cuenta de {{appName}}",
"select_account_team": "Seleccionar cuenta o equipo",
"allow_client_to": "Esto permitirá que {{clientName}}",
"associate_with_cal_account": "Lo asocie con su información personal de {{clientName}}",
"see_personal_info": "Consulte su información personal, incluida la información personal que haya hecho pública",
"see_primary_email_address": "Consulte su dirección de correo electrónico principal",
"connect_installed_apps": "Se conecte a sus aplicaciones instaladas",
"access_event_type": "Lea, edite y elimine sus tipos de eventos",
"access_availability": "Lea, edite y elimine su disponibilidad",
"access_bookings": "Lea, edite y elimine sus reservas",
"allow_client_to_do": "¿Desea permitir que {{clientName}} haga esto?",
"oauth_access_information": "Al hacer clic en permitir, permite que esta aplicación utilice su información de acuerdo con sus términos de servicio y política de privacidad. Puede eliminar el acceso en la App Store de {{appName}}.",
"allow": "Permitir",
"view_only_edit_availability_not_onboarded": "Este usuario no ha completado la incorporación. No podrá establecer su disponibilidad hasta que haya completado la incorporación.", "view_only_edit_availability_not_onboarded": "Este usuario no ha completado la incorporación. No podrá establecer su disponibilidad hasta que haya completado la incorporación.",
"view_only_edit_availability": "Está viendo la disponibilidad de este usuario. Sólo puede editar su propia disponibilidad.", "view_only_edit_availability": "Está viendo la disponibilidad de este usuario. Sólo puede editar su propia disponibilidad.",
"you_can_override_calendar_in_advanced_tab": "Puede anular esto por evento en la Configuración avanzada de cada tipo de evento.",
"edit_users_availability": "Editar disponibilidad del usuario: {{username}}", "edit_users_availability": "Editar disponibilidad del usuario: {{username}}",
"resend_invitation": "Reenviar invitación",
"invitation_resent": "Se reenvió la invitación.",
"add_client": "Agregar cliente",
"copy_client_secret_info": "Después de copiar el secreto, ya no podrá volver a verlo",
"add_new_client": "Agregar nuevo cliente",
"this_app_is_not_setup_already": "Esta aplicación aún no se ha configurado",
"as_csv": "como CSV",
"overlay_my_calendar": "Superponer mi calendario",
"overlay_my_calendar_toc": "Al conectarse a su calendario, acepta nuestra política de privacidad y nuestros términos de uso. Puede revocar el acceso en cualquier momento.",
"view_overlay_calendar_events": "Consulte los eventos de su calendario para evitar el conflicto de reservas.",
"lock_timezone_toggle_on_booking_page": "Bloquear la zona horaria en la página de reserva",
"description_lock_timezone_toggle_on_booking_page": "Bloquear la zona horaria en la página de reserva, es útil para eventos en persona.",
"extensive_whitelabeling": "Asistencia dedicada en materia de incorporación e ingeniería", "extensive_whitelabeling": "Asistencia dedicada en materia de incorporación e ingeniería",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agregue sus nuevas cadenas arriba ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agregue sus nuevas cadenas arriba ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
} }
@@ -506,6 +506,7 @@
"next_step": "Saltatu pausoa", "next_step": "Saltatu pausoa",
"prev_step": "Aurreko pausoa", "prev_step": "Aurreko pausoa",
"install": "Instalatu", "install": "Instalatu",
"install_paid_app": "Harpidetu",
"installed": "Instalatua", "installed": "Instalatua",
"disconnect": "Deskonektatu", "disconnect": "Deskonektatu",
"automation": "Automatizazioa", "automation": "Automatizazioa",
+18 -3
View File
@@ -289,6 +289,8 @@
"when": "Quand", "when": "Quand",
"where": "Où", "where": "Où",
"add_to_calendar": "Ajouter au calendrier", "add_to_calendar": "Ajouter au calendrier",
"add_to_calendar_description": "Sélectionnez l'endroit où ajouter des événements lorsque vous êtes réservé.",
"add_events_to": "Ajouter les événements à",
"add_another_calendar": "Ajouter un autre calendrier", "add_another_calendar": "Ajouter un autre calendrier",
"other": "Autre", "other": "Autre",
"email_sign_in_subject": "Votre lien de connexion pour {{appName}}", "email_sign_in_subject": "Votre lien de connexion pour {{appName}}",
@@ -407,7 +409,7 @@
"automatically_adjust_theme": "Ajuster automatiquement l'apparence en fonction des préférences de l'invité", "automatically_adjust_theme": "Ajuster automatiquement l'apparence en fonction des préférences de l'invité",
"user_dynamic_booking_disabled": "Certains utilisateurs du groupe ont actuellement désactivé les réservations de groupe dynamiques", "user_dynamic_booking_disabled": "Certains utilisateurs du groupe ont actuellement désactivé les réservations de groupe dynamiques",
"allow_dynamic_booking_tooltip": "Les liens de réservation de groupe peuvent être créés dynamiquement en ajoutant plusieurs noms d'utilisateur séparés par un « + ». Exemple : « {{appName}}/bailey+peer ».", "allow_dynamic_booking_tooltip": "Les liens de réservation de groupe peuvent être créés dynamiquement en ajoutant plusieurs noms d'utilisateur séparés par un « + ». Exemple : « {{appName}}/bailey+peer ».",
"allow_dynamic_booking": "Autoriser les participants à prendre rendez-vous avec vous via des réservations de groupe dynamiques", "allow_dynamic_booking": "Autorisez les participants à prendre rendez-vous avec vous via des réservations de groupe dynamiques.",
"dynamic_booking": "Liens de groupe dynamiques", "dynamic_booking": "Liens de groupe dynamiques",
"allow_seo_indexing": "Autorisez les moteurs de recherche à accéder à votre contenu public.", "allow_seo_indexing": "Autorisez les moteurs de recherche à accéder à votre contenu public.",
"seo_indexing": "Autoriser l'indexation SEO", "seo_indexing": "Autoriser l'indexation SEO",
@@ -450,13 +452,14 @@
"go_to_billing_portal": "Accéder au portail de facturation", "go_to_billing_portal": "Accéder au portail de facturation",
"need_anything_else": "Besoin d'autre chose ?", "need_anything_else": "Besoin d'autre chose ?",
"further_billing_help": "Si vous avez besoin d'aide pour la facturation, notre équipe d'assistance est là pour vous aider.", "further_billing_help": "Si vous avez besoin d'aide pour la facturation, notre équipe d'assistance est là pour vous aider.",
"contact": "Contact", "contact": "Contacter",
"our_support_team": "notre équipe d'assistance", "our_support_team": "notre équipe d'assistance",
"contact_our_support_team": "Contactez notre équipe d'assistance", "contact_our_support_team": "Contactez notre équipe d'assistance",
"uh_oh": "Oups !", "uh_oh": "Oups !",
"no_event_types_have_been_setup": "Cet utilisateur n'a pas encore configuré de type d'événement.", "no_event_types_have_been_setup": "Cet utilisateur n'a pas encore configuré de type d'événement.",
"edit_logo": "Modifier le logo", "edit_logo": "Modifier le logo",
"upload_a_logo": "Télécharger un logo", "upload_a_logo": "Télécharger un logo",
"upload_logo": "Télécharger un logo",
"remove_logo": "Supprimer le logo", "remove_logo": "Supprimer le logo",
"enable": "Activer", "enable": "Activer",
"code": "Code", "code": "Code",
@@ -569,6 +572,7 @@
"your_team_name": "Nom de votre équipe", "your_team_name": "Nom de votre équipe",
"team_updated_successfully": "Équipe mise à jour avec succès", "team_updated_successfully": "Équipe mise à jour avec succès",
"your_team_updated_successfully": "Votre équipe a été mise à jour avec succès.", "your_team_updated_successfully": "Votre équipe a été mise à jour avec succès.",
"your_org_updated_successfully": "Votre organisation a été mise à jour avec succès.",
"about": "À propos", "about": "À propos",
"team_description": "Quelques mots à propos de votre équipe. Ces informations apparaîtront sur la page publique de votre équipe.", "team_description": "Quelques mots à propos de votre équipe. Ces informations apparaîtront sur la page publique de votre équipe.",
"org_description": "Quelques phrases à propos de votre organisation. Elles apparaîtront sur la page de profil public de votre organisation.", "org_description": "Quelques phrases à propos de votre organisation. Elles apparaîtront sur la page de profil public de votre organisation.",
@@ -844,6 +848,8 @@
"next_step": "Passer l'étape", "next_step": "Passer l'étape",
"prev_step": "Étape précédente", "prev_step": "Étape précédente",
"install": "Installer", "install": "Installer",
"install_paid_app": "S'abonner",
"start_paid_trial": "Démarrer l'essai gratuit",
"installed": "Installée", "installed": "Installée",
"active_install_one": "{{count}} installation active", "active_install_one": "{{count}} installation active",
"active_install_other": "{{count}} installations actives", "active_install_other": "{{count}} installations actives",
@@ -1092,6 +1098,8 @@
"developer_documentation": "Documentation pour développeurs", "developer_documentation": "Documentation pour développeurs",
"get_in_touch": "Contactez-nous", "get_in_touch": "Contactez-nous",
"contact_support": "Contacter l'assistance", "contact_support": "Contacter l'assistance",
"premium_support": "Assistance Premium",
"community_support": "Aide communautaire",
"feedback": "Commentaires", "feedback": "Commentaires",
"submitted_feedback": "Merci pour vos commentaires !", "submitted_feedback": "Merci pour vos commentaires !",
"feedback_error": "Erreur lors de l'envoi du commentaire", "feedback_error": "Erreur lors de l'envoi du commentaire",
@@ -2024,7 +2032,7 @@
"summary_of_events_for_your_team_for_the_last_30_days": "Voici votre résumé des événements populaires pour votre équipe {{teamName}} au cours des 30 derniers jours", "summary_of_events_for_your_team_for_the_last_30_days": "Voici votre résumé des événements populaires pour votre équipe {{teamName}} au cours des 30 derniers jours",
"me": "Moi", "me": "Moi",
"monthly_digest_email": "E-mail de résumé mensuel", "monthly_digest_email": "E-mail de résumé mensuel",
"monthly_digest_email_for_teams": "E-mail de résumé mensuel pour les équipes", "monthly_digest_email_for_teams": "E-mail de résumé mensuel pour les équipes.",
"verify_team_tooltip": "Vérifiez votre équipe pour activer l'envoi de messages aux participants", "verify_team_tooltip": "Vérifiez votre équipe pour activer l'envoi de messages aux participants",
"member_removed": "Membre supprimé", "member_removed": "Membre supprimé",
"my_availability": "Mes disponibilités", "my_availability": "Mes disponibilités",
@@ -2065,12 +2073,19 @@
"access_bookings": "Lire, modifier, supprimer vos réservations", "access_bookings": "Lire, modifier, supprimer vos réservations",
"allow_client_to_do": "Autoriser {{clientName}} à faire cela ?", "allow_client_to_do": "Autoriser {{clientName}} à faire cela ?",
"allow": "Autoriser", "allow": "Autoriser",
"you_can_override_calendar_in_advanced_tab": "Vous pouvez modifier ceci pour chaque événement dans les paramètres avancés de chaque type d'événement.",
"edit_users_availability": "Modifier la disponibilité de l'utilisateur : {{username}}", "edit_users_availability": "Modifier la disponibilité de l'utilisateur : {{username}}",
"resend_invitation": "Renvoyer l'invitation", "resend_invitation": "Renvoyer l'invitation",
"invitation_resent": "L'invitation a été renvoyée.", "invitation_resent": "L'invitation a été renvoyée.",
"add_client": "Ajouter un client", "add_client": "Ajouter un client",
"add_new_client": "Ajouter un nouveau client", "add_new_client": "Ajouter un nouveau client",
"as_csv": "au format CSV", "as_csv": "au format CSV",
"overlay_my_calendar": "Superposer mon calendrier",
"overlay_my_calendar_toc": "En vous connectant à votre calendrier, vous acceptez notre politique de confidentialité et nos conditions d'utilisation. Vous pouvez révoquer cet accès à tout moment.",
"view_overlay_calendar_events": "Consultez les événements de votre calendrier afin d'éviter les réservations incompatibles.",
"lock_timezone_toggle_on_booking_page": "Verrouiller le fuseau horaire sur la page de réservation",
"description_lock_timezone_toggle_on_booking_page": "Pour verrouiller le fuseau horaire sur la page de réservation, utile pour les événements en personne.",
"extensive_whitelabeling": "Marque blanche étendue", "extensive_whitelabeling": "Marque blanche étendue",
"unlimited_teams": "Équipes illimitées",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
} }
@@ -268,6 +268,7 @@
"set_availability": "ציין את הזמינות שלך", "set_availability": "ציין את הזמינות שלך",
"availability_settings": "הגדרות זמינוּת", "availability_settings": "הגדרות זמינוּת",
"continue_without_calendar": "להמשיך בלי לוח שנה", "continue_without_calendar": "להמשיך בלי לוח שנה",
"continue_with": "להמשיך עם {{appName}}",
"connect_your_calendar": "קשר את לוח השנה שלך", "connect_your_calendar": "קשר את לוח השנה שלך",
"connect_your_video_app": "חיבור אפליקציות הווידאו שלך", "connect_your_video_app": "חיבור אפליקציות הווידאו שלך",
"connect_your_video_app_instructions": "חבר/י את אפליקציות הווידאו שלך כדי להשתמש בהן עבור סוגי האירועים שלך.", "connect_your_video_app_instructions": "חבר/י את אפליקציות הווידאו שלך כדי להשתמש בהן עבור סוגי האירועים שלך.",
@@ -288,6 +289,8 @@
"when": "מתי", "when": "מתי",
"where": "היכן", "where": "היכן",
"add_to_calendar": "הוספה ללוח השנה", "add_to_calendar": "הוספה ללוח השנה",
"add_to_calendar_description": "בחר/י להיכן יש להוסיף אירועים כשאת/ה עסוק/ה.",
"add_events_to": "הוספת אירועים ל",
"add_another_calendar": "להוסיף לוח שנה אחר", "add_another_calendar": "להוסיף לוח שנה אחר",
"other": "אחר", "other": "אחר",
"email_sign_in_subject": "קישור הכניסה שלך אל {{appName}}", "email_sign_in_subject": "קישור הכניסה שלך אל {{appName}}",
@@ -422,6 +425,7 @@
"booking_created": "ההזמנה נוצרה", "booking_created": "ההזמנה נוצרה",
"booking_rejected": "ההזמנה נדחתה", "booking_rejected": "ההזמנה נדחתה",
"booking_requested": "התקבלה בקשת הזמנה", "booking_requested": "התקבלה בקשת הזמנה",
"booking_payment_initiated": "הופעל תשלום על ההזמנה",
"meeting_ended": "הפגישה הסתיימה", "meeting_ended": "הפגישה הסתיימה",
"form_submitted": "הטופס נשלח", "form_submitted": "הטופס נשלח",
"booking_paid": "בוצע תשלום עבור ההזמנה", "booking_paid": "בוצע תשלום עבור ההזמנה",
@@ -456,6 +460,7 @@
"no_event_types_have_been_setup": "משתמש זה עדיין לא הגדיר סוג אירוע.", "no_event_types_have_been_setup": "משתמש זה עדיין לא הגדיר סוג אירוע.",
"edit_logo": "עריכת לוגו", "edit_logo": "עריכת לוגו",
"upload_a_logo": "העלאת לוגו", "upload_a_logo": "העלאת לוגו",
"upload_logo": "העלאת לוגו",
"remove_logo": "הסרת לוגו", "remove_logo": "הסרת לוגו",
"enable": "הפעלה", "enable": "הפעלה",
"code": "קוד", "code": "קוד",
@@ -568,6 +573,7 @@
"your_team_name": "שם הצוות שלך", "your_team_name": "שם הצוות שלך",
"team_updated_successfully": "עדכון הצוות בוצע בהצלחה", "team_updated_successfully": "עדכון הצוות בוצע בהצלחה",
"your_team_updated_successfully": "הצוות שלך עודכן בהצלחה.", "your_team_updated_successfully": "הצוות שלך עודכן בהצלחה.",
"your_org_updated_successfully": "הארגון שלך עודכן בהצלחה.",
"about": "אודות", "about": "אודות",
"team_description": "מספר משפטים אודות הצוות. המידע הזה יופיע בדף ה-URL של הצוות.", "team_description": "מספר משפטים אודות הצוות. המידע הזה יופיע בדף ה-URL של הצוות.",
"org_description": "מספר משפטים אודות הארגון. הם יופיעו בדף עם כתובת ה-URL של הארגון.", "org_description": "מספר משפטים אודות הארגון. הם יופיעו בדף עם כתובת ה-URL של הארגון.",
@@ -599,6 +605,7 @@
"hide_book_a_team_member": "הסתרת הלחצן לשריון זמן של חבר/ת צוות", "hide_book_a_team_member": "הסתרת הלחצן לשריון זמן של חבר/ת צוות",
"hide_book_a_team_member_description": "הסתר/י את הלחצן לשריון זמן של חבר/ת צוות מהדפים הציבוריים שלך.", "hide_book_a_team_member_description": "הסתר/י את הלחצן לשריון זמן של חבר/ת צוות מהדפים הציבוריים שלך.",
"danger_zone": "אזור מסוכן", "danger_zone": "אזור מסוכן",
"account_deletion_cannot_be_undone": "יש לנקוט זהירות. מחיקת חשבון היא פעולה בלתי הפיכה.",
"back": "הקודם", "back": "הקודם",
"cancel": "ביטול", "cancel": "ביטול",
"cancel_all_remaining": "לבטל את כל הנותרים", "cancel_all_remaining": "לבטל את כל הנותרים",
@@ -688,6 +695,7 @@
"people": "אנשים", "people": "אנשים",
"your_email": "הדוא\"ל שלך", "your_email": "הדוא\"ל שלך",
"change_avatar": "שינוי אווטאר", "change_avatar": "שינוי אווטאר",
"upload_avatar": "העלאת אווטאר",
"language": "שפה", "language": "שפה",
"timezone": "אזור זמן", "timezone": "אזור זמן",
"first_day_of_week": "היום הראשון בשבוע", "first_day_of_week": "היום הראשון בשבוע",
@@ -778,6 +786,7 @@
"disable_guests": "השבתת אורחים", "disable_guests": "השבתת אורחים",
"disable_guests_description": "השבת את האפשרות להוסיף אורחים נוספים בעת ביצוע הזמנה.", "disable_guests_description": "השבת את האפשרות להוסיף אורחים נוספים בעת ביצוע הזמנה.",
"private_link": "יצירת קישור פרטי", "private_link": "יצירת קישור פרטי",
"enable_private_url": "לאפשר כתובת URL פרטית",
"private_link_label": "קישור פרטי", "private_link_label": "קישור פרטי",
"private_link_hint": "הקישור הפרטי שלך ייווצר מחדש לאחר כל שימוש", "private_link_hint": "הקישור הפרטי שלך ייווצר מחדש לאחר כל שימוש",
"copy_private_link": "העתקת קישור פרטי", "copy_private_link": "העתקת קישור פרטי",
@@ -840,6 +849,7 @@
"next_step": "לדלג על שלב זה", "next_step": "לדלג על שלב זה",
"prev_step": "לשלב הקודם", "prev_step": "לשלב הקודם",
"install": "התקנה", "install": "התקנה",
"install_paid_app": "הרשמה למינוי",
"installed": "מותקן", "installed": "מותקן",
"active_install_one": "התקנה פעילה {{count}}", "active_install_one": "התקנה פעילה {{count}}",
"active_install_other": "{{count}} התקנות פעילות", "active_install_other": "{{count}} התקנות פעילות",
@@ -1088,6 +1098,7 @@
"developer_documentation": "מסמכי מפתחים", "developer_documentation": "מסמכי מפתחים",
"get_in_touch": "יצירת קשר", "get_in_touch": "יצירת קשר",
"contact_support": "פנייה לתמיכה", "contact_support": "פנייה לתמיכה",
"community_support": "תמיכת קהילה",
"feedback": "משוב", "feedback": "משוב",
"submitted_feedback": "תודה על המשוב!", "submitted_feedback": "תודה על המשוב!",
"feedback_error": "שגיאה בעת שליחת משוב", "feedback_error": "שגיאה בעת שליחת משוב",
@@ -1213,6 +1224,7 @@
"organizer_name_variable": "שם המארגן/ת", "organizer_name_variable": "שם המארגן/ת",
"app_upgrade_description": "כדי להשתמש בתכונה זו, עליך לשדרג לחשבון Pro.", "app_upgrade_description": "כדי להשתמש בתכונה זו, עליך לשדרג לחשבון Pro.",
"invalid_number": "מספר טלפון לא תקין", "invalid_number": "מספר טלפון לא תקין",
"invalid_url_error_message": "כתובת URL לא חוקית עבור {{label}}. כתובת URL לדוגמה: {{sampleUrl}}",
"navigate": "ניווט", "navigate": "ניווט",
"open": "פתח", "open": "פתח",
"close": "סגירה", "close": "סגירה",
@@ -1276,6 +1288,7 @@
"personal_cal_url": "כתובת ה-URL האישית שלי של {{appName}}", "personal_cal_url": "כתובת ה-URL האישית שלי של {{appName}}",
"bio_hint": "מספר משפטים אודותיך. המידע הזה יופיע בדף ה-URL האישי שלך.", "bio_hint": "מספר משפטים אודותיך. המידע הזה יופיע בדף ה-URL האישי שלך.",
"user_has_no_bio": "משתמש זה עדיין לא הוסיף ביוגרפיה.", "user_has_no_bio": "משתמש זה עדיין לא הוסיף ביוגרפיה.",
"bio": "ביוגרפיה",
"delete_account_modal_title": "מחיקת החשבון", "delete_account_modal_title": "מחיקת החשבון",
"confirm_delete_account_modal": "בטוח שברצונך למחוק את חשבון {{appName}} שלך?", "confirm_delete_account_modal": "בטוח שברצונך למחוק את חשבון {{appName}} שלך?",
"delete_my_account": "מחיקת החשבון שלי", "delete_my_account": "מחיקת החשבון שלי",
@@ -1286,6 +1299,7 @@
"select_calendars": "בחר את לוחות השנה שבהם ברצונך לבדוק אם יש התנגשויות, כדי למנוע כפל הזמנות.", "select_calendars": "בחר את לוחות השנה שבהם ברצונך לבדוק אם יש התנגשויות, כדי למנוע כפל הזמנות.",
"check_for_conflicts": "בדיקת התנגשויות", "check_for_conflicts": "בדיקת התנגשויות",
"view_recordings": "צפייה בהקלטות", "view_recordings": "צפייה בהקלטות",
"check_for_recordings": "חיפוש הקלטות",
"adding_events_to": "הוספת אירועים ל", "adding_events_to": "הוספת אירועים ל",
"follow_system_preferences": "פעל לפי העדפות המערכת", "follow_system_preferences": "פעל לפי העדפות המערכת",
"custom_brand_colors": "צבעי מותג בהתאמה אישית", "custom_brand_colors": "צבעי מותג בהתאמה אישית",
@@ -1530,6 +1544,7 @@
"problem_registering_domain": "הייתה בעיה ברישום תת-הדומיין; אפשר לנסות שוב או לפנות למנהל/ת מערכת", "problem_registering_domain": "הייתה בעיה ברישום תת-הדומיין; אפשר לנסות שוב או לפנות למנהל/ת מערכת",
"team_publish": "פרסום צוות", "team_publish": "פרסום צוות",
"number_text_notifications": "מספר טלפון (להודעות טקסט)", "number_text_notifications": "מספר טלפון (להודעות טקסט)",
"number_sms_notifications": "מספר טלפון (להודעות SMS)",
"attendee_email_variable": "כתובת הדוא\"ל של המשתתף", "attendee_email_variable": "כתובת הדוא\"ל של המשתתף",
"attendee_email_info": "כתובת הדוא\"ל של האדם שביצע את ההזמנה", "attendee_email_info": "כתובת הדוא\"ל של האדם שביצע את ההזמנה",
"kbar_search_placeholder": "הקלד/י פקודה או חפש/י...", "kbar_search_placeholder": "הקלד/י פקודה או חפש/י...",
@@ -1594,6 +1609,7 @@
"options": "אפשרויות", "options": "אפשרויות",
"enter_option": "הזנת ה-{{index}} של האפשרות", "enter_option": "הזנת ה-{{index}} של האפשרות",
"add_an_option": "הוספת אפשרות", "add_an_option": "הוספת אפשרות",
"location_already_exists": "המיקום הזה כבר קיים, יש לבחור מיקום חדש",
"radio": "רדיו", "radio": "רדיו",
"google_meet_warning": "כדי להשתמש ב-Google Meet, יש להגדיר את Google Calendar כלוח השנה של המארח", "google_meet_warning": "כדי להשתמש ב-Google Meet, יש להגדיר את Google Calendar כלוח השנה של המארח",
"individual": "משתמש בודד", "individual": "משתמש בודד",
@@ -1613,6 +1629,7 @@
"date_overrides_mark_all_day_unavailable_other": "סימון אי-זמינות בתאריכים מסוימים", "date_overrides_mark_all_day_unavailable_other": "סימון אי-זמינות בתאריכים מסוימים",
"date_overrides_add_btn": "הוספת מעקף", "date_overrides_add_btn": "הוספת מעקף",
"date_overrides_update_btn": "עדכון מעקף", "date_overrides_update_btn": "עדכון מעקף",
"date_successfully_added": "עקיפת תאריך נוספה בהצלחה",
"event_type_duplicate_copy_text": "{{slug}}-עותק", "event_type_duplicate_copy_text": "{{slug}}-עותק",
"set_as_default": "להגדיר כברירת מחדל", "set_as_default": "להגדיר כברירת מחדל",
"hide_eventtype_details": "הסתרת פרטי סוג האירוע", "hide_eventtype_details": "הסתרת פרטי סוג האירוע",
@@ -1639,6 +1656,7 @@
"minimum_round_robin_hosts_count": "מספר המארחים שחייבים להשתתף", "minimum_round_robin_hosts_count": "מספר המארחים שחייבים להשתתף",
"hosts": "מארחים", "hosts": "מארחים",
"upgrade_to_enable_feature": "אתה צריך לייצר צוות כדי להפעיל את היכולת. לחץ ליצירת צוות.", "upgrade_to_enable_feature": "אתה צריך לייצר צוות כדי להפעיל את היכולת. לחץ ליצירת צוות.",
"orgs_upgrade_to_enable_feature": "כדי לאפשר שימוש בתכונה הזו, יש לשדרג לתוכנית שלנו לארגונים.",
"new_attendee": "משתתף/ת חדש/ה", "new_attendee": "משתתף/ת חדש/ה",
"awaiting_approval": "בהמתנה לאישור", "awaiting_approval": "בהמתנה לאישור",
"requires_google_calendar": "האפליקציה הזו מחייבת חיבור ל-Google Calendar", "requires_google_calendar": "האפליקציה הזו מחייבת חיבור ל-Google Calendar",
@@ -1743,6 +1761,7 @@
"show_on_booking_page": "להציג בדף ההזמנות", "show_on_booking_page": "להציג בדף ההזמנות",
"get_started_zapier_templates": "התחל עם תבניות Zapier", "get_started_zapier_templates": "התחל עם תבניות Zapier",
"team_is_unpublished": "צוות {{team}} אינו מפורסם", "team_is_unpublished": "צוות {{team}} אינו מפורסם",
"org_is_unpublished_description": "הקישור לארגון הזה אינו זמין כעת. יש ליצור קשר עם הבעלים של הארגון או לבקש מהם לפרסם אותו.",
"team_is_unpublished_description": "קישור ה-{{entity}} הזה אינו זמין כעת. יש ליצור קשר עם הבעלים של ה-{{entity}} או לבקש מהם לפרסם אותו.", "team_is_unpublished_description": "קישור ה-{{entity}} הזה אינו זמין כעת. יש ליצור קשר עם הבעלים של ה-{{entity}} או לבקש מהם לפרסם אותו.",
"team_member": "חבר צוות", "team_member": "חבר צוות",
"a_routing_form": "טופס ניתוב", "a_routing_form": "טופס ניתוב",
@@ -1877,6 +1896,7 @@
"edit_invite_link": "עריכת הגדרות הקישור", "edit_invite_link": "עריכת הגדרות הקישור",
"invite_link_copied": "קישור ההזמנה הועתק", "invite_link_copied": "קישור ההזמנה הועתק",
"invite_link_deleted": "קישור ההזמנה נמחק", "invite_link_deleted": "קישור ההזמנה נמחק",
"api_key_deleted": "מפתח API נמחק",
"invite_link_updated": "הגדרות קישור ההזמנה נשמרו", "invite_link_updated": "הגדרות קישור ההזמנה נשמרו",
"link_expires_after": "הקישורים מוגדרים לפוג לאחר...", "link_expires_after": "הקישורים מוגדרים לפוג לאחר...",
"one_day": "יום אחד", "one_day": "יום אחד",
@@ -2009,7 +2029,13 @@
"attendee_last_name_variable": "שם המשפחה של המשתתף", "attendee_last_name_variable": "שם המשפחה של המשתתף",
"attendee_first_name_info": "השם הפרטי של האדם שביצע את ההזמנה", "attendee_first_name_info": "השם הפרטי של האדם שביצע את ההזמנה",
"attendee_last_name_info": "שם המשפחה של האדם שביצע את ההזמנה", "attendee_last_name_info": "שם המשפחה של האדם שביצע את ההזמנה",
"your_monthly_digest": "הסיכום החודשי שלך",
"member_name": "שם החבר/ה",
"most_popular_events": "האירועים הפופולריים ביותר",
"summary_of_events_for_your_team_for_the_last_30_days": "הנה הסיכום של האירועים הפופולריים של הצוות שלך, {{teamName}}, ל-30 הימים האחרונים",
"me": "אני", "me": "אני",
"monthly_digest_email": "אימייל עם סיכום חודשי",
"monthly_digest_email_for_teams": "אימייל עם סיכום חודשי עבור צוותים",
"verify_team_tooltip": "אמת/י את הצוות שלך כדי לאפשר שליחת הודעות למשתתפים", "verify_team_tooltip": "אמת/י את הצוות שלך כדי לאפשר שליחת הודעות למשתתפים",
"member_removed": "החבר הוסר", "member_removed": "החבר הוסר",
"my_availability": "הזמינות שלי", "my_availability": "הזמינות שלי",
@@ -2039,13 +2065,41 @@
"team_no_event_types": "אין לצוות זה אף סוג של אירוע", "team_no_event_types": "אין לצוות זה אף סוג של אירוע",
"seat_options_doesnt_multiple_durations": "האפשרויות של הושבה במקומות לא תומכות במשכי זמן שונים", "seat_options_doesnt_multiple_durations": "האפשרויות של הושבה במקומות לא תומכות במשכי זמן שונים",
"include_calendar_event": "כלילת אירוע מלוח השנה", "include_calendar_event": "כלילת אירוע מלוח השנה",
"oAuth": "OAuth",
"recently_added": "נוספו לאחרונה", "recently_added": "נוספו לאחרונה",
"no_members_found": "לא נמצא אף חבר", "no_members_found": "לא נמצא אף חבר",
"event_setup_length_error": "הגדרת אירוע: משך הזמן חייב להיות לפחות דקה אחת.", "event_setup_length_error": "הגדרת אירוע: משך הזמן חייב להיות לפחות דקה אחת.",
"availability_schedules": "לוחות זמנים לזמינוּת", "availability_schedules": "לוחות זמנים לזמינוּת",
"unauthorized": "אין הרשאה",
"access_cal_account": "{{clientName}} רוצה לקבל גישה לחשבון {{appName}} שלך",
"select_account_team": "בחירת חשבון או צוות",
"allow_client_to": "הדבר יאפשר ל-{{clientName}}:",
"associate_with_cal_account": "לשייך בינך לבין הפרטים האישיים שלך מ-{{clientName}}",
"see_personal_info": "לראות את הפרטים האישיים שלך, כולל פרטים אישיים שהגדרת כגלויים לכולם",
"see_primary_email_address": "לראות את כתובת הדוא\"ל הראשית שלך",
"connect_installed_apps": "להתחבר לאפליקציות המותקנות שלך",
"access_event_type": "לקרוא, לערוך ולמחוק את סוגי האירועים שלך",
"access_availability": "לקרוא, לערוך ולמחוק את הזמינות שלך",
"access_bookings": "לקרוא, לערוך ולמחוק את ההזמנות שלך",
"allow_client_to_do": "האם לאפשר ל-{{clientName}} לעשות זאת?",
"oauth_access_information": "לחיצה על 'אפשר' מהווה מתן הרשאה מצידך ליישום זה להשתמש במידע שלך בהתאם לתנאי השירות ולמדיניות הפרטיות שלו. ניתן לשלול את הגישה בחנות האפליקציות של {{appName}}.",
"allow": "אפשר",
"view_only_edit_availability_not_onboarded": "משתמש זה לא השלים תהליך הטמעה. לא תהיה לך אפשרות להגדיר את הזמינות שלו עד שהוא יעשה זאת.", "view_only_edit_availability_not_onboarded": "משתמש זה לא השלים תהליך הטמעה. לא תהיה לך אפשרות להגדיר את הזמינות שלו עד שהוא יעשה זאת.",
"view_only_edit_availability": "את/ה צופה בזמינות של משתמש זה. יש לך אפשרות לערוך רק את פרטי הזמינות שלך.", "view_only_edit_availability": "את/ה צופה בזמינות של משתמש זה. יש לך אפשרות לערוך רק את פרטי הזמינות שלך.",
"you_can_override_calendar_in_advanced_tab": "ניתן לעקוף זאת על בסיס כל אירוע לגופו בהגדרות המתקדמות בכל סוג אירוע.",
"edit_users_availability": "עריכת הזמינות של משתמש: {{username}}", "edit_users_availability": "עריכת הזמינות של משתמש: {{username}}",
"resend_invitation": "שליחת ההזמנה מחדש",
"invitation_resent": "ההזמנה נשלחה מחדש.",
"add_client": "הוספת לקוח",
"copy_client_secret_info": "לאחר העתקת הסוד, כבר לא תהיה לך אפשרות לראות אותו",
"add_new_client": "הוספת לקוח חדש",
"this_app_is_not_setup_already": "האפליקציה הזו עדיין לא הוגדרה",
"as_csv": "כ-CSV",
"overlay_my_calendar": "הצג את לוח השנה שלי בשכבת-על",
"overlay_my_calendar_toc": "על ידי חיבור אל לוח השנה שלך, את/ה מקבל/ת את מדיניות הפרטיות ואת תנאי השימוש שלנו. אפשר לשלול את הגישה בכל שלב.",
"view_overlay_calendar_events": "ראה/י את האירועים שלך בלוח השנה כדי למנוע התנגשות בהזמנות.",
"lock_timezone_toggle_on_booking_page": "נעילת אזור הזמן בדף ההזמנות",
"description_lock_timezone_toggle_on_booking_page": "כדי לנעול את אזור הזמן בדף ההזמנות – שימושי לאירועים אישיים.",
"extensive_whitelabeling": "תהליך הטמעה והנדסת תמיכה אישי", "extensive_whitelabeling": "תהליך הטמעה והנדסת תמיכה אישי",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
} }
@@ -268,6 +268,7 @@
"set_availability": "Imposta la tua disponibilità", "set_availability": "Imposta la tua disponibilità",
"availability_settings": "Impostazioni di disponibilità", "availability_settings": "Impostazioni di disponibilità",
"continue_without_calendar": "Continua senza calendario", "continue_without_calendar": "Continua senza calendario",
"continue_with": "Continua con {{appName}}",
"connect_your_calendar": "Collega il tuo calendario", "connect_your_calendar": "Collega il tuo calendario",
"connect_your_video_app": "Collega le tue applicazioni video", "connect_your_video_app": "Collega le tue applicazioni video",
"connect_your_video_app_instructions": "Collega le tue applicazioni video per utilizzarle per i tuoi tipi di evento.", "connect_your_video_app_instructions": "Collega le tue applicazioni video per utilizzarle per i tuoi tipi di evento.",
@@ -288,6 +289,8 @@
"when": "Quando", "when": "Quando",
"where": "Dove", "where": "Dove",
"add_to_calendar": "Aggiungi al calendario", "add_to_calendar": "Aggiungi al calendario",
"add_to_calendar_description": "Seleziona dove aggiungere eventi quando c'è una prenotazione.",
"add_events_to": "Aggiungi eventi a",
"add_another_calendar": "Aggiungi un altro calendario", "add_another_calendar": "Aggiungi un altro calendario",
"other": "Altro", "other": "Altro",
"email_sign_in_subject": "Link di accesso a {{appName}}", "email_sign_in_subject": "Link di accesso a {{appName}}",
@@ -422,6 +425,7 @@
"booking_created": "Prenotazione Creata", "booking_created": "Prenotazione Creata",
"booking_rejected": "Prenotazione Rifiutata", "booking_rejected": "Prenotazione Rifiutata",
"booking_requested": "Richiesta di prenotazione inviata", "booking_requested": "Richiesta di prenotazione inviata",
"booking_payment_initiated": "Pagamento della prenotazione iniziato",
"meeting_ended": "Riunione terminata", "meeting_ended": "Riunione terminata",
"form_submitted": "Modulo inviato", "form_submitted": "Modulo inviato",
"booking_paid": "Prenotazione pagata", "booking_paid": "Prenotazione pagata",
@@ -456,6 +460,7 @@
"no_event_types_have_been_setup": "Questo utente non ha ancora impostato alcun tipo di evento.", "no_event_types_have_been_setup": "Questo utente non ha ancora impostato alcun tipo di evento.",
"edit_logo": "Modifica logo", "edit_logo": "Modifica logo",
"upload_a_logo": "Carica un logo", "upload_a_logo": "Carica un logo",
"upload_logo": "Carica logo",
"remove_logo": "Rimuovi logo", "remove_logo": "Rimuovi logo",
"enable": "Abilita", "enable": "Abilita",
"code": "Codice", "code": "Codice",
@@ -568,6 +573,7 @@
"your_team_name": "Nome del tuo team", "your_team_name": "Nome del tuo team",
"team_updated_successfully": "Team aggiornato con successo", "team_updated_successfully": "Team aggiornato con successo",
"your_team_updated_successfully": "Il tuo team è stato aggiornato con successo.", "your_team_updated_successfully": "Il tuo team è stato aggiornato con successo.",
"your_org_updated_successfully": "La tua organizzazione è stata aggiornata con successo.",
"about": "Informazioni", "about": "Informazioni",
"team_description": "Alcune frasi sul tuo team. Appariranno nella pagina URL del team.", "team_description": "Alcune frasi sul tuo team. Appariranno nella pagina URL del team.",
"org_description": "Qualche informazione sulla tua organizzazione. Le informazioni appariranno nella pagina di profilo dell'organizzazione.", "org_description": "Qualche informazione sulla tua organizzazione. Le informazioni appariranno nella pagina di profilo dell'organizzazione.",
@@ -599,6 +605,7 @@
"hide_book_a_team_member": "Nascondi pulsante Prenota un membro del team", "hide_book_a_team_member": "Nascondi pulsante Prenota un membro del team",
"hide_book_a_team_member_description": "Nasconde il pulsante Prenota un membro del team nelle pagine pubbliche.", "hide_book_a_team_member_description": "Nasconde il pulsante Prenota un membro del team nelle pagine pubbliche.",
"danger_zone": "Zona pericolosa", "danger_zone": "Zona pericolosa",
"account_deletion_cannot_be_undone": "Attenzione. Non è possibile annullare l'eliminazione dell'account.",
"back": "Indietro", "back": "Indietro",
"cancel": "Annulla", "cancel": "Annulla",
"cancel_all_remaining": "Annulla tutti i rimanenti", "cancel_all_remaining": "Annulla tutti i rimanenti",
@@ -688,6 +695,7 @@
"people": "Persone", "people": "Persone",
"your_email": "La Tua Email", "your_email": "La Tua Email",
"change_avatar": "Cambia Avatar", "change_avatar": "Cambia Avatar",
"upload_avatar": "Carica avatar",
"language": "Lingua", "language": "Lingua",
"timezone": "Timezone", "timezone": "Timezone",
"first_day_of_week": "Primo giorno della settimana", "first_day_of_week": "Primo giorno della settimana",
@@ -778,6 +786,7 @@
"disable_guests": "Disabilita Ospiti", "disable_guests": "Disabilita Ospiti",
"disable_guests_description": "Disabilita l'aggiunta di ulteriori ospiti durante la prenotazione.", "disable_guests_description": "Disabilita l'aggiunta di ulteriori ospiti durante la prenotazione.",
"private_link": "Genera URL privato", "private_link": "Genera URL privato",
"enable_private_url": "Abilita URL privato",
"private_link_label": "Link privato", "private_link_label": "Link privato",
"private_link_hint": "Il tuo link privato verrà rigenerato dopo ogni utilizzo", "private_link_hint": "Il tuo link privato verrà rigenerato dopo ogni utilizzo",
"copy_private_link": "Copia link privato", "copy_private_link": "Copia link privato",
@@ -840,6 +849,7 @@
"next_step": "Salta passo", "next_step": "Salta passo",
"prev_step": "Passo precedente", "prev_step": "Passo precedente",
"install": "Installa", "install": "Installa",
"install_paid_app": "Abbonati",
"installed": "Installato", "installed": "Installato",
"active_install_one": "{{count}} installazione attiva", "active_install_one": "{{count}} installazione attiva",
"active_install_other": "{{count}} installazioni attive", "active_install_other": "{{count}} installazioni attive",
@@ -1088,6 +1098,7 @@
"developer_documentation": "Documentazione sviluppatore", "developer_documentation": "Documentazione sviluppatore",
"get_in_touch": "Contattaci", "get_in_touch": "Contattaci",
"contact_support": "Contatta il supporto", "contact_support": "Contatta il supporto",
"community_support": "Supporto della community",
"feedback": "Feedback", "feedback": "Feedback",
"submitted_feedback": "Grazie per il tuo feedback!", "submitted_feedback": "Grazie per il tuo feedback!",
"feedback_error": "Errore durante l'invio del feedback", "feedback_error": "Errore durante l'invio del feedback",
@@ -1213,6 +1224,7 @@
"organizer_name_variable": "Nome organizzatore", "organizer_name_variable": "Nome organizzatore",
"app_upgrade_description": "Per poter utilizzare questa funzionalità, è necessario passare a un account Pro.", "app_upgrade_description": "Per poter utilizzare questa funzionalità, è necessario passare a un account Pro.",
"invalid_number": "Numero di telefono non valido", "invalid_number": "Numero di telefono non valido",
"invalid_url_error_message": "URL non valido per {{label}}. URL di esempio: {{sampleUrl}}",
"navigate": "Esplora", "navigate": "Esplora",
"open": "Apri", "open": "Apri",
"close": "Chiudi", "close": "Chiudi",
@@ -1276,6 +1288,7 @@
"personal_cal_url": "Il mio URL personale di {{appName}}", "personal_cal_url": "Il mio URL personale di {{appName}}",
"bio_hint": "Scrivi qualcosa di te. Queste informazioni appariranno nella tua pagina personale.", "bio_hint": "Scrivi qualcosa di te. Queste informazioni appariranno nella tua pagina personale.",
"user_has_no_bio": "Questo utente non ha ancora aggiunto una biografia.", "user_has_no_bio": "Questo utente non ha ancora aggiunto una biografia.",
"bio": "Biografia",
"delete_account_modal_title": "Elimina account", "delete_account_modal_title": "Elimina account",
"confirm_delete_account_modal": "Eliminare il tuo account {{appName}}?", "confirm_delete_account_modal": "Eliminare il tuo account {{appName}}?",
"delete_my_account": "Elimina il mio account", "delete_my_account": "Elimina il mio account",
@@ -1286,6 +1299,7 @@
"select_calendars": "Seleziona su quali calendari desideri controllare i conflitti per evitare doppie prenotazioni.", "select_calendars": "Seleziona su quali calendari desideri controllare i conflitti per evitare doppie prenotazioni.",
"check_for_conflicts": "Controlla conflitti", "check_for_conflicts": "Controlla conflitti",
"view_recordings": "Visualizza registrazioni", "view_recordings": "Visualizza registrazioni",
"check_for_recordings": "Controlla le registrazioni",
"adding_events_to": "Aggiungendo eventi a", "adding_events_to": "Aggiungendo eventi a",
"follow_system_preferences": "Segui le preferenze di sistema", "follow_system_preferences": "Segui le preferenze di sistema",
"custom_brand_colors": "Colori del marchio personalizzati", "custom_brand_colors": "Colori del marchio personalizzati",
@@ -1530,6 +1544,7 @@
"problem_registering_domain": "Si è verificato un problema durante la registrazione del sottodominio, riprova o contatta un amministratore", "problem_registering_domain": "Si è verificato un problema durante la registrazione del sottodominio, riprova o contatta un amministratore",
"team_publish": "Pubblica team", "team_publish": "Pubblica team",
"number_text_notifications": "Numero di telefono (notifiche di testo)", "number_text_notifications": "Numero di telefono (notifiche di testo)",
"number_sms_notifications": "Numero di telefono (notifiche SMS)",
"attendee_email_variable": "E-mail partecipante", "attendee_email_variable": "E-mail partecipante",
"attendee_email_info": "E-mail della persona che prenota", "attendee_email_info": "E-mail della persona che prenota",
"kbar_search_placeholder": "Digita un comando o esegui una ricerca...", "kbar_search_placeholder": "Digita un comando o esegui una ricerca...",
@@ -1594,6 +1609,7 @@
"options": "Opzioni", "options": "Opzioni",
"enter_option": "Immetti opzione {{index}}", "enter_option": "Immetti opzione {{index}}",
"add_an_option": "Aggiungi un'opzione", "add_an_option": "Aggiungi un'opzione",
"location_already_exists": "Questa posizione esiste già. Seleziona una nuova posizione",
"radio": "Pulsante di opzione", "radio": "Pulsante di opzione",
"google_meet_warning": "Per usare Google Meet, è necessario impostare Google Calendar come calendario di destinazione", "google_meet_warning": "Per usare Google Meet, è necessario impostare Google Calendar come calendario di destinazione",
"individual": "Persona", "individual": "Persona",
@@ -1613,6 +1629,7 @@
"date_overrides_mark_all_day_unavailable_other": "Segna come non disponibile nelle date selezionate", "date_overrides_mark_all_day_unavailable_other": "Segna come non disponibile nelle date selezionate",
"date_overrides_add_btn": "Aggiungi configurazione data specifica", "date_overrides_add_btn": "Aggiungi configurazione data specifica",
"date_overrides_update_btn": "Aggiorna configurazione data specifica", "date_overrides_update_btn": "Aggiorna configurazione data specifica",
"date_successfully_added": "Configurazione delle date aggiunta correttamente",
"event_type_duplicate_copy_text": "{{slug}}-copia", "event_type_duplicate_copy_text": "{{slug}}-copia",
"set_as_default": "Imposta come predefinito", "set_as_default": "Imposta come predefinito",
"hide_eventtype_details": "Nascondi dettagli del tipo di evento", "hide_eventtype_details": "Nascondi dettagli del tipo di evento",
@@ -1639,6 +1656,7 @@
"minimum_round_robin_hosts_count": "Numero di organizzatori necessario per partecipare", "minimum_round_robin_hosts_count": "Numero di organizzatori necessario per partecipare",
"hosts": "Organizzatori", "hosts": "Organizzatori",
"upgrade_to_enable_feature": "Per abilitare questa funzione, è necessario creare un team. Fai clic per creare un team.", "upgrade_to_enable_feature": "Per abilitare questa funzione, è necessario creare un team. Fai clic per creare un team.",
"orgs_upgrade_to_enable_feature": "Per abilitare questa funzione, è necessario eseguire l'upgrade al nostro piano Enterprise.",
"new_attendee": "Nuovo partecipante", "new_attendee": "Nuovo partecipante",
"awaiting_approval": "In attesa di approvazione", "awaiting_approval": "In attesa di approvazione",
"requires_google_calendar": "L'app richiede una connessione a Google Calendar", "requires_google_calendar": "L'app richiede una connessione a Google Calendar",
@@ -1743,6 +1761,7 @@
"show_on_booking_page": "Mostra nella pagina di prenotazione", "show_on_booking_page": "Mostra nella pagina di prenotazione",
"get_started_zapier_templates": "Inizia con i modelli Zapier", "get_started_zapier_templates": "Inizia con i modelli Zapier",
"team_is_unpublished": "{{team}} non è pubblicato", "team_is_unpublished": "{{team}} non è pubblicato",
"org_is_unpublished_description": "Il link di questa organizzazione non è attualmente disponibile. Contatta il proprietario dell'organizzazione o chiedigli di pubblicarlo.",
"team_is_unpublished_description": "Questo link di {{entity}} non è attualmente disponibile. Contatta il proprietario di {{entity}} o chiedigli di pubblicarlo.", "team_is_unpublished_description": "Questo link di {{entity}} non è attualmente disponibile. Contatta il proprietario di {{entity}} o chiedigli di pubblicarlo.",
"team_member": "Membro del team", "team_member": "Membro del team",
"a_routing_form": "Un modulo di instradamento", "a_routing_form": "Un modulo di instradamento",
@@ -1877,6 +1896,7 @@
"edit_invite_link": "Modifica impostazioni link", "edit_invite_link": "Modifica impostazioni link",
"invite_link_copied": "Link d'invito copiato", "invite_link_copied": "Link d'invito copiato",
"invite_link_deleted": "Link d'invito eliminato", "invite_link_deleted": "Link d'invito eliminato",
"api_key_deleted": "Chiave API eliminata",
"invite_link_updated": "Impostazioni link d'invito salvate", "invite_link_updated": "Impostazioni link d'invito salvate",
"link_expires_after": "La scadenza dei link è impostata dopo...", "link_expires_after": "La scadenza dei link è impostata dopo...",
"one_day": "1 giorno", "one_day": "1 giorno",
@@ -2009,7 +2029,13 @@
"attendee_last_name_variable": "Cognome del partecipante", "attendee_last_name_variable": "Cognome del partecipante",
"attendee_first_name_info": "Nome della persona che prenota", "attendee_first_name_info": "Nome della persona che prenota",
"attendee_last_name_info": "Cognome della persona che prenota", "attendee_last_name_info": "Cognome della persona che prenota",
"your_monthly_digest": "Riepilogo mensile",
"member_name": "Nome del membro",
"most_popular_events": "Eventi più popolari",
"summary_of_events_for_your_team_for_the_last_30_days": "Ecco il riepilogo degli eventi popolari per il tuo team {{teamName}} negli ultimi 30 giorni",
"me": "Io", "me": "Io",
"monthly_digest_email": "E-mail riepilogo mensile",
"monthly_digest_email_for_teams": "E-mail di riepilogo mensili per i team",
"verify_team_tooltip": "Effettua la verifica del tuo team per abilitare l'invio di messaggi ai partecipanti", "verify_team_tooltip": "Effettua la verifica del tuo team per abilitare l'invio di messaggi ai partecipanti",
"member_removed": "Membro rimosso", "member_removed": "Membro rimosso",
"my_availability": "La mia disponibilità", "my_availability": "La mia disponibilità",
@@ -2039,13 +2065,41 @@
"team_no_event_types": "Questo team non ha nessun tipo di evento", "team_no_event_types": "Questo team non ha nessun tipo di evento",
"seat_options_doesnt_multiple_durations": "L'opzione di prenotazione dei posti non supporta durate multiple", "seat_options_doesnt_multiple_durations": "L'opzione di prenotazione dei posti non supporta durate multiple",
"include_calendar_event": "Includi evento del calendario", "include_calendar_event": "Includi evento del calendario",
"oAuth": "OAuth",
"recently_added": "Aggiunti di recente", "recently_added": "Aggiunti di recente",
"no_members_found": "Nessun membro trovato", "no_members_found": "Nessun membro trovato",
"event_setup_length_error": "Impostazione evento: la durata deve essere di almeno 1 minuto.", "event_setup_length_error": "Impostazione evento: la durata deve essere di almeno 1 minuto.",
"availability_schedules": "Calendario disponibilità", "availability_schedules": "Calendario disponibilità",
"unauthorized": "Non autorizzato",
"access_cal_account": "{{clientName}} vorrebbe accedere al tuo account {{appName}}",
"select_account_team": "Seleziona un account o un team",
"allow_client_to": "Ciò consentirà a {{clientName}} di",
"associate_with_cal_account": "Associarti con i tuoi dati personali da {{clientName}}",
"see_personal_info": "Vedere i tuoi dati personali, inclusi i dati personali che hai reso disponibili al pubblico",
"see_primary_email_address": "Vedere il tuo indirizzo e-mail principale",
"connect_installed_apps": "Connettersi alle tue applicazioni installate",
"access_event_type": "Leggere, modificare, eliminare i tuoi tipi di eventi",
"access_availability": "Leggere, modificare, eliminare la tua disponibilità",
"access_bookings": "Leggere, modificare, eliminare le tue prenotazioni",
"allow_client_to_do": "Consentire a {{clientName}} di farlo?",
"oauth_access_information": "Facendo clic su Consenti, consentirai a questa applicazione di usare i tuoi dati in conformità ai suoi termini di servizio e informativa sulla privacy. Puoi revocare l'accesso nelle impostazioni di {{appName}} nell'App Store.",
"allow": "Consenti",
"view_only_edit_availability_not_onboarded": "Questo utente non ha completato l'onboarding. Non sarai in grado di impostare la sua disponibilità fino a quando non avrà completato l'onboarding.", "view_only_edit_availability_not_onboarded": "Questo utente non ha completato l'onboarding. Non sarai in grado di impostare la sua disponibilità fino a quando non avrà completato l'onboarding.",
"view_only_edit_availability": "Stai visualizzando la disponibilità di questo utente. Puoi solo modificare la tua disponibilità.", "view_only_edit_availability": "Stai visualizzando la disponibilità di questo utente. Puoi solo modificare la tua disponibilità.",
"you_can_override_calendar_in_advanced_tab": "Puoi configurare queste impostazioni su base per evento nelle impostazioni avanzate di ciascun tipo di evento.",
"edit_users_availability": "Modifica la disponibilità dell'utente: {{username}}", "edit_users_availability": "Modifica la disponibilità dell'utente: {{username}}",
"resend_invitation": "Invia di nuovo l'invito",
"invitation_resent": "L'invito è stato inviato di nuovo.",
"add_client": "Aggiungi cliente",
"copy_client_secret_info": "Dopo aver copiato questa parola segreta non sarai più in grado di vederla",
"add_new_client": "Aggiungi nuovo cliente",
"this_app_is_not_setup_already": "Questa applicazione non è stata ancora impostata",
"as_csv": "come CSV",
"overlay_my_calendar": "Sovrapponi il mio calendario",
"overlay_my_calendar_toc": "Collegando il tuo calendario, accetti la nostra informativa sulla privacy e i termini di servizio. Puoi revocare l'accesso in qualsiasi momento.",
"view_overlay_calendar_events": "Visualizza gli eventi del tuo calendario per prevenire prenotazioni in conflitto.",
"lock_timezone_toggle_on_booking_page": "Blocca fuso orario nella pagina di prenotazione",
"description_lock_timezone_toggle_on_booking_page": "Per bloccare il fuso orario nella pagina di prenotazione, utile per gli eventi di persona.",
"extensive_whitelabeling": "Assistenza per l'onboarding e supporto tecnico dedicati", "extensive_whitelabeling": "Assistenza per l'onboarding e supporto tecnico dedicati",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Aggiungi le tue nuove stringhe qui sopra ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Aggiungi le tue nuove stringhe qui sopra ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
} }
@@ -268,6 +268,7 @@
"set_availability": "利用可否の設定", "set_availability": "利用可否の設定",
"availability_settings": "利用できる設定", "availability_settings": "利用できる設定",
"continue_without_calendar": "カレンダーなしで続行", "continue_without_calendar": "カレンダーなしで続行",
"continue_with": "{{appName}} で続行",
"connect_your_calendar": "カレンダーに接続", "connect_your_calendar": "カレンダーに接続",
"connect_your_video_app": "ビデオアプリを接続", "connect_your_video_app": "ビデオアプリを接続",
"connect_your_video_app_instructions": "ご利用のイベントの種類で動画アプリを使用するには、ビデオアプリを接続してください。", "connect_your_video_app_instructions": "ご利用のイベントの種類で動画アプリを使用するには、ビデオアプリを接続してください。",
@@ -288,6 +289,8 @@
"when": "日時", "when": "日時",
"where": "参加方法", "where": "参加方法",
"add_to_calendar": "カレンダーに追加", "add_to_calendar": "カレンダーに追加",
"add_to_calendar_description": "予約時にイベントを追加する場所を選びます。",
"add_events_to": "イベントの追加先",
"add_another_calendar": "別のカレンダーを追加", "add_another_calendar": "別のカレンダーを追加",
"other": "その他", "other": "その他",
"email_sign_in_subject": "{{appName}} のサインインリンク", "email_sign_in_subject": "{{appName}} のサインインリンク",
@@ -422,6 +425,7 @@
"booking_created": "予約を作成しました", "booking_created": "予約を作成しました",
"booking_rejected": "予約が拒否されました", "booking_rejected": "予約が拒否されました",
"booking_requested": "予約がリクエストされました", "booking_requested": "予約がリクエストされました",
"booking_payment_initiated": "予約に関するお支払いを開始しました",
"meeting_ended": "ミーティングが終了しました", "meeting_ended": "ミーティングが終了しました",
"form_submitted": "フォームが送信されました", "form_submitted": "フォームが送信されました",
"booking_paid": "予約の支払いが済みました", "booking_paid": "予約の支払いが済みました",
@@ -456,6 +460,7 @@
"no_event_types_have_been_setup": "このユーザーはまだイベントタイプを設定していません。", "no_event_types_have_been_setup": "このユーザーはまだイベントタイプを設定していません。",
"edit_logo": "ロゴを編集", "edit_logo": "ロゴを編集",
"upload_a_logo": "ロゴをアップロード", "upload_a_logo": "ロゴをアップロード",
"upload_logo": "ロゴをアップロード",
"remove_logo": "ロゴを削除する", "remove_logo": "ロゴを削除する",
"enable": "有効にする", "enable": "有効にする",
"code": "コード", "code": "コード",
@@ -568,6 +573,7 @@
"your_team_name": "チーム名", "your_team_name": "チーム名",
"team_updated_successfully": "チームが正常に更新されました", "team_updated_successfully": "チームが正常に更新されました",
"your_team_updated_successfully": "チームが正常に更新されました。", "your_team_updated_successfully": "チームが正常に更新されました。",
"your_org_updated_successfully": "組織は正常に更新されました。",
"about": "このアプリについて", "about": "このアプリについて",
"team_description": "あなたのチームについての簡単な説明文です。あなたのチームの URL ページに表示されます。", "team_description": "あなたのチームについての簡単な説明文です。あなたのチームの URL ページに表示されます。",
"org_description": "あなたの組織についての簡単な説明文です。あなたの組織の URL ページに表示されます。", "org_description": "あなたの組織についての簡単な説明文です。あなたの組織の URL ページに表示されます。",
@@ -599,6 +605,7 @@
"hide_book_a_team_member": "「チームメンバーを予約する」ボタンを非表示にする", "hide_book_a_team_member": "「チームメンバーを予約する」ボタンを非表示にする",
"hide_book_a_team_member_description": "公開ページから「チームメンバーを予約する」ボタンを非表示にします。", "hide_book_a_team_member_description": "公開ページから「チームメンバーを予約する」ボタンを非表示にします。",
"danger_zone": "危険ゾーン", "danger_zone": "危険ゾーン",
"account_deletion_cannot_be_undone": "ご注意ください。アカウントの削除は元に戻せません。",
"back": "戻る", "back": "戻る",
"cancel": "キャンセル", "cancel": "キャンセル",
"cancel_all_remaining": "残りをすべてキャンセル", "cancel_all_remaining": "残りをすべてキャンセル",
@@ -688,6 +695,7 @@
"people": "ユーザー", "people": "ユーザー",
"your_email": "あなたのメールアドレス", "your_email": "あなたのメールアドレス",
"change_avatar": "アバターを変更", "change_avatar": "アバターを変更",
"upload_avatar": "アバターをアップロード",
"language": "言語", "language": "言語",
"timezone": "タイムゾーン", "timezone": "タイムゾーン",
"first_day_of_week": "週の最初の日", "first_day_of_week": "週の最初の日",
@@ -778,6 +786,7 @@
"disable_guests": "ゲストを無効化", "disable_guests": "ゲストを無効化",
"disable_guests_description": "予約中のゲストの追加を無効にします。", "disable_guests_description": "予約中のゲストの追加を無効にします。",
"private_link": "プライベートリンクを生成", "private_link": "プライベートリンクを生成",
"enable_private_url": "プライベート URL を有効にする",
"private_link_label": "プライベートリンク", "private_link_label": "プライベートリンク",
"private_link_hint": "プライベートリンクは使用するたびに再生成されます", "private_link_hint": "プライベートリンクは使用するたびに再生成されます",
"copy_private_link": "プライベートリンクをコピー", "copy_private_link": "プライベートリンクをコピー",
@@ -840,6 +849,7 @@
"next_step": "手順をスキップ", "next_step": "手順をスキップ",
"prev_step": "前の手順", "prev_step": "前の手順",
"install": "インストール", "install": "インストール",
"install_paid_app": "サブスクライブ",
"installed": "インストール済み", "installed": "インストール済み",
"active_install_one": "{{count}} 件のアクティブなインストール", "active_install_one": "{{count}} 件のアクティブなインストール",
"active_install_other": "{{count}} 件のアクティブなインストール", "active_install_other": "{{count}} 件のアクティブなインストール",
@@ -1088,6 +1098,7 @@
"developer_documentation": "開発者向けドキュメント", "developer_documentation": "開発者向けドキュメント",
"get_in_touch": "お問い合わせ", "get_in_touch": "お問い合わせ",
"contact_support": "サポートに連絡", "contact_support": "サポートに連絡",
"community_support": "コミュニティサポート",
"feedback": "フィードバック", "feedback": "フィードバック",
"submitted_feedback": "フィードバックをありがとうございます!", "submitted_feedback": "フィードバックをありがとうございます!",
"feedback_error": "フィードバックの送信エラー", "feedback_error": "フィードバックの送信エラー",
@@ -1213,6 +1224,7 @@
"organizer_name_variable": "主催者名", "organizer_name_variable": "主催者名",
"app_upgrade_description": "この機能を利用するには、Pro アカウントへのアップグレードが必要です。", "app_upgrade_description": "この機能を利用するには、Pro アカウントへのアップグレードが必要です。",
"invalid_number": "電話番号が無効です", "invalid_number": "電話番号が無効です",
"invalid_url_error_message": "{{label}} の無効な URL です。サンプル URL:{{sampleUrl}}",
"navigate": "ナビゲート", "navigate": "ナビゲート",
"open": "開く", "open": "開く",
"close": "閉じる", "close": "閉じる",
@@ -1276,6 +1288,7 @@
"personal_cal_url": "私の個人 {{appName}} URL", "personal_cal_url": "私の個人 {{appName}} URL",
"bio_hint": "あなたに関する簡潔な説明。これはあなたの個人 URL ページに表示されます。", "bio_hint": "あなたに関する簡潔な説明。これはあなたの個人 URL ページに表示されます。",
"user_has_no_bio": "このユーザーはまだ経歴を追加していません。", "user_has_no_bio": "このユーザーはまだ経歴を追加していません。",
"bio": "経歴",
"delete_account_modal_title": "アカウントを削除する", "delete_account_modal_title": "アカウントを削除する",
"confirm_delete_account_modal": "{{appName}} アカウントを削除してもよろしいですか?", "confirm_delete_account_modal": "{{appName}} アカウントを削除してもよろしいですか?",
"delete_my_account": "アカウントを削除する", "delete_my_account": "アカウントを削除する",
@@ -1286,6 +1299,7 @@
"select_calendars": "ダブルブッキングを防ぐために、スケジュールの重なりをチェックするカレンダーを選択してください。", "select_calendars": "ダブルブッキングを防ぐために、スケジュールの重なりをチェックするカレンダーを選択してください。",
"check_for_conflicts": "スケジュールの重なりをチェック", "check_for_conflicts": "スケジュールの重なりをチェック",
"view_recordings": "録音を表示", "view_recordings": "録音を表示",
"check_for_recordings": "レコーディングを確認",
"adding_events_to": "イベントの追加先", "adding_events_to": "イベントの追加先",
"follow_system_preferences": "システム環境設定に従う", "follow_system_preferences": "システム環境設定に従う",
"custom_brand_colors": "カスタムブランドカラー", "custom_brand_colors": "カスタムブランドカラー",
@@ -1530,6 +1544,7 @@
"problem_registering_domain": "サブドメインの登録時に問題が発生しました。もう一度お試しいただくか、管理者までお問い合せください", "problem_registering_domain": "サブドメインの登録時に問題が発生しました。もう一度お試しいただくか、管理者までお問い合せください",
"team_publish": "チームを公開", "team_publish": "チームを公開",
"number_text_notifications": "電話番号 (テキスト通知)", "number_text_notifications": "電話番号 (テキスト通知)",
"number_sms_notifications": "電話番号(SMS 通知)",
"attendee_email_variable": "出席者のメールアドレス", "attendee_email_variable": "出席者のメールアドレス",
"attendee_email_info": "予約者のメールアドレス", "attendee_email_info": "予約者のメールアドレス",
"kbar_search_placeholder": "コマンドを入力するか、検索してください...", "kbar_search_placeholder": "コマンドを入力するか、検索してください...",
@@ -1594,6 +1609,7 @@
"options": "オプション", "options": "オプション",
"enter_option": "オプション {{index}} を入力してください", "enter_option": "オプション {{index}} を入力してください",
"add_an_option": "オプションを追加", "add_an_option": "オプションを追加",
"location_already_exists": "この場所はすでに存在します。新しい場所を選んでください",
"radio": "ラジオボタン", "radio": "ラジオボタン",
"google_meet_warning": "Google Meet を使用するには、目的のカレンダーを Google カレンダーに設定する必要があります", "google_meet_warning": "Google Meet を使用するには、目的のカレンダーを Google カレンダーに設定する必要があります",
"individual": "個人", "individual": "個人",
@@ -1613,6 +1629,7 @@
"date_overrides_mark_all_day_unavailable_other": "選択した日付を参加不可としてマーク", "date_overrides_mark_all_day_unavailable_other": "選択した日付を参加不可としてマーク",
"date_overrides_add_btn": "上書きを追加", "date_overrides_add_btn": "上書きを追加",
"date_overrides_update_btn": "上書きを更新", "date_overrides_update_btn": "上書きを更新",
"date_successfully_added": "日付の上書きが正常に追加されました",
"event_type_duplicate_copy_text": "{{slug}}-copy", "event_type_duplicate_copy_text": "{{slug}}-copy",
"set_as_default": "デフォルトとして設定", "set_as_default": "デフォルトとして設定",
"hide_eventtype_details": "イベントの種類の詳細を非表示にする", "hide_eventtype_details": "イベントの種類の詳細を非表示にする",
@@ -1639,6 +1656,7 @@
"minimum_round_robin_hosts_count": "出席が必要なホストの数", "minimum_round_robin_hosts_count": "出席が必要なホストの数",
"hosts": "ホスト", "hosts": "ホスト",
"upgrade_to_enable_feature": "この機能を有効にするには、チームを作成する必要があります。クリックしてチームを作成してください。", "upgrade_to_enable_feature": "この機能を有効にするには、チームを作成する必要があります。クリックしてチームを作成してください。",
"orgs_upgrade_to_enable_feature": "この機能を有効にするには Enterprise プランにアップグレードする必要があります。",
"new_attendee": "新規参加者", "new_attendee": "新規参加者",
"awaiting_approval": "承認を待っています", "awaiting_approval": "承認を待っています",
"requires_google_calendar": "このアプリは Google カレンダーとの接続が必要です", "requires_google_calendar": "このアプリは Google カレンダーとの接続が必要です",
@@ -1743,6 +1761,7 @@
"show_on_booking_page": "予約ページに表示", "show_on_booking_page": "予約ページに表示",
"get_started_zapier_templates": "Zapier テンプレートの使用を開始する", "get_started_zapier_templates": "Zapier テンプレートの使用を開始する",
"team_is_unpublished": "{{team}} は公開されていません", "team_is_unpublished": "{{team}} は公開されていません",
"org_is_unpublished_description": "この組織のリンクは現在利用できません。組織の所有者に連絡するか、リンクを公開するよう依頼してください。",
"team_is_unpublished_description": "この {{entity}} のリンクは現在利用できません。{{entity}} の所有者に問い合わせるか、リンクを公開するように依頼してください。", "team_is_unpublished_description": "この {{entity}} のリンクは現在利用できません。{{entity}} の所有者に問い合わせるか、リンクを公開するように依頼してください。",
"team_member": "チームメンバー", "team_member": "チームメンバー",
"a_routing_form": "ルーティングフォーム", "a_routing_form": "ルーティングフォーム",
@@ -1877,6 +1896,7 @@
"edit_invite_link": "リンクの設定を編集する", "edit_invite_link": "リンクの設定を編集する",
"invite_link_copied": "招待リンクをコピーしました", "invite_link_copied": "招待リンクをコピーしました",
"invite_link_deleted": "招待リンクを削除しました", "invite_link_deleted": "招待リンクを削除しました",
"api_key_deleted": "API キーを削除しました",
"invite_link_updated": "招待リンクの設定を保存しました", "invite_link_updated": "招待リンクの設定を保存しました",
"link_expires_after": "リンクの期限切れまで...", "link_expires_after": "リンクの期限切れまで...",
"one_day": "1 日", "one_day": "1 日",
@@ -2009,7 +2029,13 @@
"attendee_last_name_variable": "出席者の姓", "attendee_last_name_variable": "出席者の姓",
"attendee_first_name_info": "予約者の名", "attendee_first_name_info": "予約者の名",
"attendee_last_name_info": "予約者の姓", "attendee_last_name_info": "予約者の姓",
"your_monthly_digest": "月 1 回のダイジェスト",
"member_name": "メンバーの名前",
"most_popular_events": "最も人気のイベント",
"summary_of_events_for_your_team_for_the_last_30_days": "こちらはこの 30 日間、チーム {{teamName}} で最も人気があったイベントのサマリーです",
"me": "私", "me": "私",
"monthly_digest_email": "月 1 回のダイジェストのメール",
"monthly_digest_email_for_teams": "チームのための月 1 回のダイジェストのメール",
"verify_team_tooltip": "出席者へのメッセージ送信ができるようにするには、チームを確認してください", "verify_team_tooltip": "出席者へのメッセージ送信ができるようにするには、チームを確認してください",
"member_removed": "メンバーが削除されました", "member_removed": "メンバーが削除されました",
"my_availability": "私の空き状況", "my_availability": "私の空き状況",
@@ -2039,13 +2065,41 @@
"team_no_event_types": "このチームにはイベントタイプはありません", "team_no_event_types": "このチームにはイベントタイプはありません",
"seat_options_doesnt_multiple_durations": "座席オプションは複数の期間をサポートしていません", "seat_options_doesnt_multiple_durations": "座席オプションは複数の期間をサポートしていません",
"include_calendar_event": "カレンダーのイベントを含める", "include_calendar_event": "カレンダーのイベントを含める",
"oAuth": "OAuth",
"recently_added": "最近追加されました", "recently_added": "最近追加されました",
"no_members_found": "メンバーが見つかりません", "no_members_found": "メンバーが見つかりません",
"event_setup_length_error": "イベント設定:時間は 1 分以上でなくてはいけません。", "event_setup_length_error": "イベント設定:時間は 1 分以上でなくてはいけません。",
"availability_schedules": "空き状況一覧", "availability_schedules": "空き状況一覧",
"unauthorized": "権限がありません",
"access_cal_account": "{{clientName}}があなたの {{appName}} アカウントへのアクセスを求めています",
"select_account_team": "アカウントまたはチームを選択",
"allow_client_to": "これにより {{clientName}} は",
"associate_with_cal_account": "あなたと {{clientName}} からのあなたの個人情報を関連づけられます",
"see_personal_info": "あなたの個人情報(あなたがこれまでに公開したあなたの個人情報など)を表示",
"see_primary_email_address": "プライマリメールアドレスを表示",
"connect_installed_apps": "インストールしたアプリに接続",
"access_event_type": "イベントタイプを読み取り、編集し、削除する",
"access_availability": "空き状況を読み取り、編集し、削除する",
"access_bookings": "予約を読み取り、編集し、削除する",
"allow_client_to_do": "{{clientName}} にこれの実行を許可しますか?",
"oauth_access_information": "「許可」をクリックすると、このアプリのサービス利用規約とプライバシーポリシーに従って、アプリにあなたの個人情報の使用を許可することになります。{{appName}} の App Store でアクセスを削除できます。",
"allow": "許可",
"view_only_edit_availability_not_onboarded": "このユーザーはオンボーディングを完了していません。オンボーディングを完了するまで、ユーザーの空き状況は設定できません。", "view_only_edit_availability_not_onboarded": "このユーザーはオンボーディングを完了していません。オンボーディングを完了するまで、ユーザーの空き状況は設定できません。",
"view_only_edit_availability": "このユーザーの空き状況を表示しています。編集できるのは自分の空き状況だけです。", "view_only_edit_availability": "このユーザーの空き状況を表示しています。編集できるのは自分の空き状況だけです。",
"you_can_override_calendar_in_advanced_tab": "各イベントタイプの詳細設定で、イベントごとにこれを上書きすることができます。",
"edit_users_availability": "ユーザーの空き状況を編集:{{username}}", "edit_users_availability": "ユーザーの空き状況を編集:{{username}}",
"resend_invitation": "招待を再送",
"invitation_resent": "招待は再送されました。",
"add_client": "顧客を追加",
"copy_client_secret_info": "このシークレットをコピーすると、もう表示できなくなります",
"add_new_client": "新しい顧客を追加",
"this_app_is_not_setup_already": "このアプリはまだ設定されていません",
"as_csv": "CSV として",
"overlay_my_calendar": "カレンダーを重ね合わせる",
"overlay_my_calendar_toc": "カレンダーに接続することで、弊社のプライバシーポリシーと利用規約に同意することになります。アクセスはいつでも取り消せます。",
"view_overlay_calendar_events": "カレンダーのイベントを表示して予約が重ならないようにします。",
"lock_timezone_toggle_on_booking_page": "予約ページのタイムゾーンを固定する",
"description_lock_timezone_toggle_on_booking_page": "予約ページのタイムゾーンを固定するためのもので、対面のイベントに役立ちます。",
"extensive_whitelabeling": "専用のオンボーディングサポートとエンジニアリングサポート", "extensive_whitelabeling": "専用のオンボーディングサポートとエンジニアリングサポート",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
} }
@@ -849,6 +849,7 @@
"next_step": "건너뛰기", "next_step": "건너뛰기",
"prev_step": "이전 단계", "prev_step": "이전 단계",
"install": "설치", "install": "설치",
"install_paid_app": "구독",
"installed": "설치됨", "installed": "설치됨",
"active_install_one": "{{count}}개 활성 설치", "active_install_one": "{{count}}개 활성 설치",
"active_install_other": "{{count}}개 활성 설치", "active_install_other": "{{count}}개 활성 설치",
@@ -1097,6 +1098,7 @@
"developer_documentation": "개발자 문서", "developer_documentation": "개발자 문서",
"get_in_touch": "연락하기", "get_in_touch": "연락하기",
"contact_support": "지원 문의", "contact_support": "지원 문의",
"community_support": "커뮤니티 지원",
"feedback": "피드백", "feedback": "피드백",
"submitted_feedback": "피드백을 주셔서 감사합니다!", "submitted_feedback": "피드백을 주셔서 감사합니다!",
"feedback_error": "피드백을 보내는 중 오류 발생", "feedback_error": "피드백을 보내는 중 오류 발생",
@@ -268,6 +268,7 @@
"set_availability": "Beschikbaarheid bepalen", "set_availability": "Beschikbaarheid bepalen",
"availability_settings": "Beschikbaarheidsinstellingen", "availability_settings": "Beschikbaarheidsinstellingen",
"continue_without_calendar": "Doorgaan zonder kalender", "continue_without_calendar": "Doorgaan zonder kalender",
"continue_with": "Ga door met {{appName}}",
"connect_your_calendar": "Koppel uw agenda", "connect_your_calendar": "Koppel uw agenda",
"connect_your_video_app": "Koppel uw videoapps", "connect_your_video_app": "Koppel uw videoapps",
"connect_your_video_app_instructions": "Koppel uw videoapps om ze op uw gebeurtenistypes te gebruiken.", "connect_your_video_app_instructions": "Koppel uw videoapps om ze op uw gebeurtenistypes te gebruiken.",
@@ -288,6 +289,8 @@
"when": "Wanneer", "when": "Wanneer",
"where": "Waar", "where": "Waar",
"add_to_calendar": "Toevoegen aan kalender", "add_to_calendar": "Toevoegen aan kalender",
"add_to_calendar_description": "Selecteer waar u gebeurtenissen wilt toevoegen wanneer u geboekt bent.",
"add_events_to": "Voeg gebeurtenissen toe aan",
"add_another_calendar": "Andere agenda toevoegen", "add_another_calendar": "Andere agenda toevoegen",
"other": "Overige", "other": "Overige",
"email_sign_in_subject": "Uw aanmeldingslink voor {{appName}}", "email_sign_in_subject": "Uw aanmeldingslink voor {{appName}}",
@@ -422,6 +425,7 @@
"booking_created": "Boeking aangemaakt", "booking_created": "Boeking aangemaakt",
"booking_rejected": "Boeking geweigerd", "booking_rejected": "Boeking geweigerd",
"booking_requested": "Boeking aangevraagd", "booking_requested": "Boeking aangevraagd",
"booking_payment_initiated": "Betaling boeking gestart",
"meeting_ended": "Vergadering beëindigd", "meeting_ended": "Vergadering beëindigd",
"form_submitted": "Formulier verzonden", "form_submitted": "Formulier verzonden",
"booking_paid": "Boeking betaald", "booking_paid": "Boeking betaald",
@@ -456,6 +460,7 @@
"no_event_types_have_been_setup": "Deze gebruiker heeft nog geen evenementen.", "no_event_types_have_been_setup": "Deze gebruiker heeft nog geen evenementen.",
"edit_logo": "Bewerk logo", "edit_logo": "Bewerk logo",
"upload_a_logo": "Logo uploaden", "upload_a_logo": "Logo uploaden",
"upload_logo": "Logo uploaden",
"remove_logo": "Logo verwijderen", "remove_logo": "Logo verwijderen",
"enable": "Inschakelen", "enable": "Inschakelen",
"code": "Code", "code": "Code",
@@ -568,6 +573,7 @@
"your_team_name": "Naam van uw team", "your_team_name": "Naam van uw team",
"team_updated_successfully": "Team bijgewerkt", "team_updated_successfully": "Team bijgewerkt",
"your_team_updated_successfully": "Uw team is succesvol bijgewerkt.", "your_team_updated_successfully": "Uw team is succesvol bijgewerkt.",
"your_org_updated_successfully": "Uw organisatie is bijgewerkt.",
"about": "Introductie", "about": "Introductie",
"team_description": "Een paar zinnen over uw team. Dit is zichtbaar op uw teampagina.", "team_description": "Een paar zinnen over uw team. Dit is zichtbaar op uw teampagina.",
"org_description": "Een paar zinnen over uw organisatie. Dit wordt weergegeven op op uw organisatiepagina.", "org_description": "Een paar zinnen over uw organisatie. Dit wordt weergegeven op op uw organisatiepagina.",
@@ -599,6 +605,7 @@
"hide_book_a_team_member": "Knop 'Teamlid boeken' verbergen", "hide_book_a_team_member": "Knop 'Teamlid boeken' verbergen",
"hide_book_a_team_member_description": "Verberg de knop 'Teamlid boeken' op uw openbare pagina's.", "hide_book_a_team_member_description": "Verberg de knop 'Teamlid boeken' op uw openbare pagina's.",
"danger_zone": "Gevarenzone", "danger_zone": "Gevarenzone",
"account_deletion_cannot_be_undone": "Wees voorzichtig. Het verwijderen van een account kan niet ongedaan worden gemaakt.",
"back": "Terug", "back": "Terug",
"cancel": "Annuleren", "cancel": "Annuleren",
"cancel_all_remaining": "Alle resterende annuleren", "cancel_all_remaining": "Alle resterende annuleren",
@@ -688,6 +695,7 @@
"people": "Mensen", "people": "Mensen",
"your_email": "Uw E-mailadres", "your_email": "Uw E-mailadres",
"change_avatar": "Wijzig avatar", "change_avatar": "Wijzig avatar",
"upload_avatar": "Avatar uploaden",
"language": "Taal", "language": "Taal",
"timezone": "Tijdzone", "timezone": "Tijdzone",
"first_day_of_week": "Eerste dag van de week", "first_day_of_week": "Eerste dag van de week",
@@ -778,6 +786,7 @@
"disable_guests": "Gasten uitschakelen", "disable_guests": "Gasten uitschakelen",
"disable_guests_description": "Schakel toevoegen van extra gasten uit tijdens het boeken.", "disable_guests_description": "Schakel toevoegen van extra gasten uit tijdens het boeken.",
"private_link": "Privélink genereren", "private_link": "Privélink genereren",
"enable_private_url": "Privé-URL inschakelen",
"private_link_label": "Privélink", "private_link_label": "Privélink",
"private_link_hint": "Uw privélink wordt na elk gebruik opnieuw gegenereerd", "private_link_hint": "Uw privélink wordt na elk gebruik opnieuw gegenereerd",
"copy_private_link": "Privélink kopiëren", "copy_private_link": "Privélink kopiëren",
@@ -840,6 +849,7 @@
"next_step": "Stap overslaan", "next_step": "Stap overslaan",
"prev_step": "Vorige stap", "prev_step": "Vorige stap",
"install": "Installeren", "install": "Installeren",
"install_paid_app": "Abonneren",
"installed": "Geinstalleerd", "installed": "Geinstalleerd",
"active_install_one": "{{count}} actieve installatie", "active_install_one": "{{count}} actieve installatie",
"active_install_other": "{{count}} actieve installaties", "active_install_other": "{{count}} actieve installaties",
@@ -1088,6 +1098,7 @@
"developer_documentation": "Ontwikkelaarsdocumentatie", "developer_documentation": "Ontwikkelaarsdocumentatie",
"get_in_touch": "Neem contact op", "get_in_touch": "Neem contact op",
"contact_support": "Neem contact op met de ondersteuning", "contact_support": "Neem contact op met de ondersteuning",
"community_support": "Ondersteuning door de community",
"feedback": "Feedback", "feedback": "Feedback",
"submitted_feedback": "Bedankt voor uw feedback!", "submitted_feedback": "Bedankt voor uw feedback!",
"feedback_error": "Fout bij verzenden van feedback", "feedback_error": "Fout bij verzenden van feedback",
@@ -1213,6 +1224,7 @@
"organizer_name_variable": "Naam organisator", "organizer_name_variable": "Naam organisator",
"app_upgrade_description": "Om deze functie te gebruiken, moet u upgraden naar een Pro-account.", "app_upgrade_description": "Om deze functie te gebruiken, moet u upgraden naar een Pro-account.",
"invalid_number": "Ongeldig telefoonnummer", "invalid_number": "Ongeldig telefoonnummer",
"invalid_url_error_message": "Ongeldige URL voor {{label}}. Voorbeeld-URL: {{sampleUrl}}",
"navigate": "Navigeren", "navigate": "Navigeren",
"open": "Openen", "open": "Openen",
"close": "Sluiten", "close": "Sluiten",
@@ -1276,6 +1288,7 @@
"personal_cal_url": "Mijn persoonlijke {{appName}}-URL", "personal_cal_url": "Mijn persoonlijke {{appName}}-URL",
"bio_hint": "Een korte introductie over uzelf. Dit wordt weergegeven op uw persoonlijke pagina.", "bio_hint": "Een korte introductie over uzelf. Dit wordt weergegeven op uw persoonlijke pagina.",
"user_has_no_bio": "Deze gebruiker heeft nog geen bio toegevoegd.", "user_has_no_bio": "Deze gebruiker heeft nog geen bio toegevoegd.",
"bio": "Bio",
"delete_account_modal_title": "Account verwijderen", "delete_account_modal_title": "Account verwijderen",
"confirm_delete_account_modal": "Weet u zeker dat u uw {{appName}}-account wilt verwijderen?", "confirm_delete_account_modal": "Weet u zeker dat u uw {{appName}}-account wilt verwijderen?",
"delete_my_account": "Verwijder mijn account", "delete_my_account": "Verwijder mijn account",
@@ -1286,6 +1299,7 @@
"select_calendars": "Selecteer de agenda's die u wilt controleren op conflicten om dubbele boekingen te voorkomen.", "select_calendars": "Selecteer de agenda's die u wilt controleren op conflicten om dubbele boekingen te voorkomen.",
"check_for_conflicts": "Controleer op conflicten", "check_for_conflicts": "Controleer op conflicten",
"view_recordings": "Opnames weergeven", "view_recordings": "Opnames weergeven",
"check_for_recordings": "Controleer op opnames",
"adding_events_to": "Toevoegen van gebeurtenissen aan", "adding_events_to": "Toevoegen van gebeurtenissen aan",
"follow_system_preferences": "Volg systeemvoorkeuren", "follow_system_preferences": "Volg systeemvoorkeuren",
"custom_brand_colors": "Aangepaste merkkleuren", "custom_brand_colors": "Aangepaste merkkleuren",
@@ -1530,6 +1544,7 @@
"problem_registering_domain": "Er is een probleem opgetreden bij het registreren van het subdomein. Probeer het opnieuw of neem contact op met een beheerder", "problem_registering_domain": "Er is een probleem opgetreden bij het registreren van het subdomein. Probeer het opnieuw of neem contact op met een beheerder",
"team_publish": "Team publiceren", "team_publish": "Team publiceren",
"number_text_notifications": "Telefoonnummer (sms-meldingen)", "number_text_notifications": "Telefoonnummer (sms-meldingen)",
"number_sms_notifications": "Telefoonnummer (sms-meldingen)",
"attendee_email_variable": "E-mailadres deelnemer", "attendee_email_variable": "E-mailadres deelnemer",
"attendee_email_info": "Het e-mailadres van de persoon die boekt", "attendee_email_info": "Het e-mailadres van de persoon die boekt",
"kbar_search_placeholder": "Typ een opdracht of zoek...", "kbar_search_placeholder": "Typ een opdracht of zoek...",
@@ -1594,6 +1609,7 @@
"options": "Opties", "options": "Opties",
"enter_option": "Voer optie {{index}} in", "enter_option": "Voer optie {{index}} in",
"add_an_option": "Voeg een optie toe", "add_an_option": "Voeg een optie toe",
"location_already_exists": "Deze locatie bestaat al. Selecteer een nieuwe locatie",
"radio": "Radio", "radio": "Radio",
"google_meet_warning": "Om Google Meet te kunnen gebruiken, moet u uw bestemmingsagenda instellen op een Google-agenda", "google_meet_warning": "Om Google Meet te kunnen gebruiken, moet u uw bestemmingsagenda instellen op een Google-agenda",
"individual": "Individueel", "individual": "Individueel",
@@ -1613,6 +1629,7 @@
"date_overrides_mark_all_day_unavailable_other": "Markeren als niet beschikbaar op geselecteerde datums", "date_overrides_mark_all_day_unavailable_other": "Markeren als niet beschikbaar op geselecteerde datums",
"date_overrides_add_btn": "Overschrijving toevoegen", "date_overrides_add_btn": "Overschrijving toevoegen",
"date_overrides_update_btn": "Overschrijving bijwerken", "date_overrides_update_btn": "Overschrijving bijwerken",
"date_successfully_added": "Datumoverschrijing toegevoegd",
"event_type_duplicate_copy_text": "{{slug}}-kopie", "event_type_duplicate_copy_text": "{{slug}}-kopie",
"set_as_default": "Als standaard instellen", "set_as_default": "Als standaard instellen",
"hide_eventtype_details": "Gebeurtenistypegegevens verbergen", "hide_eventtype_details": "Gebeurtenistypegegevens verbergen",
@@ -1639,6 +1656,7 @@
"minimum_round_robin_hosts_count": "Aantal vereiste hosts voor deelname", "minimum_round_robin_hosts_count": "Aantal vereiste hosts voor deelname",
"hosts": "Hosts", "hosts": "Hosts",
"upgrade_to_enable_feature": "U moet een team maken om deze functie in te schakelen. Klik om een team te maken.", "upgrade_to_enable_feature": "U moet een team maken om deze functie in te schakelen. Klik om een team te maken.",
"orgs_upgrade_to_enable_feature": "U moet upgraden naar ons Enterprise-abonnement om deze functie in te schakelen.",
"new_attendee": "Nieuwe deelnemer", "new_attendee": "Nieuwe deelnemer",
"awaiting_approval": "In afwachting van goedkeuring", "awaiting_approval": "In afwachting van goedkeuring",
"requires_google_calendar": "Deze app vereist een Google Agenda-koppeling", "requires_google_calendar": "Deze app vereist een Google Agenda-koppeling",
@@ -1743,6 +1761,7 @@
"show_on_booking_page": "Weergeven op boekingspagina", "show_on_booking_page": "Weergeven op boekingspagina",
"get_started_zapier_templates": "Aan de slag met Zapier-sjablonen", "get_started_zapier_templates": "Aan de slag met Zapier-sjablonen",
"team_is_unpublished": "{{team}} is niet gepubliceerd", "team_is_unpublished": "{{team}} is niet gepubliceerd",
"org_is_unpublished_description": "Deze organisatiekoppeling is momenteel niet beschikbaar. Neem contact op met de organisatie-eigenaar of vraag om het te publiceren.",
"team_is_unpublished_description": "Deze {{entity}}-link is momenteel niet beschikbaar. Neem contact op met de {{entity}}-eigenaar of vraag hem het te publiceren.", "team_is_unpublished_description": "Deze {{entity}}-link is momenteel niet beschikbaar. Neem contact op met de {{entity}}-eigenaar of vraag hem het te publiceren.",
"team_member": "Teamlid", "team_member": "Teamlid",
"a_routing_form": "Een routeringsformulier", "a_routing_form": "Een routeringsformulier",
@@ -1877,6 +1896,7 @@
"edit_invite_link": "Linkinstellingen bewerken", "edit_invite_link": "Linkinstellingen bewerken",
"invite_link_copied": "Uitnodigingslink gekopieerd", "invite_link_copied": "Uitnodigingslink gekopieerd",
"invite_link_deleted": "Uitnodigingslink verwijderd", "invite_link_deleted": "Uitnodigingslink verwijderd",
"api_key_deleted": "API-sleutel verwijderd",
"invite_link_updated": "Uitnodigingslinkinstellingen opgeslagen", "invite_link_updated": "Uitnodigingslinkinstellingen opgeslagen",
"link_expires_after": "Links verlopen na...", "link_expires_after": "Links verlopen na...",
"one_day": "1 dag", "one_day": "1 dag",
@@ -2009,7 +2029,13 @@
"attendee_last_name_variable": "Achternaam deelnemer", "attendee_last_name_variable": "Achternaam deelnemer",
"attendee_first_name_info": "De voornaam van de persoon die boekt", "attendee_first_name_info": "De voornaam van de persoon die boekt",
"attendee_last_name_info": "De achternaam van de persoon die boekt", "attendee_last_name_info": "De achternaam van de persoon die boekt",
"your_monthly_digest": "Uw maandelijks overzicht",
"member_name": "Lidnaam",
"most_popular_events": "Populairste gebeurtenissen",
"summary_of_events_for_your_team_for_the_last_30_days": "Dit is uw overzicht van populaire evenementen voor uw team {{teamName}} van de afgelopen 30 dagen",
"me": "Ik", "me": "Ik",
"monthly_digest_email": "Maandelijks overzichts-e-mail",
"monthly_digest_email_for_teams": "Maandelijks overzichts-e-mail voor teams",
"verify_team_tooltip": "Verifieer uw team om het versturen van berichten naar deelnemers in te schakelen", "verify_team_tooltip": "Verifieer uw team om het versturen van berichten naar deelnemers in te schakelen",
"member_removed": "Lid verwijderd", "member_removed": "Lid verwijderd",
"my_availability": "Mijn beschikbaarheid", "my_availability": "Mijn beschikbaarheid",
@@ -2039,13 +2065,41 @@
"team_no_event_types": "Dit team heeft geen gebeurtenistypes", "team_no_event_types": "Dit team heeft geen gebeurtenistypes",
"seat_options_doesnt_multiple_durations": "De plaatsoptie ondersteunt geen meerdere duren", "seat_options_doesnt_multiple_durations": "De plaatsoptie ondersteunt geen meerdere duren",
"include_calendar_event": "Agendagebeurtenis opnemen", "include_calendar_event": "Agendagebeurtenis opnemen",
"oAuth": "OAuth",
"recently_added": "Recent toegevoegd", "recently_added": "Recent toegevoegd",
"no_members_found": "Geen leden gevonden", "no_members_found": "Geen leden gevonden",
"event_setup_length_error": "Gebeurtenisconfiguratie: de duur moet minimaal 1 minuut zijn.", "event_setup_length_error": "Gebeurtenisconfiguratie: de duur moet minimaal 1 minuut zijn.",
"availability_schedules": "Beschikbaarheidsschema's", "availability_schedules": "Beschikbaarheidsschema's",
"unauthorized": "Ongeautoriseerd",
"access_cal_account": "{{clientName}} wil graag toegang tot uw {{appName}}-account",
"select_account_team": "Account of team selecteren",
"allow_client_to": "Hierdoor kan {{clientName}}",
"associate_with_cal_account": "U koppelen aan uw persoonsgegevens van {{clientName}}",
"see_personal_info": "Uw persoonsgegevens bekijken, waaronder alle persoonsgegevens die u openbaar heeft gemaakt",
"see_primary_email_address": "Uw primaire e-mailadres bekijken",
"connect_installed_apps": "Koppelen met uw geïnstalleerde apps",
"access_event_type": "Uw gebeurtenistypes lezen, bewerken en verwijderen",
"access_availability": "Uw beschikbaarheid lezen, bewerken en verwijderen",
"access_bookings": "Uw boekingen lezen, bewerken en verwijderen",
"allow_client_to_do": "Dit toestaan voor {{clientName}}?",
"oauth_access_information": "Door op Toestaan te klikken, geeft u deze app toestemming om uw gegevens te gebruiken in overeenstemming met hun gebruiksvoorwaarden en privacybeleid. U kunt de toegang verwijderen in de {{appName}} App Store.",
"allow": "Toestaan",
"view_only_edit_availability_not_onboarded": "Deze gebruiker heeft de onboarding nog niet voltooid. U kunt diens beschikbaarheid pas instellen nadat ze men de onboarding heeft voltooid.", "view_only_edit_availability_not_onboarded": "Deze gebruiker heeft de onboarding nog niet voltooid. U kunt diens beschikbaarheid pas instellen nadat ze men de onboarding heeft voltooid.",
"view_only_edit_availability": "U bekijkt de beschikbaarheid van deze gebruiker. U kunt alleen uw eigen beschikbaarheid bewerken.", "view_only_edit_availability": "U bekijkt de beschikbaarheid van deze gebruiker. U kunt alleen uw eigen beschikbaarheid bewerken.",
"you_can_override_calendar_in_advanced_tab": "U kunt dit per gebeurtenis overschrijven in Geavanceerde instellingen voor elk gebeurtenistype.",
"edit_users_availability": "Beschikbaarheid van gebruiker bewerken: {{username}}", "edit_users_availability": "Beschikbaarheid van gebruiker bewerken: {{username}}",
"resend_invitation": "Uitnodiging opnieuw versturen",
"invitation_resent": "De uitnodiging is opnieuw verstuurd.",
"add_client": "Klant toevoegen",
"copy_client_secret_info": "Na het kopiëren van het geheim kunt u het niet meer bekijken",
"add_new_client": "Nieuwe klant toevoegen",
"this_app_is_not_setup_already": "Deze app is nog niet ingesteld",
"as_csv": "als CSV",
"overlay_my_calendar": "Mijn agenda overleggen",
"overlay_my_calendar_toc": "Door uw agenda te koppelen accepteert u ons privacybeleid en onze gebruiksvoorwaarden. U kunt de toegang op elk moment weer intrekken.",
"view_overlay_calendar_events": "Bekijk uw agendagebeurtenissen om dubbele boekingen te voorkomen.",
"lock_timezone_toggle_on_booking_page": "Tijdzone vergrendelen op boekingspagina",
"description_lock_timezone_toggle_on_booking_page": "Om de tijdzone op de boekingspagina te vergrendelen, handig voor persoonlijke gebeurtenissen.",
"extensive_whitelabeling": "Speciale onboarding en technische ondersteuning", "extensive_whitelabeling": "Speciale onboarding en technische ondersteuning",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Voeg uw nieuwe strings hierboven toe ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Voeg uw nieuwe strings hierboven toe ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
} }
@@ -706,6 +706,7 @@
"next_step": "Hopp over trinn", "next_step": "Hopp over trinn",
"prev_step": "Forrige trinn", "prev_step": "Forrige trinn",
"install": "Installer", "install": "Installer",
"install_paid_app": "Abonner",
"installed": "Installert", "installed": "Installert",
"active_install_one": "{{count}} aktiv installasjon", "active_install_one": "{{count}} aktiv installasjon",
"active_install_other": "{{count}} aktive installasjoner", "active_install_other": "{{count}} aktive installasjoner",
@@ -268,6 +268,7 @@
"set_availability": "Ustaw swoją dostępność", "set_availability": "Ustaw swoją dostępność",
"availability_settings": "Ustawienia dostępności", "availability_settings": "Ustawienia dostępności",
"continue_without_calendar": "Kontynuuj bez kalendarza", "continue_without_calendar": "Kontynuuj bez kalendarza",
"continue_with": "Kontynuuj za pomocą: {{appName}}",
"connect_your_calendar": "Połącz swój kalendarz", "connect_your_calendar": "Połącz swój kalendarz",
"connect_your_video_app": "Podłącz aplikacje wideo", "connect_your_video_app": "Podłącz aplikacje wideo",
"connect_your_video_app_instructions": "Podłącz aplikacje wideo, aby korzystać z nich w swoich typach wydarzeń.", "connect_your_video_app_instructions": "Podłącz aplikacje wideo, aby korzystać z nich w swoich typach wydarzeń.",
@@ -288,6 +289,8 @@
"when": "Kiedy", "when": "Kiedy",
"where": "Gdzie", "where": "Gdzie",
"add_to_calendar": "Dodaj do kalendarza", "add_to_calendar": "Dodaj do kalendarza",
"add_to_calendar_description": "Wybierz, gdzie dodawać wydarzenia w zarezerwowanych terminach.",
"add_events_to": "Dodaj wydarzenia do",
"add_another_calendar": "Dodaj kolejny kalendarz", "add_another_calendar": "Dodaj kolejny kalendarz",
"other": "Inne", "other": "Inne",
"email_sign_in_subject": "Twój link logowania do aplikacji {{appName}}", "email_sign_in_subject": "Twój link logowania do aplikacji {{appName}}",
@@ -422,6 +425,7 @@
"booking_created": "Rezerwacja Utworzona", "booking_created": "Rezerwacja Utworzona",
"booking_rejected": "Odrzucono rezerwację", "booking_rejected": "Odrzucono rezerwację",
"booking_requested": "Poproszono o rezerwację", "booking_requested": "Poproszono o rezerwację",
"booking_payment_initiated": "Rozpoczęto płatność za rezerwację",
"meeting_ended": "Spotkanie zakończone", "meeting_ended": "Spotkanie zakończone",
"form_submitted": "Przesłano formularz", "form_submitted": "Przesłano formularz",
"booking_paid": "Rezerwacja opłacona", "booking_paid": "Rezerwacja opłacona",
@@ -456,6 +460,7 @@
"no_event_types_have_been_setup": "Ten użytkownik nie ustawił jeszcze żadnych typów wydarzeń.", "no_event_types_have_been_setup": "Ten użytkownik nie ustawił jeszcze żadnych typów wydarzeń.",
"edit_logo": "Edytuj logo", "edit_logo": "Edytuj logo",
"upload_a_logo": "Prześlij logo", "upload_a_logo": "Prześlij logo",
"upload_logo": "Prześlij logo",
"remove_logo": "Usuń logo", "remove_logo": "Usuń logo",
"enable": "Włącz", "enable": "Włącz",
"code": "Kod", "code": "Kod",
@@ -568,6 +573,7 @@
"your_team_name": "Nazwa Twojego zespołu", "your_team_name": "Nazwa Twojego zespołu",
"team_updated_successfully": "Zespół został pomyślnie zaktualizowany", "team_updated_successfully": "Zespół został pomyślnie zaktualizowany",
"your_team_updated_successfully": "Twój zespół został pomyślnie zaktualizowany.", "your_team_updated_successfully": "Twój zespół został pomyślnie zaktualizowany.",
"your_org_updated_successfully": "Twoja organizacja została pomyślnie zaktualizowana.",
"about": "O aplikacji", "about": "O aplikacji",
"team_description": "Kilka zdań na temat Twojego zespołu. Będą wyświetlane na stronie adresu URL Twojego zespołu.", "team_description": "Kilka zdań na temat Twojego zespołu. Będą wyświetlane na stronie adresu URL Twojego zespołu.",
"org_description": "Kilka zdań na temat Twojej organizacji. Pojawią się na stronie adresu URL Twojej organizacji.", "org_description": "Kilka zdań na temat Twojej organizacji. Pojawią się na stronie adresu URL Twojej organizacji.",
@@ -599,6 +605,7 @@
"hide_book_a_team_member": "Ukryj przycisk Zarezerwuj członka zespołu", "hide_book_a_team_member": "Ukryj przycisk Zarezerwuj członka zespołu",
"hide_book_a_team_member_description": "Ukryj przycisk Zarezerwuj członka zespołu na swoich stronach publicznych.", "hide_book_a_team_member_description": "Ukryj przycisk Zarezerwuj członka zespołu na swoich stronach publicznych.",
"danger_zone": "Strefa zagrożenia", "danger_zone": "Strefa zagrożenia",
"account_deletion_cannot_be_undone": "Uwaga. Usunięcie konta jest nieodwracalne.",
"back": "Wróć", "back": "Wróć",
"cancel": "Anuluj", "cancel": "Anuluj",
"cancel_all_remaining": "Anuluj wszystkie pozostałe", "cancel_all_remaining": "Anuluj wszystkie pozostałe",
@@ -688,6 +695,7 @@
"people": "Ludzie", "people": "Ludzie",
"your_email": "Twój e-mail", "your_email": "Twój e-mail",
"change_avatar": "Zmień Awatar", "change_avatar": "Zmień Awatar",
"upload_avatar": "Prześlij awatar",
"language": "Język", "language": "Język",
"timezone": "Strefa Czasowa", "timezone": "Strefa Czasowa",
"first_day_of_week": "Pierwszy dzień tygodnia", "first_day_of_week": "Pierwszy dzień tygodnia",
@@ -778,6 +786,7 @@
"disable_guests": "Wyłącz Gości", "disable_guests": "Wyłącz Gości",
"disable_guests_description": "Wyłącz dodawanie dodatkowych gości podczas rezerwacji.", "disable_guests_description": "Wyłącz dodawanie dodatkowych gości podczas rezerwacji.",
"private_link": "Wygeneruj link prywatny", "private_link": "Wygeneruj link prywatny",
"enable_private_url": "Włącz prywatny adres URL",
"private_link_label": "Link prywatny", "private_link_label": "Link prywatny",
"private_link_hint": "Twój link prywatny będzie odnawiać się po każdym użyciu", "private_link_hint": "Twój link prywatny będzie odnawiać się po każdym użyciu",
"copy_private_link": "Skopiuj link prywatny", "copy_private_link": "Skopiuj link prywatny",
@@ -840,6 +849,7 @@
"next_step": "Pomiń krok", "next_step": "Pomiń krok",
"prev_step": "Poprzedni krok", "prev_step": "Poprzedni krok",
"install": "Zainstaluj", "install": "Zainstaluj",
"install_paid_app": "Subskrybuj",
"installed": "Zainstalowane", "installed": "Zainstalowane",
"active_install_one": "Aktywne instalacje: {{count}}", "active_install_one": "Aktywne instalacje: {{count}}",
"active_install_other": "Aktywne instalacje: {{count}}", "active_install_other": "Aktywne instalacje: {{count}}",
@@ -1088,6 +1098,7 @@
"developer_documentation": "Dokumentacja dla programistów", "developer_documentation": "Dokumentacja dla programistów",
"get_in_touch": "Kontakt", "get_in_touch": "Kontakt",
"contact_support": "Skontaktuj się z pomocą techniczną", "contact_support": "Skontaktuj się z pomocą techniczną",
"community_support": "Wsparcie społeczności",
"feedback": "Opinia", "feedback": "Opinia",
"submitted_feedback": "Dziękujemy za opinię!", "submitted_feedback": "Dziękujemy za opinię!",
"feedback_error": "Podczas wysyłania opinii wystąpił błąd", "feedback_error": "Podczas wysyłania opinii wystąpił błąd",
@@ -1213,6 +1224,7 @@
"organizer_name_variable": "Nazwa organizatora", "organizer_name_variable": "Nazwa organizatora",
"app_upgrade_description": "Aby korzystać z tej funkcji, musisz uaktualnić do konta Pro.", "app_upgrade_description": "Aby korzystać z tej funkcji, musisz uaktualnić do konta Pro.",
"invalid_number": "Nieprawidłowy numer telefonu", "invalid_number": "Nieprawidłowy numer telefonu",
"invalid_url_error_message": "Nieprawidłowy adres URL dla: {{label}}. Przykładowy adres URL: {{sampleUrl}}",
"navigate": "Nawigacja", "navigate": "Nawigacja",
"open": "Otwórz", "open": "Otwórz",
"close": "Zamknij", "close": "Zamknij",
@@ -1276,6 +1288,7 @@
"personal_cal_url": "Mój osobisty adres URL aplikacji {{appName}}", "personal_cal_url": "Mój osobisty adres URL aplikacji {{appName}}",
"bio_hint": "Kilka zdań o Tobie. Pojawią się one na Twojej osobistej stronie URL.", "bio_hint": "Kilka zdań o Tobie. Pojawią się one na Twojej osobistej stronie URL.",
"user_has_no_bio": "Ten użytkownik nie dodał jeszcze biogramu.", "user_has_no_bio": "Ten użytkownik nie dodał jeszcze biogramu.",
"bio": "Informacje o profilu",
"delete_account_modal_title": "Usuń konto", "delete_account_modal_title": "Usuń konto",
"confirm_delete_account_modal": "Czy na pewno chcesz usunąć swoje konto {{appName}}?", "confirm_delete_account_modal": "Czy na pewno chcesz usunąć swoje konto {{appName}}?",
"delete_my_account": "Usuń konto", "delete_my_account": "Usuń konto",
@@ -1286,6 +1299,7 @@
"select_calendars": "Wybierz kalendarze, które chcesz sprawdzić pod kątem konfliktów, aby uniknąć podwójnych rezerwacji.", "select_calendars": "Wybierz kalendarze, które chcesz sprawdzić pod kątem konfliktów, aby uniknąć podwójnych rezerwacji.",
"check_for_conflicts": "Sprawdź konflikty", "check_for_conflicts": "Sprawdź konflikty",
"view_recordings": "Wyświetl nagrania", "view_recordings": "Wyświetl nagrania",
"check_for_recordings": "Sprawdź nagrania",
"adding_events_to": "Dodawanie wydarzeń do", "adding_events_to": "Dodawanie wydarzeń do",
"follow_system_preferences": "Śledź preferencje systemowe", "follow_system_preferences": "Śledź preferencje systemowe",
"custom_brand_colors": "Niestandardowe kolory marki", "custom_brand_colors": "Niestandardowe kolory marki",
@@ -1530,6 +1544,7 @@
"problem_registering_domain": "Wystąpił problem z zarejestrowaniem subdomeny, spróbuj ponownie lub skontaktuj się z administratorem", "problem_registering_domain": "Wystąpił problem z zarejestrowaniem subdomeny, spróbuj ponownie lub skontaktuj się z administratorem",
"team_publish": "Opublikuj zespół", "team_publish": "Opublikuj zespół",
"number_text_notifications": "Numer telefonu (powiadomienia SMS)", "number_text_notifications": "Numer telefonu (powiadomienia SMS)",
"number_sms_notifications": "Numer telefonu (powiadomienia SMS)",
"attendee_email_variable": "Adres e-mail uczestnika", "attendee_email_variable": "Adres e-mail uczestnika",
"attendee_email_info": "Adres e-mail osoby rezerwującej", "attendee_email_info": "Adres e-mail osoby rezerwującej",
"kbar_search_placeholder": "Wpisz polecenie lub wyszukaj...", "kbar_search_placeholder": "Wpisz polecenie lub wyszukaj...",
@@ -1594,6 +1609,7 @@
"options": "Opcje", "options": "Opcje",
"enter_option": "Wprowadź opcję {{index}}", "enter_option": "Wprowadź opcję {{index}}",
"add_an_option": "Dodaj opcję", "add_an_option": "Dodaj opcję",
"location_already_exists": "Ta lokalizacja już istnieje. Wybierz nową lokalizację",
"radio": "Przycisk radiowy", "radio": "Przycisk radiowy",
"google_meet_warning": "Aby korzystać z usługi Google Meet, musisz wybrać Kalendarz Google jako kalendarz docelowy.", "google_meet_warning": "Aby korzystać z usługi Google Meet, musisz wybrać Kalendarz Google jako kalendarz docelowy.",
"individual": "Pojedyncza osoba", "individual": "Pojedyncza osoba",
@@ -1613,6 +1629,7 @@
"date_overrides_mark_all_day_unavailable_other": "Zaznacz niedostępność w wybrane dni", "date_overrides_mark_all_day_unavailable_other": "Zaznacz niedostępność w wybrane dni",
"date_overrides_add_btn": "Dodaj zastąpienie", "date_overrides_add_btn": "Dodaj zastąpienie",
"date_overrides_update_btn": "Zaktualizuj zastąpienie", "date_overrides_update_btn": "Zaktualizuj zastąpienie",
"date_successfully_added": "Pomyślnie dodano nadpisanie daty",
"event_type_duplicate_copy_text": "{{slug}}-kopia", "event_type_duplicate_copy_text": "{{slug}}-kopia",
"set_as_default": "Ustaw jako domyślne", "set_as_default": "Ustaw jako domyślne",
"hide_eventtype_details": "Ukryj szczegóły typu wydarzenia", "hide_eventtype_details": "Ukryj szczegóły typu wydarzenia",
@@ -1639,6 +1656,7 @@
"minimum_round_robin_hosts_count": "Liczba gospodarzy, których uczestnictwo jest wymagane", "minimum_round_robin_hosts_count": "Liczba gospodarzy, których uczestnictwo jest wymagane",
"hosts": "Gospodarze", "hosts": "Gospodarze",
"upgrade_to_enable_feature": "Musisz utworzyć zespół, aby włączyć tę funkcję. Kliknij, aby utworzyć zespół.", "upgrade_to_enable_feature": "Musisz utworzyć zespół, aby włączyć tę funkcję. Kliknij, aby utworzyć zespół.",
"orgs_upgrade_to_enable_feature": "Aby włączyć tę funkcję, musisz przejść na plan Enterprise.",
"new_attendee": "Nowy uczestnik", "new_attendee": "Nowy uczestnik",
"awaiting_approval": "Oczekiwanie na zatwierdzenie", "awaiting_approval": "Oczekiwanie na zatwierdzenie",
"requires_google_calendar": "Ta aplikacja wymaga połączenia z Kalendarzem Google", "requires_google_calendar": "Ta aplikacja wymaga połączenia z Kalendarzem Google",
@@ -1743,6 +1761,7 @@
"show_on_booking_page": "Pokaż na stronie rezerwacji", "show_on_booking_page": "Pokaż na stronie rezerwacji",
"get_started_zapier_templates": "Zacznij korzystać z szablonów Zapier", "get_started_zapier_templates": "Zacznij korzystać z szablonów Zapier",
"team_is_unpublished": "Zespół {{team}} nie został opublikowany", "team_is_unpublished": "Zespół {{team}} nie został opublikowany",
"org_is_unpublished_description": "Link organizacji jest obecnie niedostępny. Skontaktuj się z właścicielem organizacji lub poproś o jego opublikowanie.",
"team_is_unpublished_description": "Link jednostki {{entity}} jest obecnie niedostępny. Skontaktuj się z właścicielem jednostki {{entity}} lub poproś o jego opublikowanie.", "team_is_unpublished_description": "Link jednostki {{entity}} jest obecnie niedostępny. Skontaktuj się z właścicielem jednostki {{entity}} lub poproś o jego opublikowanie.",
"team_member": "Członek zespołu", "team_member": "Członek zespołu",
"a_routing_form": "Formularz przekierowania", "a_routing_form": "Formularz przekierowania",
@@ -1877,6 +1896,7 @@
"edit_invite_link": "Edytuj ustawienia linku", "edit_invite_link": "Edytuj ustawienia linku",
"invite_link_copied": "Skopiowano link zaproszenia", "invite_link_copied": "Skopiowano link zaproszenia",
"invite_link_deleted": "Usunięto link zaproszenia", "invite_link_deleted": "Usunięto link zaproszenia",
"api_key_deleted": "Klucz API został usunięty",
"invite_link_updated": "Zapisano ustawienia linku zaproszenia", "invite_link_updated": "Zapisano ustawienia linku zaproszenia",
"link_expires_after": "Linki wygasną po...", "link_expires_after": "Linki wygasną po...",
"one_day": "1 dzień", "one_day": "1 dzień",
@@ -2009,7 +2029,13 @@
"attendee_last_name_variable": "Nazwisko uczestnika", "attendee_last_name_variable": "Nazwisko uczestnika",
"attendee_first_name_info": "Imię osoby rezerwującej", "attendee_first_name_info": "Imię osoby rezerwującej",
"attendee_last_name_info": "Nazwisko osoby rezerwującej", "attendee_last_name_info": "Nazwisko osoby rezerwującej",
"your_monthly_digest": "Miesięczne podsumowanie",
"member_name": "Nazwa członka",
"most_popular_events": "Najpopularniejsze wydarzenia",
"summary_of_events_for_your_team_for_the_last_30_days": "Oto podsumowanie popularnych wydarzeń dla Twojego zespołu {{teamName}} z ostatnich 30 dni",
"me": "Ja", "me": "Ja",
"monthly_digest_email": "E-mail z miesięcznym podsumowaniem",
"monthly_digest_email_for_teams": "E-mail dla zespołów z miesięcznym podsumowaniem",
"verify_team_tooltip": "Zweryfikuj zespół, aby włączyć wysyłanie wiadomości do uczestników", "verify_team_tooltip": "Zweryfikuj zespół, aby włączyć wysyłanie wiadomości do uczestników",
"member_removed": "Usunięto członka", "member_removed": "Usunięto członka",
"my_availability": "Moja dostępność", "my_availability": "Moja dostępność",
@@ -2039,13 +2065,41 @@
"team_no_event_types": "Ten zespół nie ma żadnych typów zdarzeń.", "team_no_event_types": "Ten zespół nie ma żadnych typów zdarzeń.",
"seat_options_doesnt_multiple_durations": "Opcja Miejsce nie obsługuje wielu czasów trwania.", "seat_options_doesnt_multiple_durations": "Opcja Miejsce nie obsługuje wielu czasów trwania.",
"include_calendar_event": "Uwzględnij wydarzenie z kalendarza", "include_calendar_event": "Uwzględnij wydarzenie z kalendarza",
"oAuth": "OAuth",
"recently_added": "Niedawno dodane", "recently_added": "Niedawno dodane",
"no_members_found": "Nie znaleziono członków", "no_members_found": "Nie znaleziono członków",
"event_setup_length_error": "Konfiguracja wydarzenia: czas trwania musi wynosić co najmniej minutę.", "event_setup_length_error": "Konfiguracja wydarzenia: czas trwania musi wynosić co najmniej minutę.",
"availability_schedules": "Harmonogramy dostępności", "availability_schedules": "Harmonogramy dostępności",
"unauthorized": "Brak autoryzacji",
"access_cal_account": "{{clientName}} chce uzyskać dostęp do Twojego konta w aplikacji {{appName}}",
"select_account_team": "Wybierz konto lub zespół",
"allow_client_to": "Dzięki temu klient ({{clientName}}) będzie mógł",
"associate_with_cal_account": "Powiązać Cię z Twoimi danymi osobistymi od klienta ({{clientName}})",
"see_personal_info": "Zobaczyć Twoje dane osobiste, w tym wszystkie dane osobiste udostępnione przez Ciebie publicznie",
"see_primary_email_address": "Zobaczyć Twój główny adres e-mail",
"connect_installed_apps": "Połączyć się z zainstalowanymi przez Ciebie aplikacjami",
"access_event_type": "Przeglądać, edytować i usuwać Twoje typy wydarzeń",
"access_availability": "Przeglądać, edytować i usuwać Twoją dostępność",
"access_bookings": "Przeglądać, edytować i usuwać Twoje rezerwacje",
"allow_client_to_do": "Czy chcesz umożliwić to klientowi ({{clientName}})?",
"oauth_access_information": "Klikając „Zezwól”, pozwolisz tej aplikacji na korzystanie z Twoich danych zgodnie z jej regulaminem i polityką prywatności. Możesz odwołać pozwolenie na dostęp w ustawieniach sklepu aplikacji {{appName}}.",
"allow": "Zezwól",
"view_only_edit_availability_not_onboarded": "Ten użytkownik nie zakończył procesu wdrażania. Ustawienie jego dostępności będzie niedostępne, póki nie zakończy wdrażania.", "view_only_edit_availability_not_onboarded": "Ten użytkownik nie zakończył procesu wdrażania. Ustawienie jego dostępności będzie niedostępne, póki nie zakończy wdrażania.",
"view_only_edit_availability": "Wyświetlasz dostępność tego użytkownika. Możesz edytować tylko własną dostępność.", "view_only_edit_availability": "Wyświetlasz dostępność tego użytkownika. Możesz edytować tylko własną dostępność.",
"you_can_override_calendar_in_advanced_tab": "Możesz nadpisać to na podstawie wydarzeń w ustawieniach zaawansowanych przy każdym typie wydarzenia.",
"edit_users_availability": "Edytuj dostępność użytkownika: {{username}}", "edit_users_availability": "Edytuj dostępność użytkownika: {{username}}",
"resend_invitation": "Ponownie wyślij zaproszenie",
"invitation_resent": "Zaproszenie zostało wysłane ponownie.",
"add_client": "Dodaj klienta",
"copy_client_secret_info": "Po skopiowaniu sekretu nie będzie można go już przeglądać",
"add_new_client": "Dodaj nowego klienta",
"this_app_is_not_setup_already": "Ta aplikacja nie została jeszcze skonfigurowana",
"as_csv": "jako plik CSV",
"overlay_my_calendar": "Nakładka kalendarza",
"overlay_my_calendar_toc": "Łącząc się z kalendarzem, akceptujesz naszą politykę prywatności i warunki użytkowania. Możesz odwołać pozwolenie na dostęp w dowolnym momencie.",
"view_overlay_calendar_events": "Zobacz wydarzenia kalendarza, aby zapobiec kolidującym rezerwacjom.",
"lock_timezone_toggle_on_booking_page": "Zablokuj strefę czasową na stronie rezerwacji",
"description_lock_timezone_toggle_on_booking_page": "Aby zablokować strefę czasową na stronie rezerwacji (przydatne dla wydarzeń stacjonarnych).",
"extensive_whitelabeling": "Dedykowane wsparcie w zakresie wdrożenia i obsługi technicznej", "extensive_whitelabeling": "Dedykowane wsparcie w zakresie wdrożenia i obsługi technicznej",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodaj nowe ciągi powyżej ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodaj nowe ciągi powyżej ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
} }
Loaded 100 of 325 files, more files were not shown because too many files have changed in this diff. Show more