Compare commits

..
Author SHA1 Message Date
Benny Joo b2ade568c8 refactor: more pbac replacements (#24135)
Post release / release (push) Canceled after 0s
* getUserConnectedApps

* create teams

* use permission check in team deletion

* team read

* revert

* publish handler

* wip

* revert

* wip

* wip
2025-09-30 13:11:02 +00:00
27820ce897 feat: auto-accept team invitations for existing users (#24091)
* feat: auto-accept team invitations for existing users

- Change email button text from 'View Invitation' to 'Accept Invite'
- Implement auto-accept flow when clicking email CTA
- Update TeamService.inviteMemberByToken to support auto-acceptance
- Add new autoAcceptInvite tRPC endpoint for handling auto-acceptance
- Update invitation link generation to include autoAccept parameter
- Handle both team and organization invitation scenarios
- Maintain payment/billing flow integration with TeamBilling.updateQuantity
- Preserve backward compatibility with existing manual flow
- Update all locale files with new 'Accept Invite' button text

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* revert: locale changes except English

- Keep only English 'Accept Invite' translation
- Revert all other locale files to original 'View Invitation' translations
- Maintain core auto-accept invitation functionality

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* simplify: remove autoAccept parameter and make auto-acceptance default

- Remove autoAccept parameter from TeamService.inviteMemberByToken
- Always auto-accept invitations for existing users clicking email links
- Remove autoAccept logic from teams server-page.tsx
- Remove autoAccept=true from invitation URLs
- Delete autoAcceptInvite handler and schema files
- Remove autoAcceptInvite endpoint from tRPC router
- Simplify invitation flow to match new user pattern

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* test: update teamService test to expect auto-accepted memberships

- Change expectation from accepted: false to accepted: true
- Update test description to reflect auto-accept behavior
- Fix TypeScript type casting to use Pick<TeamRepository, 'deleteById'>
- Aligns with new default auto-acceptance for team invitations

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* update

* Update utils.ts

* fix type error

* delete token

* add prisma transaction

* update

* update param

* test: fix mock objects in teamService tests with realistic data

- Fix duplicate property assignments in mock user objects
- Use proper email format (user@example.com) for email fields
- Use proper username format (testuser) for username fields
- Fix logic error in acceptInvitationByToken (|| to &&)
- Add autoAccept parameter to resendInvitation.handler.ts
- All 16 tests passing with proper TypeScript types

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* test: add e2e tests for team invitation auto-accept flow

- Add test for existing user auto-accepting team invitation via email link
- Add test for error handling when wrong user tries to use invitation link
- Verify proper user identity validation and database state changes
- Follow existing e2e test patterns with browser context isolation
- Fix ESLint warnings: replace conditional with assertion and remove unused browser parameter

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* fix

* fix

* fix: update team owner creation in e2e tests to include proper names

- Fix email subject mismatch in auto-accept invitation tests
- Team owners now created with explicit names instead of undefined
- Matches pattern used in other working team invitation tests

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* fix: update organization invitation test helper to match new auto-accept link format

- Change expectExistingUserToBeInvitedToOrganization to look for 'teams?token' instead of 'settings/team'
- Fixes 'Invite link not found' error in organization booking e2e test
- Aligns with auto-accept invitation URL changes that use /teams?token= format
- Fix eslint disable comment for playwright rule

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* address coderrabit review

* fix failing test

* addressed review

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-30 13:07:21 +00:00
emrysal fea7e66996 chore: release v5.7.7 2025-09-30 12:57:27 +00:00
sean-brydon 474d1571ae Remove checkAdminOrOwner from settings layout client (#24162) 2025-09-30 12:08:05 +01:00
Rajiv Sahal 2ef627b87e fix: css issue for 12/24 styles (#24141) 2025-09-30 08:38:17 +00:00
sean-brydon 2dfb82b623 Bring back correct routing form teams (#24158) 2025-09-30 08:37:16 +00:00
sean-brydon ccd7fdfd52 refactor role manage (#24146) 2025-09-30 08:35:26 +01:00
Keith Williams f92e59ed34 chore: Update yarn.lock (#24153) 2025-09-30 02:19:30 +00:00
Keith WilliamsandDevin AI d29477349a test: replace prismaMock with BookingRepository mock in checkBookingLimits tests (#24152)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-29 23:34:20 +00:00
Alex van Andel 259e480baa chore: Handle stripe refunds slightly differently to reduce hard errors (#24150)
* chore: Handle stripe refunds slightly differently to reduce hard errors

* Remove 'Received and discarded' error when a credential is not found for a triggered subscription

* Fix handling of no credential found (do show in stripe debugger)

* Minimal overhaul of delete handling

* Fix doc typo
2025-09-29 23:07:08 +00:00
Benny Joo 0055dd1142 perf: dynamically import sentry in Next.js api routes' wrapper (#23710)
* dynamically import sentry in nextjs api routes wrapper

* refactor

* same for pages router

* fixes

* address feedback

* refactor

* address feedback
2025-09-30 02:13:50 +05:30
Volnei Munhoz 61c44780f0 chore: Optional skip warnings for lint-staged (#24102)
* add optional skip warnings

* remove test changes

* allow underscore variables pattern being ignored by eslint (#24103)
2025-09-29 19:51:18 +00:00
Calcom BotandLingo.dev 9710d3b15c feat: update translations via @LingoDotDev (#23991)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-09-29 19:23:59 +00:00
sean-brydon 04cefebd75 add permissions.canUpdateOrganization to show/hide items in org navbar (#24142) 2025-09-29 15:11:39 +00:00
Amit Sharma dbd927c002 fix: intercom zindex (#24096) 2025-09-29 11:43:11 -03:00
e6b2116a2b feat: Calendar Cache and Sync (#23876)
* feat: calendar cache and sync - wip

* Add env.example

* refactor on CalendarCacheEventService

* remove test console.log

* Fix type checks errors

* chore: remove pt comment

* add route.ts

* chore: fix tests

* Improve cache impl

* chore: update recurring event id

* chore: small improvements

* calendar cache improvements

* Fix remove dynamic imports

* Add cleanup stale cache

* Fix tests

* add event update

* type fixes

* feat: add comprehensive tests for new calendar subscription API routes

- Add tests for /api/cron/calendar-subscriptions-cleanup route (9 tests)
- Add tests for /api/cron/calendar-subscriptions route (10 tests)
- Add tests for /api/webhooks/calendar-subscription/[provider] route (11 tests)
- Add missing feature flags for calendar-subscription-cache and calendar-subscription-sync
- All 30 tests pass with comprehensive coverage of authentication, feature flags, error handling, and service instantiation

Tests cover:
- Authentication scenarios (API key validation, Bearer tokens, query parameters)
- Feature flag combinations (cache/sync enabled/disabled states)
- Success and error handling (including non-Error exceptions)
- Service instantiation with proper dependency injection
- Provider validation for webhook endpoints

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* feat: add comprehensive tests for calendar subscription services, repositories, and adapters

- Add unit tests for CalendarSubscriptionService with subscription, webhook, and event processing
- Add unit tests for CalendarCacheEventService with cache operations and cleanup
- Add unit tests for CalendarSyncService with Cal.com event filtering and booking operations
- Add unit tests for CalendarCacheEventRepository with CRUD operations
- Add unit tests for SelectedCalendarRepository with calendar selection management
- Add unit tests for GoogleCalendarSubscriptionAdapter with subscription and event fetching
- Add unit tests for Office365CalendarSubscriptionAdapter with placeholder implementation
- Add unit tests for AdaptersFactory with provider management and adapter creation
- Fix lint issues by removing explicit 'any' type casting and unused variables
- All tests follow Cal.com conventions using Vitest framework with proper mocking

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix: improve calendar-subscriptions-cleanup test performance by adding missing mocks

- Add comprehensive mocks for defaultResponderForAppDir, logger, performance monitoring, and Sentry
- Fix slow test execution (933ms -> <100ms) caused by missing dependency mocks
- Ensure consistent test performance across different environments

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* Fix tests

* Fix tests

* type fix

* Fix coderabbit comments

* Fix types

* Fix test

* Update apps/web/app/api/cron/calendar-subscriptions/route.ts

Co-authored-by: Alex van Andel <me@alexvanandel.com>

* Fixes by first review

* feat: add database migrations for calendar cache and sync fields

- Add CalendarCacheEventStatus enum with confirmed, tentative, cancelled values
- Add new fields to SelectedCalendar: channelId, channelKind, channelResourceId, channelResourceUri, channelExpiration, syncSubscribedAt, syncToken, syncedAt, syncErrorAt, syncErrorCount
- Create CalendarCacheEvent table with foreign key to SelectedCalendar
- Add necessary indexes and constraints for performance and data integrity

Fixes database schema issues causing e2e test failures with 'column does not exist' errors.

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* only google-calendar for now

* docs: add Calendar Cache and Sync feature documentation

- Add comprehensive feature overview and motivation
- Document feature flags with SQL examples
- Include SQL examples for enabling features for users and teams
- Reference technical documentation files

Addresses PR #23876 documentation requirements

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* docs: update calendar subscription README with comprehensive documentation

- Undo incorrect changes to main README.md
- Update packages/features/calendar-subscription/README.md with:
  - Feature overview and motivation
  - Environment variables section
  - Complete feature flags documentation with SQL examples
  - SQL examples for enabling features for users and teams
  - Detailed architecture documentation

Addresses PR #23876 documentation requirements

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix docs

* Fix test to available calendars

* Fix test to available calendars

* add migration and sync boilerplate

* fix typo

* remove double log

* sync boilerplate

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-09-29 14:26:14 +00:00
Udit Takkar 855a02091f chore: update tooltip text (#24143) 2025-09-29 11:20:28 -03:00
Volnei MunhozandDevin AI 358434429e feat: add booking flags for skipAvailabilityCheck, skipEventLimitsCheck, skipCalendarSyncTaskCreation (#24122)
- Add three optional boolean flags to BookingHandlerInput interface
- Implement conditional logic to skip availability checks when skipAvailabilityCheck is true
- Implement conditional logic to skip event limits checks when skipEventLimitsCheck is true
- Implement conditional logic to skip calendar sync when skipCalendarSyncTaskCreation is true
- Add comprehensive tests for all three flags and combined scenarios
- Maintain backward compatibility with default false values
- Fix linting issues (unused variables and expressions)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-29 14:02:31 +00:00
sean-brydonandhbjORbj b00061d85e chore: Add addMembersToTeams logic from 24099 (#24140)
* Add addMembersToTeams logic from 24099

* remove import

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-09-29 12:54:35 +00:00
Udit Takkarandsean-brydon 022f08c05e feat: add 10 minute cooldown in instant meetings (#24119)
* feat: add 10 minute

* feat: add 10 minute

* chore: change name

* chore: change name

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2025-09-29 16:03:03 +04:00
emrysal 6ff5c8ce5f chore: release v5.7.6
Post release / release (push) Canceled after 0s
2025-09-29 11:22:43 +00:00
Lauris Skraucis 3be7fb9dd8 fix: v2 create org user with time zone (#24133)
* fix: tzdata undefined import

* refactor: capitalize org user timezone

* test: org user creation with time zone
2025-09-29 11:12:18 +00:00
Benny Joo 72dd970b6e refactor: use permission check service in /organizations endpoints (#24099)
* getUserConnectedApps

* create teams

* use permission check in team deletion

* team read

* revert

* publish handler

* wip
2025-09-29 11:07:19 +00:00
sean-brydon f4d09ece43 chore: (PBAC) readonly eventTypes (#24131)
* Chore(PBAC): readonly eventTypes

* Set readonly value
2025-09-29 11:06:55 +00:00
sean-brydonandEunjae Lee 239e6085ba feat: pbac - private teams (#23998)
* Add permission + migration for listMembersPrivate + depends on lsitMember

* Add legacy list members PBAC private logic

* Add checks in team pages + handler

* add list memeber checks for private orgs

* Add permision visibility scope to loading permissions

* getMembers handler

* fix fallback permission

* Verify private teams work with private org

* Apply suggestion from @eunjae-lee

Co-authored-by: Eunjae Lee <hey@eunjae.dev>

* Remove and private scope function and add param to old

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
2025-09-29 11:30:54 +01:00
sean-brydon b17eab3aeb chore: Use enums instead of hardcoded strings (#24132)
* Use enums instead of hardcoded strings

* use membership role
2025-09-29 10:57:36 +01:00
752f7fcde0 refactor: replace checkAdminOrOwner with team.create permission in teams page (#24116)
* refactor: replace checkAdminOrOwner with team.create permission in teams page

- Replace role-based access control with PBAC using team.create permission
- Follow PBAC refactoring guide pattern for server-side permission checking
- Use permission-specific variable naming (canCreateTeam)
- Maintain existing TeamsListing component interface

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: implement permissions object pattern for TeamsListing

- Replace isOrgAdmin prop with permissions object containing canCreateTeam
- Follow PBAC refactoring guide pattern for UI components
- Update TeamsListing component to use permissions.canCreateTeam
- Maintain existing functionality while improving code structure
- All type checking and linting verification passed

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* use different method

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2025-09-29 14:11:42 +05:30
f6b061f787 fix: update handler to not delete banner (#24106)
* Fix update handler to not delete banner

* code cleanup

* add banner-upload.e2e

* fix: add slug fallback in banner-upload e2e test

- Use org.slug as fallback when requestedSlug is undefined
- Addresses GitHub comment about guarding against undefined slug
- Prevents test failures when requestedSlug is not set
- Fix ESLint warnings: remove unnecessary escape characters and unused variable

Co-Authored-By: sean@cal.com <Sean@brydon.io>

* test: add banner preservation test for profile updates

- Verify that updating organization profile without banner changes preserves existing banner
- Ensures update handler doesn't overwrite banner when undefined is passed
- Addresses user request to test banner preservation during profile saves

Co-Authored-By: sean@cal.com <Sean@brydon.io>

* remove banner e2e wip

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-29 09:31:49 +01:00
Eunjae LeeandDevin AI cc8a1290ee docs: add comprehensive DataTable usage guide (#24107)
* docs: add comprehensive DataTable usage guide

- Document DataTableProvider, DataTableWrapper, and DataTable components
- Cover filter system with all filter types and operators
- Explain segment system (system vs user segments)
- Document pagination modes (infinite vs standard)
- Include real-world usage examples from UserListTable, BookingsListingView, MemberList
- Add TypeScript types reference and best practices

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* docs: update DataTable guide to recommend standard pagination

- De-emphasize virtualized rendering and infinite mode
- Position standard pagination as the recommended approach
- Document known issues with infinite loading mode
- Reorder pagination section to prioritize standard mode
- Update best practices to recommend standard pagination

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* docs: add missing DataTable guide topics

- Document getFacetedUniqueValues for faceted filters with real examples
- Add useSegments prop requirement for filter segment enablement
- Enhance server-side filtering documentation with comprehensive patterns
- Include examples from useFacetedUniqueValues and useInsightsRoutingParameters

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* docs: add missing DataTable guide topics

- Document getFacetedUniqueValues for faceted filters with real examples
- Add useSegments prop requirement for filter segment enablement
- Enhance server-side filtering documentation with comprehensive patterns
- Include examples from useFacetedUniqueValues and useInsightsRoutingParameters
- Correct server-side vs client-side filtering rationale

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* docs: enhance DataTable guide with troubleshooting and testing

- Remove duplicate Key Hooks section (lines 783-803)
- Add visual ASCII diagrams for architecture and component hierarchy
- Add comprehensive troubleshooting section with 7 common issues and solutions
- Add testing patterns section with Vitest examples and best practices
- Update table of contents to include new sections
- Maintain existing high-quality documentation standards

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* remove some sections

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-29 07:59:55 +00:00
Eunjae LeeandDevin AI b7cf893169 refactor: replace checkAdminOrOwner with PBAC availability.read permission (#24115)
- Replace role-based check with PermissionCheckService
- Use availability.read permission with OWNER/ADMIN fallback roles
- Maintain existing privacy logic for organizations
- Follow established PBAC pattern from bookings implementation

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-09-29 07:59:23 +00:00
Lauris Skraucis 1458cacdbb fix: v2 api event type splitName booking field (#24111)
* fix: v2 api event type splitName booking field

* fix: test
2025-09-29 09:19:24 +02:00
165 changed files with 9169 additions and 900 deletions
+6
View File
@@ -144,6 +144,12 @@ GOOGLE_WEBHOOK_TOKEN=
# Optional URL to override for tunelling webhooks. Defaults to NEXT_PUBLIC_WEBAPP_URL.
GOOGLE_WEBHOOK_URL=
# Token to verify incoming webhooks from Microsoft Calendar
MICROSOFT_WEBHOOK_TOKEN=
# Optional URL to override for tunelling webhooks. Defaults to NEXT_PUBLIC_WEBAPP_URL.
MICROSOFT_WEBHOOK_URL=
# Inbox to send user feedback
SEND_FEEDBACK_EMAIL=
View File
View File
@@ -6,6 +6,7 @@ import { createMocks } from "node-mocks-http";
import { describe, expect, test } from "vitest";
import { HttpError } from "@calcom/lib/http-error";
import type { User } from "@calcom/prisma/client";
import handler from "../../../pages/api/selected-calendars/_post";
@@ -72,7 +73,7 @@ describe("POST /api/selected-calendars", () => {
prismaMock.user.findFirstOrThrow.mockResolvedValue({
id: 444444,
} as any);
} as User);
prismaMock.selectedCalendar.create.mockResolvedValue({
credentialId: 1,
@@ -95,6 +96,16 @@ describe("POST /api/selected-calendars", () => {
unwatchAttempts: 0,
createdAt: new Date(),
updatedAt: new Date(),
channelId: null,
channelKind: null,
channelResourceId: null,
channelResourceUri: null,
channelExpiration: null,
syncSubscribedAt: null,
syncToken: null,
syncedAt: null,
syncErrorAt: null,
syncErrorCount: null,
});
await handler(req, res);
@@ -140,6 +151,16 @@ describe("POST /api/selected-calendars", () => {
unwatchAttempts: 0,
createdAt: new Date(),
updatedAt: new Date(),
channelId: null,
channelKind: null,
channelResourceId: null,
channelResourceUri: null,
channelExpiration: null,
syncSubscribedAt: null,
syncToken: null,
syncedAt: null,
syncErrorAt: null,
syncErrorCount: null,
});
await handler(req, res);
@@ -38,6 +38,7 @@ import type {
GuestsDefaultFieldOutput_2024_06_14,
NameDefaultFieldInput_2024_06_14,
NotesDefaultFieldInput_2024_06_14,
SplitNameDefaultFieldOutput_2024_06_14,
UpdateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import type { PlatformOAuthClient, Team, User, Schedule, EventType } from "@calcom/prisma/client";
@@ -1447,23 +1448,23 @@ describe("Event types Endpoints", () => {
try {
await eventTypesRepositoryFixture.delete(eventType.id);
} catch (e) {
// Event type might have been deleted by the test
console.log(e);
}
try {
await userRepositoryFixture.delete(user.id);
} catch (e) {
// User might have been deleted by the test
console.log(e);
}
try {
await userRepositoryFixture.delete(falseTestUser.id);
} catch (e) {
// User might have been deleted by the test
console.log(e);
}
try {
await userRepositoryFixture.delete(orgUser.id);
} catch (e) {
// User might have been deleted by the test
console.log(e);
}
await app.close();
});
@@ -2187,6 +2188,47 @@ describe("Event types Endpoints", () => {
});
});
describe("split name booking field", () => {
it("should create an event type with split name booking field", async () => {
const splitNameBookingField: SplitNameDefaultFieldOutput_2024_06_14 = {
isDefault: true,
type: "splitName",
slug: "splitName",
firstNameLabel: "First name",
firstNamePlaceholder: "",
lastNameLabel: "last name",
lastNamePlaceholder: "",
lastNameRequired: false,
disableOnPrefill: false,
};
const body: CreateEventTypeInput_2024_06_14 = {
title: "Coding class 9",
slug: "coding-class-9",
description: "Let's learn how to code like a pro.",
lengthInMinutes: 60,
bookingFields: [splitNameBookingField],
};
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const createdEventType = responseBody.data;
const splitNameBookingFieldResponse = createdEventType.bookingFields.find(
(field) => field.type === "splitName"
) as SplitNameDefaultFieldOutput_2024_06_14 | undefined;
expect(splitNameBookingFieldResponse).toEqual(splitNameBookingField);
await eventTypesRepositoryFixture.delete(createdEventType.id);
});
});
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
@@ -2194,12 +2236,12 @@ describe("Event types Endpoints", () => {
await eventTypesRepositoryFixture.delete(legacyEventTypeId1);
await eventTypesRepositoryFixture.delete(legacyEventTypeId2);
} catch (e) {
// Event type might have been deleted by the test
console.log(e);
}
try {
await userRepositoryFixture.delete(user.id);
} catch (e) {
// User might have been deleted by the test
console.log(e);
}
await app.close();
});
@@ -2379,7 +2421,7 @@ describe("Event types Endpoints", () => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const updatedEventType = responseBody.data;
+expect(updatedEventType.hidden).toBe(false);
expect(updatedEventType.hidden).toBe(false);
});
});
@@ -2389,12 +2431,12 @@ describe("Event types Endpoints", () => {
try {
await eventTypesRepositoryFixture.delete(legacyEventTypeId1);
} catch (e) {
// Event type might have been deleted by the test
console.log(e);
}
try {
await userRepositoryFixture.delete(user.id);
} catch (e) {
// User might have been deleted by the test
console.log(e);
}
await app.close();
});
@@ -351,6 +351,7 @@ describe("Organizations Users Endpoints", () => {
email: `organizations-users-new-member-${randomString()}@api.com`,
bio,
metadata,
timeZone: "Europe/Rome",
};
const emailSpy = jest
@@ -367,6 +368,7 @@ describe("Organizations Users Endpoints", () => {
expect(userData.email).toBe(newOrgUser.email);
expect(userData.bio).toBe(newOrgUser.bio);
expect(userData.metadata).toEqual(newOrgUser.metadata);
expect(userData.timeZone).toBe(newOrgUser.timeZone);
expect(emailSpy).toHaveBeenCalledWith({
usernameOrEmail: newOrgUser.email,
orgName: org.name,
@@ -1,3 +1,4 @@
import { CapitalizeTimeZone } from "@/lib/inputs/capitalize-timezone";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Expose, Transform } from "class-transformer";
import {
@@ -112,6 +113,7 @@ export class CreateUserInput {
@ApiProperty({ type: String, required: false, description: "Time zone", example: "America/New_York" })
@IsOptional()
@IsString()
@CapitalizeTimeZone()
@Validate(TimeZoneValidator)
@Expose()
timeZone?: string;
@@ -1,6 +1,6 @@
import type { ValidatorConstraintInterface } from "class-validator";
import { ValidatorConstraint } from "class-validator";
import tzdata from "tzdata";
import * as tzdata from "tzdata";
@ValidatorConstraint({ name: "timezoneValidator", async: false })
export class TimeZoneValidator implements ValidatorConstraintInterface {
-8
View File
@@ -636,10 +636,6 @@ select:focus {
@apply !border-r-default left-0.5 !border-y-0 !border-l-0;
}
.intercom-lightweight-app {
@apply z-40 !important;
}
@import "../../web/styles/tokens.css";
@layer base {
@@ -1027,10 +1023,6 @@ select:focus {
@apply !border-r-default left-0.5 !border-y-0 !border-l-0;
}
.intercom-lightweight-app {
@apply z-40 !important;
}
.prose :where(a:not([data-card])):not(:where([class~="not-prose"] *)) {
text-decoration: none;
}
@@ -5,10 +5,11 @@ import { unstable_cache } from "next/cache";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
import { OrganizationRepository } from "@calcom/lib/server/repository/organization";
import { MembershipRole } from "@calcom/prisma/enums";
import { availabilityRouter } from "@calcom/trpc/server/routers/viewer/availability/_router";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -72,8 +73,14 @@ const Page = async ({ searchParams: _searchParams }: PageProps) => {
orgId: organizationId,
})
: false;
const isOrgAdminOrOwner = checkAdminOrOwner(session?.user?.org?.role);
const canViewTeamAvailability = isOrgAdminOrOwner || !isOrgPrivate;
const permissionService = new PermissionCheckService();
const teamIdsWithPermission = await permissionService.getTeamIdsWithPermission({
userId: session.user.id,
permission: "availability.read",
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
});
const canViewTeamAvailability = teamIdsWithPermission.length > 0 || !isOrgPrivate;
return (
<ShellMainAppDir
@@ -23,7 +23,8 @@ const ServerPage = async ({ searchParams: _searchParams }: ServerPageProps) => {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const searchParams = await _searchParams;
const token = Array.isArray(searchParams?.token) ? searchParams.token[0] : searchParams?.token;
const callbackUrl = token ? `/teams?token=${encodeURIComponent(token)}` : null;
const autoAccept = Array.isArray(searchParams?.autoAccept) ? searchParams.autoAccept[0] : searchParams?.autoAccept;
const callbackUrl = token ? `/teams?token=${encodeURIComponent(token)}${autoAccept ? `&autoAccept=${encodeURIComponent(autoAccept)}` : ''}` : null;
if (!session) {
redirect(callbackUrl ? `/auth/login?callbackUrl=${callbackUrl}` : "/auth/login");
@@ -2,11 +2,12 @@ import type { SearchParams } from "app/_types";
import type { Session } from "next-auth";
import { unstable_cache } from "next/cache";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import { TeamsListing } from "@calcom/features/ee/teams/components/TeamsListing";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { TeamRepository } from "@calcom/lib/server/repository/team";
import { TeamService } from "@calcom/lib/server/service/teamService";
import prisma from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
@@ -32,14 +33,21 @@ export const ServerTeamsListing = async ({
session: Session;
}) => {
const token = Array.isArray(searchParams?.token) ? searchParams.token[0] : searchParams?.token;
const autoAccept = Array.isArray(searchParams?.autoAccept) ? searchParams.autoAccept[0] : searchParams?.autoAccept;
const userId = session.user.id;
let invitationAccepted = false;
let teamNameFromInvite,
errorMsgFromInvite = null;
if (token) {
try {
teamNameFromInvite = await TeamService.inviteMemberByToken(token, userId);
if (autoAccept === "true") {
await TeamService.acceptInvitationByToken(token, userId);
invitationAccepted = true;
} else {
teamNameFromInvite = await TeamService.inviteMemberByToken(token, userId);
}
} catch (e) {
errorMsgFromInvite = "Error while fetching teams";
if (e instanceof TRPCError) errorMsgFromInvite = e.message;
@@ -49,21 +57,30 @@ export const ServerTeamsListing = async ({
const teams = await getCachedTeams(userId);
const userProfile = session?.user?.profile;
const orgId = userProfile?.organizationId ?? session?.user.org?.id;
const orgRole =
session?.user?.org?.role ??
userProfile?.organization?.members.find((m: { userId: number }) => m.userId === userId)?.role;
const isOrgAdminOrOwner = checkAdminOrOwner(orgRole);
const permissionCheckService = new PermissionCheckService();
const canCreateTeam = orgId
? await permissionCheckService.checkPermission({
userId: session.user.id,
teamId: orgId,
permission: "team.create",
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
})
: false;
return {
Main: (
<TeamsListing
invitationAccepted={invitationAccepted}
teams={teams}
orgId={orgId ?? null}
isOrgAdmin={isOrgAdminOrOwner}
permissions={{
canCreateTeam: canCreateTeam,
}}
teamNameFromInvite={teamNameFromInvite ?? null}
errorMsgFromInvite={errorMsgFromInvite}
/>
),
CTA: !orgId || isOrgAdminOrOwner ? <TeamsCTA /> : null,
CTA: !orgId || canCreateTeam ? <TeamsCTA /> : null,
};
};
@@ -18,6 +18,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
import { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { MembershipRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import { ArrowButton } from "@calcom/ui/components/arrow-button";
import { Badge } from "@calcom/ui/components/badge";
@@ -46,6 +47,10 @@ function NewFormButton({ setNewFormDialogState }: { setNewFormDialogState: SetNe
createFunction={(teamId) => {
setNewFormDialogState({ action: "new", target: teamId ? String(teamId) : "" });
}}
withPermission={{
permission: "routingForm.create",
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
}}
/>
);
}
@@ -175,6 +175,7 @@ const organizationAdminKeys = ["privacy", "OAuth Clients", "SSO", "directory_syn
export interface SettingsPermissions {
canViewRoles?: boolean;
canViewOrganizationBilling?: boolean;
canUpdateOrganization?: boolean;
}
const useTabs = ({
@@ -190,7 +191,6 @@ const useTabs = ({
const { data: user } = trpc.viewer.me.get.useQuery({ includePasswordAdded: true });
const orgBranding = useOrgBranding();
const isAdmin = session.data?.user.role === UserPermissionRole.ADMIN;
const isOrgAdminOrOwner = checkAdminOrOwner(orgBranding?.role);
const processTabsMemod = useMemo(() => {
const processedTabs = getTabs(orgBranding).map((tab) => {
@@ -203,10 +203,10 @@ const useTabs = ({
};
} else if (tab.href === "/settings/organizations") {
const newArray = (tab?.children ?? []).filter(
(child) => isOrgAdminOrOwner || !organizationAdminKeys.includes(child.name)
(child) => permissions?.canUpdateOrganization || !organizationAdminKeys.includes(child.name)
);
if (isOrgAdminOrOwner) {
if (permissions?.canUpdateOrganization) {
newArray.splice(4, 0, {
name: "attributes",
href: "/settings/organizations/attributes",
@@ -238,7 +238,7 @@ const useTabs = ({
});
}
} else {
if (isOrgAdminOrOwner) {
if (permissions?.canUpdateOrganization) {
newArray.push({
name: "billing",
href: "/settings/organizations/billing",
@@ -264,7 +264,7 @@ const useTabs = ({
return { ...tab, children: filtered };
} else if (tab.href === "/settings/developer") {
const filtered = tab?.children?.filter(
(childTab) => isOrgAdminOrOwner || childTab.name !== "admin_api"
(childTab) => permissions?.canUpdateOrganization || childTab.name !== "admin_api"
);
return { ...tab, children: filtered };
}
@@ -274,12 +274,12 @@ const useTabs = ({
// check if name is in adminRequiredKeys
return processedTabs.filter((tab) => {
if (organizationRequiredKeys.includes(tab.name)) return !!orgBranding;
if (tab.name === "other_teams" && !isOrgAdminOrOwner) return false;
if (tab.name === "other_teams" && !permissions?.canUpdateOrganization) return false;
if (isAdmin) return true;
return !adminRequiredKeys.includes(tab.name);
});
}, [isAdmin, orgBranding, isOrgAdminOrOwner, user, isDelegationCredentialEnabled, isPbacEnabled, permissions]);
}, [isAdmin, orgBranding, user, isDelegationCredentialEnabled, isPbacEnabled, permissions]);
return processTabsMemod;
};
@@ -560,8 +560,6 @@ const SettingsSidebarContainer = ({
}
}, [searchParams?.get("id"), otherTeams]);
const isOrgAdminOrOwner = checkAdminOrOwner(orgBranding?.role);
return (
<nav
style={{ maxHeight: `calc(100vh - ${bannersHeight}px)`, top: `${bannersHeight}px` }}
@@ -638,12 +636,12 @@ const SettingsSidebarContainer = ({
as="p"
className="text-subtle truncate text-sm font-medium leading-5"
loadingClassName="ms-3">
{t(isOrgAdminOrOwner ? "my_teams" : tab.name)}
{t("my_teams")}
</Skeleton>
</div>
</Link>
<TeamListCollapsible teamFeatures={teamFeatures} />
{(!orgBranding?.id || isOrgAdminOrOwner) && (
{(!orgBranding?.id || permissions?.canUpdateOrganization) && (
<VerticalTabItem
name={t("add_a_team")}
href={`${WEBAPP_URL}/settings/teams/new`}
@@ -47,6 +47,7 @@ export default async function SettingsLayoutAppDir(props: SettingsLayoutProps) {
let teamFeatures: Record<number, TeamFeatures> | null = null;
let canViewRoles = false;
let canViewOrganizationBilling = false;
let canUpdateOrganization = false;
const orgId = session?.user?.profile?.organizationId ?? session?.user.org?.id;
// For now we only grab organization features but it would be nice to fetch these on the server side for specific team feature flags
@@ -68,6 +69,11 @@ export default async function SettingsLayoutAppDir(props: SettingsLayoutProps) {
canViewRoles = roleActions[CrudAction.Read] ?? false;
const orgActions = PermissionMapper.toActionMap(organizationPermissions, Resource.Organization);
canViewOrganizationBilling = orgActions[CustomAction.ManageBilling] ?? isOrgAdminOrOwner;
canUpdateOrganization = orgActions[CrudAction.Update] ?? isOrgAdminOrOwner;
} else {
canViewRoles = false;
canViewOrganizationBilling = isOrgAdminOrOwner;
canUpdateOrganization = isOrgAdminOrOwner;
}
}
@@ -76,7 +82,7 @@ export default async function SettingsLayoutAppDir(props: SettingsLayoutProps) {
<SettingsLayoutAppDirClient
{...props}
teamFeatures={teamFeatures ?? {}}
permissions={{ canViewRoles, canViewOrganizationBilling }}
permissions={{ canViewRoles, canViewOrganizationBilling, canUpdateOrganization }}
/>
</>
);
@@ -22,6 +22,7 @@ interface AdvancedPermissionGroupProps {
onChange: (permissions: string[]) => void;
disabled?: boolean;
scope?: Scope;
isPrivate?: boolean;
}
const INTERNAL_DATAACCESS_KEY = "_resource";
@@ -32,10 +33,11 @@ export function AdvancedPermissionGroup({
onChange,
disabled,
scope = Scope.Organization,
isPrivate = false,
}: AdvancedPermissionGroupProps) {
const { t } = useLocale();
const { toggleSinglePermission, toggleResourcePermissionLevel } = usePermissions(scope);
const scopedRegistry = getPermissionsForScope(scope);
const scopedRegistry = getPermissionsForScope(scope, isPrivate);
const resourceConfig = scopedRegistry[resource];
const [isExpanded, setIsExpanded] = useState(false);
@@ -56,9 +56,17 @@ interface RoleSheetProps {
onOpenChange: (open: boolean) => void;
teamId: number;
scope?: Scope;
isPrivate?: boolean; // Add isPrivate prop to control permission visibility
}
export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Organization }: RoleSheetProps) {
export function RoleSheet({
role,
open,
onOpenChange,
teamId,
scope = Scope.Organization,
isPrivate = false
}: RoleSheetProps) {
const { t } = useLocale();
const router = useRouter();
const isEditing = Boolean(role);
@@ -108,7 +116,8 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
const { isAdvancedMode, permissions, color } = form.watch();
const { filteredResources, scopedRegistry } = useMemo(() => {
const scopedRegistry = getPermissionsForScope(scope);
// Use privacy-aware filtering if we have privacy information
const scopedRegistry = getPermissionsForScope(scope, isPrivate);
const filteredResources = Object.keys(scopedRegistry).filter((resource) =>
t(
scopedRegistry[resource as Resource][CrudAction.All as keyof (typeof scopedRegistry)[Resource]]
@@ -118,7 +127,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
.includes(searchQuery.toLowerCase())
);
return { filteredResources, scopedRegistry };
}, [searchQuery, t, scope]);
}, [searchQuery, t, scope, isPrivate]);
const createMutation = trpc.viewer.pbac.createRole.useMutation({
onSuccess: async () => {
@@ -221,6 +230,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
onChange={(newPermissions) => form.setValue("permissions", newPermissions)}
disabled={isSystemRole}
scope={scope}
isPrivate={isPrivate}
/>
))}{" "}
</div>
@@ -245,6 +255,7 @@ export function RoleSheet({ role, open, onOpenChange, teamId, scope = Scope.Orga
onChange={(newPermissions) => form.setValue("permissions", newPermissions)}
disabled={isSystemRole}
scope={scope}
isPrivate={isPrivate}
/>
))}
</div>{" "}
@@ -48,6 +48,7 @@ interface RolesListProps {
initialSheetOpen?: boolean;
teamId: number;
scope?: Scope;
isPrivate?: boolean;
}
export function RolesList({
@@ -57,6 +58,7 @@ export function RolesList({
initialSheetOpen,
teamId,
scope,
isPrivate,
}: RolesListProps) {
const { t } = useLocale();
const { isOpen, setIsOpen, selectedRoleId, setSelectedRoleId, handleSheetOpenChange } = useRoleStates(
@@ -113,6 +115,7 @@ export function RolesList({
onOpenChange={handleSheetOpenChange}
teamId={teamId}
scope={scope}
isPrivate={isPrivate}
/>
</>
);
@@ -18,6 +18,7 @@ interface SimplePermissionItemProps {
onChange: (permissions: string[]) => void;
disabled?: boolean;
scope?: Scope;
isPrivate?: boolean;
}
export function SimplePermissionItem({
@@ -26,10 +27,11 @@ export function SimplePermissionItem({
onChange,
disabled,
scope = Scope.Organization,
isPrivate = false,
}: SimplePermissionItemProps) {
const { t } = useLocale();
const { getResourcePermissionLevel, toggleResourcePermissionLevel } = usePermissions(scope);
const scopedRegistry = getPermissionsForScope(scope);
const scopedRegistry = getPermissionsForScope(scope, isPrivate);
const registry = scopedRegistry || PERMISSION_REGISTRY;
const isAllResources = resource === "*";
@@ -44,6 +44,18 @@ const getCachedResourcePermissions = unstable_cache(
{ revalidate: 3600 }
);
const getCachedTeamPrivacy = unstable_cache(
async (teamId: number) => {
const team = await prisma.team.findUnique({
where: { id: teamId },
select: { isPrivate: true },
});
return team?.isPrivate ?? false;
},
["team-privacy"],
{ revalidate: 3600 }
);
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("roles_and_permissions"),
@@ -69,9 +81,10 @@ const Page = async ({ searchParams }: { searchParams: Record<string, string | st
roleSearchParamsCache.parse(searchParams);
const [roles, rolePermissions] = await Promise.all([
const [roles, rolePermissions, isPrivate] = await Promise.all([
getCachedTeamRoles(session.user.org.id),
getCachedResourcePermissions(session.user.id, session.user.org.id, Resource.Role),
getCachedTeamPrivacy(session.user.org.id),
]);
// NOTE: this approach of fetching permssions per resource does not account for fall back roles.
@@ -108,6 +121,7 @@ const Page = async ({ searchParams }: { searchParams: Record<string, string | st
}}
initialSelectedRole={selectedRole}
initialSheetOpen={isSheetOpen}
isPrivate={isPrivate}
/>
</SettingsHeader>
);
@@ -102,6 +102,7 @@ const Page = async ({ params }: { params: Promise<{ id: string }> }) => {
CustomAction.ChangeMemberRole,
CustomAction.Remove,
CustomAction.ListMembers,
CustomAction.ListMembersPrivate,
CustomAction.Impersonate,
],
fallbackRoles: {
@@ -120,12 +121,17 @@ const Page = async ({ params }: { params: Promise<{ id: string }> }) => {
[CustomAction.Impersonate]: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
[CustomAction.ListMembersPrivate]: {
roles: fallbackRolesCanListMembers,
},
},
});
// Map specific permissions to member actions
const memberPermissions = {
canListMembers: permissions[CustomAction.ListMembers],
canListMembers: team.isPrivate
? permissions[CustomAction.ListMembersPrivate]
: permissions[CustomAction.ListMembers],
canInvite: permissions[CustomAction.Invite],
canChangeMemberRole: permissions[CustomAction.ChangeMemberRole],
canRemove: permissions[CustomAction.Remove],
@@ -4,6 +4,7 @@ import { cookies, headers } from "next/headers";
import { notFound } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { getTeamWithMembers } from "@calcom/features/ee/teams/lib/queries";
import type { AppFlags } from "@calcom/features/flags/config";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { PermissionMapper } from "@calcom/features/pbac/domain/mappers/PermissionMapper";
@@ -11,7 +12,6 @@ import { Resource, CrudAction, Scope } from "@calcom/features/pbac/domain/types/
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { RoleService } from "@calcom/features/pbac/services/role.service";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import { getTeamWithMembers } from "@calcom/features/ee/teams/lib/queries";
import { prisma } from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
@@ -51,6 +51,19 @@ const getCachedResourcePermissions = (userId: number, teamId: number, resource:
{ revalidate: 3600, tags: [`resource-permissions-${teamId}`] }
);
const getCachedTeamPrivacy = (teamId: number) =>
unstable_cache(
async () => {
const team = await prisma.team.findUnique({
where: { id: teamId },
select: { isPrivate: true },
});
return team?.isPrivate ?? false;
},
[`team-privacy-${teamId}`],
{ revalidate: 3600, tags: [`team-privacy-${teamId}`] }
);
const getCachedTeam = (teamId: string, userId: number) =>
unstable_cache(
async () => {
@@ -111,9 +124,10 @@ const Page = async ({
roleSearchParamsCache.parse(searchParams);
const [roles, rolePermissions] = await Promise.all([
const [roles, rolePermissions, isPrivate] = await Promise.all([
getCachedTeamRoles(team.id)(),
getCachedResourcePermissions(session.user.id, team.id, Resource.Role)(),
getCachedTeamPrivacy(team.id)(),
]);
// NOTE: this approach of fetching permssions per resource does not account for fall back roles.
@@ -153,6 +167,7 @@ const Page = async ({
initialSelectedRole={selectedRole}
initialSheetOpen={isSheetOpen}
scope={Scope.Team}
isPrivate={isPrivate}
/>
</SettingsHeader>
);
@@ -70,6 +70,7 @@ const Page = async () => {
userRole: session.user.org.role,
actions: [
CustomAction.ListMembers,
CustomAction.ListMembersPrivate,
CustomAction.Invite,
CustomAction.ChangeMemberRole,
CustomAction.Remove,
@@ -79,6 +80,9 @@ const Page = async () => {
[CustomAction.ListMembers]: {
roles: fallbackRolesThatCanSeeMembers,
},
[CustomAction.ListMembersPrivate]: {
roles: fallbackRolesThatCanSeeMembers,
},
[CustomAction.Invite]: {
roles: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
@@ -96,7 +100,9 @@ const Page = async () => {
// Map specific permissions to member actions
const memberPermissions = {
canListMembers: permissions[CustomAction.ListMembers],
canListMembers: org.isPrivate
? permissions[CustomAction.ListMembersPrivate]
: permissions[CustomAction.ListMembers],
canInvite: permissions[CustomAction.Invite],
canChangeMemberRole: permissions[CustomAction.ChangeMemberRole],
canRemove: permissions[CustomAction.Remove],
@@ -0,0 +1,213 @@
import { NextRequest } from "next/server";
import { describe, test, expect, vi, beforeEach } from "vitest";
import { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
vi.mock("next/server", () => ({
NextRequest: class MockNextRequest {
url: string;
method: string;
nextUrl: { searchParams: URLSearchParams };
private _headers: Map<string, string>;
constructor(url: string, options: { method?: string } = {}) {
this.url = url;
this.method = options.method || "GET";
this._headers = new Map();
this.nextUrl = { searchParams: new URLSearchParams(url.split("?")[1] || "") };
}
headers = {
get: (key: string): string | null => this._headers.get(key.toLowerCase()) || null,
set: (key: string, value: string): void => {
this._headers.set(key.toLowerCase(), value);
},
has: (key: string): boolean => this._headers.has(key.toLowerCase()),
};
},
NextResponse: {
json: vi.fn((body, init) => ({
json: vi.fn().mockResolvedValue(body),
status: init?.status || 200,
})),
},
}));
vi.mock("@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService");
vi.mock("@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventRepository");
vi.mock("@calcom/lib/logger", () => ({
default: {
getSubLogger: vi.fn(() => ({
info: vi.fn(),
error: vi.fn(),
})),
},
}));
vi.mock("@calcom/lib/server/perfObserver", () => ({
performance: {
mark: vi.fn(),
measure: vi.fn(),
},
}));
vi.mock("@sentry/nextjs", () => ({
wrapApiHandlerWithSentry: vi.fn((handler) => handler),
captureException: vi.fn(),
}));
vi.mock("@calcom/lib/server/getServerErrorFromUnknown", () => ({
getServerErrorFromUnknown: vi.fn((error) => ({
message: error instanceof Error ? error.message : "Unknown error",
statusCode: 500,
url: "test-url",
method: "GET",
})),
}));
vi.mock("../../defaultResponderForAppDir", () => ({
defaultResponderForAppDir: vi.fn((handler) => handler),
}));
vi.mock("@calcom/prisma", () => ({
prisma: {},
}));
const mockCalendarCacheEventService = vi.mocked(CalendarCacheEventService);
describe("/api/cron/calendar-subscriptions-cleanup", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.stubEnv("CRON_API_KEY", "test-cron-key");
vi.stubEnv("CRON_SECRET", "test-cron-secret");
});
describe("Authentication", () => {
test("should return 403 when no API key is provided", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions-cleanup");
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(403);
const body = await response.json();
expect(body.message).toBe("Forbidden");
});
test("should return 403 when invalid API key is provided", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions-cleanup");
request.headers.set("authorization", "invalid-key");
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(403);
const body = await response.json();
expect(body.message).toBe("Forbidden");
});
test("should accept CRON_API_KEY in authorization header", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions-cleanup");
request.headers.set("authorization", "test-cron-key");
const mockCleanupStaleCache = vi.fn().mockResolvedValue(undefined);
mockCalendarCacheEventService.prototype.cleanupStaleCache = mockCleanupStaleCache;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(200);
expect(mockCleanupStaleCache).toHaveBeenCalled();
});
test("should accept CRON_SECRET as Bearer token", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions-cleanup");
request.headers.set("authorization", "Bearer test-cron-secret");
const mockCleanupStaleCache = vi.fn().mockResolvedValue(undefined);
mockCalendarCacheEventService.prototype.cleanupStaleCache = mockCleanupStaleCache;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(200);
expect(mockCleanupStaleCache).toHaveBeenCalled();
});
test("should accept API key as query parameter", async () => {
const request = new NextRequest(
"http://localhost/api/cron/calendar-subscriptions-cleanup?apiKey=test-cron-key"
);
const mockCleanupStaleCache = vi.fn().mockResolvedValue(undefined);
mockCalendarCacheEventService.prototype.cleanupStaleCache = mockCleanupStaleCache;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(200);
expect(mockCleanupStaleCache).toHaveBeenCalled();
});
});
describe("Cleanup functionality", () => {
test("should successfully cleanup stale cache", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions-cleanup");
request.headers.set("authorization", "test-cron-key");
const mockCleanupStaleCache = vi.fn().mockResolvedValue(undefined);
mockCalendarCacheEventService.prototype.cleanupStaleCache = mockCleanupStaleCache;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(200);
const body = await response.json();
expect(body.ok).toBe(true);
expect(mockCleanupStaleCache).toHaveBeenCalledOnce();
});
test("should handle cleanup errors gracefully", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions-cleanup");
request.headers.set("authorization", "test-cron-key");
const mockError = new Error("Database connection failed");
const mockCleanupStaleCache = vi.fn().mockRejectedValue(mockError);
mockCalendarCacheEventService.prototype.cleanupStaleCache = mockCleanupStaleCache;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(500);
const body = await response.json();
expect(body.message).toBe("Database connection failed");
});
test("should handle non-Error exceptions", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions-cleanup");
request.headers.set("authorization", "test-cron-key");
const mockCleanupStaleCache = vi.fn().mockRejectedValue("String error");
mockCalendarCacheEventService.prototype.cleanupStaleCache = mockCleanupStaleCache;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(500);
const body = await response.json();
expect(body.message).toBe("Unknown error");
});
});
describe("Service instantiation", () => {
test("should instantiate CalendarCacheEventService with correct dependencies", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions-cleanup");
request.headers.set("authorization", "test-cron-key");
const mockCleanupStaleCache = vi.fn().mockResolvedValue(undefined);
mockCalendarCacheEventService.prototype.cleanupStaleCache = mockCleanupStaleCache;
const { GET } = await import("../route");
await GET(request, { params: Promise.resolve({}) });
expect(mockCalendarCacheEventService).toHaveBeenCalledWith({
calendarCacheEventRepository: expect.any(Object),
});
});
});
});
@@ -0,0 +1,39 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { CalendarCacheEventRepository } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventRepository";
import { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
import { prisma } from "@calcom/prisma";
import { defaultResponderForAppDir } from "@calcom/web/app/api/defaultResponderForAppDir";
/**
* Cron webhook
* Cleanup stale calendar cache
*
* @param request
* @returns
*/
async function getHandler(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (![process.env.CRON_API_KEY, `Bearer ${process.env.CRON_SECRET}`].includes(`${apiKey}`)) {
return NextResponse.json({ message: "Forbidden" }, { status: 403 });
}
// instantiate dependencies
const calendarCacheEventRepository = new CalendarCacheEventRepository(prisma);
const calendarCacheEventService = new CalendarCacheEventService({
calendarCacheEventRepository,
});
try {
await calendarCacheEventService.cleanupStaleCache();
return NextResponse.json({ ok: true });
} catch (e: unknown) {
const message = e instanceof Error ? e.message : "Unknown error";
console.error(`[calendar-subscriptions-cleanup] ${message}:`, e);
return NextResponse.json({ message }, { status: 500 });
}
}
export const GET = defaultResponderForAppDir(getHandler);
@@ -0,0 +1,229 @@
import { NextRequest } from "next/server";
import { describe, test, expect, vi, beforeEach } from "vitest";
import { CalendarSubscriptionService } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionService";
vi.mock("next/server", () => ({
NextRequest: class MockNextRequest {
url: string;
method: string;
nextUrl: { searchParams: URLSearchParams };
private _headers: Map<string, string>;
constructor(url: string, options: { method?: string } = {}) {
this.url = url;
this.method = options.method || "GET";
this._headers = new Map();
this.nextUrl = { searchParams: new URLSearchParams(url.split("?")[1] || "") };
}
headers = {
get: (key: string): string | null => this._headers.get(key.toLowerCase()) || null,
set: (key: string, value: string): void => {
this._headers.set(key.toLowerCase(), value);
},
has: (key: string): boolean => this._headers.has(key.toLowerCase()),
};
},
NextResponse: {
json: vi.fn((body, init) => ({
json: vi.fn().mockResolvedValue(body),
status: init?.status || 200,
})),
},
}));
vi.mock("@calcom/features/calendar-subscription/lib/CalendarSubscriptionService");
vi.mock("@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService");
vi.mock("@calcom/features/calendar-subscription/lib/sync/CalendarSyncService");
vi.mock("@calcom/prisma", () => ({
prisma: {},
}));
const mockCalendarSubscriptionService = vi.mocked(CalendarSubscriptionService);
describe("/api/cron/calendar-subscriptions", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
vi.stubEnv("CRON_API_KEY", "test-cron-key");
vi.stubEnv("CRON_SECRET", "test-cron-secret");
});
describe("Authentication", () => {
test("should return 403 when no API key is provided", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions");
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(403);
const body = await response.json();
expect(body.message).toBe("Forbiden");
}, 10000);
test("should return 403 when invalid API key is provided", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions");
request.headers.set("authorization", "invalid-key");
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(403);
const body = await response.json();
expect(body.message).toBe("Forbiden");
});
test("should accept valid API key", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions");
request.headers.set("authorization", "test-cron-key");
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(true);
const mockCheckForNewSubscriptions = vi.fn().mockResolvedValue(undefined);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.checkForNewSubscriptions = mockCheckForNewSubscriptions;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(200);
});
});
describe("Feature flag checks", () => {
test("should return early when cache AND sync are disabled", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions");
request.headers.set("authorization", "test-cron-key");
const mockIsCacheEnabled = vi.fn().mockResolvedValue(false);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(false);
const mockCheckForNewSubscriptions = vi.fn();
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.checkForNewSubscriptions = mockCheckForNewSubscriptions;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(200);
const body = await response.json();
expect(body.ok).toBe(true);
expect(mockCheckForNewSubscriptions).not.toHaveBeenCalled();
});
test("should proceed when both cache and sync are enabled", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions");
request.headers.set("authorization", "test-cron-key");
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(true);
const mockCheckForNewSubscriptions = vi.fn().mockResolvedValue(undefined);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.checkForNewSubscriptions = mockCheckForNewSubscriptions;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(200);
const body = await response.json();
expect(body.ok).toBe(true);
expect(mockCheckForNewSubscriptions).toHaveBeenCalledOnce();
});
});
describe("Subscription checking functionality", () => {
test("should successfully check for new subscriptions", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions");
request.headers.set("authorization", "test-cron-key");
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(true);
const mockCheckForNewSubscriptions = vi.fn().mockResolvedValue(undefined);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.checkForNewSubscriptions = mockCheckForNewSubscriptions;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(200);
const body = await response.json();
expect(body.ok).toBe(true);
expect(mockCheckForNewSubscriptions).toHaveBeenCalledOnce();
});
test("should handle subscription checking errors gracefully", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions");
request.headers.set("authorization", "test-cron-key");
const mockError = new Error("Subscription service unavailable");
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(true);
const mockCheckForNewSubscriptions = vi.fn().mockRejectedValue(mockError);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.checkForNewSubscriptions = mockCheckForNewSubscriptions;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(500);
const body = await response.json();
expect(body.message).toBe("Subscription service unavailable");
});
test("should handle non-Error exceptions", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions");
request.headers.set("authorization", "test-cron-key");
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(true);
const mockCheckForNewSubscriptions = vi.fn().mockRejectedValue("String error");
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.checkForNewSubscriptions = mockCheckForNewSubscriptions;
const { GET } = await import("../route");
const response = await GET(request, { params: Promise.resolve({}) });
expect(response.status).toBe(500);
const body = await response.json();
expect(body.message).toBe("Unknown error");
});
});
describe("Service instantiation", () => {
test("should instantiate all services with correct dependencies", async () => {
const request = new NextRequest("http://localhost/api/cron/calendar-subscriptions");
request.headers.set("authorization", "test-cron-key");
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(true);
const mockCheckForNewSubscriptions = vi.fn().mockResolvedValue(undefined);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.checkForNewSubscriptions = mockCheckForNewSubscriptions;
const { GET } = await import("../route");
await GET(request, { params: Promise.resolve({}) });
expect(mockCalendarSubscriptionService).toHaveBeenCalledWith({
adapterFactory: expect.any(Object),
selectedCalendarRepository: expect.any(Object),
featuresRepository: expect.any(Object),
calendarSyncService: expect.any(Object),
calendarCacheEventService: expect.any(Object),
});
});
});
});
@@ -0,0 +1,66 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { DefaultAdapterFactory } from "@calcom/features/calendar-subscription/adapters/AdaptersFactory";
import { CalendarSubscriptionService } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionService";
import { CalendarCacheEventRepository } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventRepository";
import { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
import { CalendarSyncService } from "@calcom/features/calendar-subscription/lib/sync/CalendarSyncService";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/SelectedCalendarRepository";
import { BookingRepository } from "@calcom/lib/server/repository/booking";
import { prisma } from "@calcom/prisma";
import { defaultResponderForAppDir } from "@calcom/web/app/api/defaultResponderForAppDir";
/**
* Cron webhook
* Checks for new calendar subscriptions (rollouts)
*
* @param request
* @returns
*/
async function getHandler(request: NextRequest) {
const apiKey = request.headers.get("authorization") || request.nextUrl.searchParams.get("apiKey");
if (![process.env.CRON_API_KEY, `Bearer ${process.env.CRON_SECRET}`].includes(`${apiKey}`)) {
return NextResponse.json({ message: "Forbiden" }, { status: 403 });
}
// instantiate dependencies
const bookingRepository = new BookingRepository(prisma);
const calendarSyncService = new CalendarSyncService({
bookingRepository,
});
const calendarCacheEventRepository = new CalendarCacheEventRepository(prisma);
const calendarCacheEventService = new CalendarCacheEventService({
calendarCacheEventRepository,
});
const calendarSubscriptionService = new CalendarSubscriptionService({
adapterFactory: new DefaultAdapterFactory(),
selectedCalendarRepository: new SelectedCalendarRepository(prisma),
featuresRepository: new FeaturesRepository(prisma),
calendarSyncService,
calendarCacheEventService,
});
// are features globally enabled
const [isCacheEnabled, isSyncEnabled] = await Promise.all([
calendarSubscriptionService.isCacheEnabled(),
calendarSubscriptionService.isSyncEnabled(),
]);
if (!isCacheEnabled && !isSyncEnabled) {
return NextResponse.json({ ok: true });
}
try {
await calendarSubscriptionService.checkForNewSubscriptions();
return NextResponse.json({ ok: true });
} catch (e) {
const message = e instanceof Error ? e.message : "Unknown error";
return NextResponse.json({ message }, { status: 500 });
}
}
export const GET = defaultResponderForAppDir(getHandler);
@@ -1,5 +1,3 @@
import { wrapApiHandlerWithSentry } from "@sentry/nextjs";
import { captureException } from "@sentry/nextjs";
import type { Params } from "app/_types";
import { ApiError } from "next/dist/server/api-utils";
import type { NextRequest } from "next/server";
@@ -21,10 +19,13 @@ export const defaultResponderForAppDir = <T extends NextResponse | Response = Ne
let ok = false;
try {
performance.mark("Start");
const result = endpointRoute
? await wrapApiHandlerWithSentry(async () => await handler(req, { params }), endpointRoute)()
: await handler(req, { params });
let result: T | undefined;
if (process.env.NODE_ENV === "development" || !endpointRoute) {
result = await handler(req, { params });
} else {
const { wrapApiHandlerWithSentry } = await import("@sentry/nextjs");
result = await wrapApiHandlerWithSentry(async () => await handler(req, { params }), endpointRoute)();
}
ok = true;
if (result) {
@@ -49,6 +50,7 @@ export const defaultResponderForAppDir = <T extends NextResponse | Response = Ne
// Don't report 400-499 errors to Sentry/console
if (!(serverError.statusCode >= 400 && serverError.statusCode < 500)) {
console.error(serverError);
const { captureException } = await import("@sentry/nextjs");
captureException(error);
}
@@ -0,0 +1,315 @@
import { NextRequest } from "next/server";
import { describe, test, expect, vi, beforeEach } from "vitest";
import { CalendarSubscriptionService } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionService";
vi.mock("next/server", () => ({
NextRequest: class MockNextRequest {
url: string;
method: string;
nextUrl: { searchParams: URLSearchParams };
private _headers: Map<string, string>;
constructor(url: string, options: { method?: string } = {}) {
this.url = url;
this.method = options.method || "POST";
this._headers = new Map();
this.nextUrl = { searchParams: new URLSearchParams(url.split("?")[1] || "") };
}
headers = {
get: (key: string): string | null => this._headers.get(key.toLowerCase()) || null,
set: (key: string, value: string): void => {
this._headers.set(key.toLowerCase(), value);
},
has: (key: string): boolean => this._headers.has(key.toLowerCase()),
};
},
NextResponse: {
json: vi.fn((body, init) => ({
json: vi.fn().mockResolvedValue(body),
status: init?.status || 200,
})),
},
}));
vi.mock("@calcom/features/calendar-subscription/lib/CalendarSubscriptionService");
vi.mock("@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService");
vi.mock("@calcom/features/calendar-subscription/lib/sync/CalendarSyncService");
vi.mock("@calcom/prisma", () => ({
prisma: {},
}));
const mockCalendarSubscriptionService = vi.mocked(CalendarSubscriptionService);
describe("/api/webhooks/calendar-subscription/[provider]", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
});
describe("Provider validation", () => {
test("should accept google_calendar provider", async () => {
const request = new NextRequest("http://localhost/api/webhooks/calendar-subscription/google_calendar", {
method: "POST",
});
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(false);
const mockProcessWebhook = vi.fn().mockResolvedValue(undefined);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.processWebhook = mockProcessWebhook;
const { POST } = await import("../route");
const response = await POST(request, {
params: Promise.resolve({ provider: "google_calendar" }),
});
expect(response.status).toBe(200);
expect(mockProcessWebhook).toHaveBeenCalledWith("google_calendar", request);
}, 10000);
test("should accept office365_calendar provider", async () => {
const request = new NextRequest(
"http://localhost/api/webhooks/calendar-subscription/office365_calendar",
{
method: "POST",
}
);
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(false);
const mockProcessWebhook = vi.fn().mockResolvedValue(undefined);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.processWebhook = mockProcessWebhook;
const { POST } = await import("../route");
const response = await POST(request, {
params: Promise.resolve({ provider: "office365_calendar" }),
});
expect(response.status).toBe(200);
expect(mockProcessWebhook).toHaveBeenCalledWith("office365_calendar", request);
});
test("should reject unsupported provider", async () => {
const request = new NextRequest(
"http://localhost/api/webhooks/calendar-subscription/unsupported_calendar",
{
method: "POST",
}
);
const { POST } = await import("../route");
const response = await POST(request, {
params: Promise.resolve({ provider: "unsupported_calendar" }),
});
expect(response.status).toBe(400);
const body = await response.json();
expect(body.message).toBe("Unsupported provider");
});
});
describe("Feature flag handling", () => {
test("should return 200 when neither cache nor sync is enabled", async () => {
const request = new NextRequest("http://localhost/api/webhooks/calendar-subscription/google_calendar", {
method: "POST",
});
const mockIsCacheEnabled = vi.fn().mockResolvedValue(false);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(false);
const mockProcessWebhook = vi.fn();
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.processWebhook = mockProcessWebhook;
const { POST } = await import("../route");
const response = await POST(request, {
params: Promise.resolve({ provider: "google_calendar" }),
});
expect(response.status).toBe(200);
const body = await response.json();
expect(body.message).toBe("No cache or sync enabled");
expect(mockProcessWebhook).not.toHaveBeenCalled();
});
test("should process webhook when cache is enabled", async () => {
const request = new NextRequest("http://localhost/api/webhooks/calendar-subscription/google_calendar", {
method: "POST",
});
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(false);
const mockProcessWebhook = vi.fn().mockResolvedValue(undefined);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.processWebhook = mockProcessWebhook;
const { POST } = await import("../route");
const response = await POST(request, {
params: Promise.resolve({ provider: "google_calendar" }),
});
expect(response.status).toBe(200);
const body = await response.json();
expect(body.message).toBe("Webhook processed");
expect(mockProcessWebhook).toHaveBeenCalledWith("google_calendar", request);
});
test("should process webhook when sync is enabled", async () => {
const request = new NextRequest("http://localhost/api/webhooks/calendar-subscription/google_calendar", {
method: "POST",
});
const mockIsCacheEnabled = vi.fn().mockResolvedValue(false);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(true);
const mockProcessWebhook = vi.fn().mockResolvedValue(undefined);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.processWebhook = mockProcessWebhook;
const { POST } = await import("../route");
const response = await POST(request, {
params: Promise.resolve({ provider: "google_calendar" }),
});
expect(response.status).toBe(200);
const body = await response.json();
expect(body.message).toBe("Webhook processed");
expect(mockProcessWebhook).toHaveBeenCalledWith("google_calendar", request);
});
test("should process webhook when both cache and sync are enabled", async () => {
const request = new NextRequest("http://localhost/api/webhooks/calendar-subscription/google_calendar", {
method: "POST",
});
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(true);
const mockProcessWebhook = vi.fn().mockResolvedValue(undefined);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.processWebhook = mockProcessWebhook;
const { POST } = await import("../route");
const response = await POST(request, {
params: Promise.resolve({ provider: "google_calendar" }),
});
expect(response.status).toBe(200);
const body = await response.json();
expect(body.message).toBe("Webhook processed");
expect(mockProcessWebhook).toHaveBeenCalledWith("google_calendar", request);
});
});
describe("Error handling", () => {
test("should handle webhook processing errors gracefully", async () => {
const request = new NextRequest("http://localhost/api/webhooks/calendar-subscription/google_calendar", {
method: "POST",
});
const mockError = new Error("Webhook validation failed");
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(false);
const mockProcessWebhook = vi.fn().mockRejectedValue(mockError);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.processWebhook = mockProcessWebhook;
const { POST } = await import("../route");
const response = await POST(request, {
params: Promise.resolve({ provider: "google_calendar" }),
});
expect(response.status).toBe(500);
const body = await response.json();
expect(body.message).toBe("Webhook validation failed");
});
test("should handle non-Error exceptions", async () => {
const request = new NextRequest("http://localhost/api/webhooks/calendar-subscription/google_calendar", {
method: "POST",
});
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(false);
const mockProcessWebhook = vi.fn().mockRejectedValue("String error");
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.processWebhook = mockProcessWebhook;
const { POST } = await import("../route");
const response = await POST(request, {
params: Promise.resolve({ provider: "google_calendar" }),
});
expect(response.status).toBe(500);
const body = await response.json();
expect(body.message).toBe("Unknown error");
});
test("should handle feature flag check errors", async () => {
const request = new NextRequest("http://localhost/api/webhooks/calendar-subscription/google_calendar", {
method: "POST",
});
const mockError = new Error("Feature flag service unavailable");
const mockIsCacheEnabled = vi.fn().mockRejectedValue(mockError);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(false);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
const { POST } = await import("../route");
const response = await POST(request, {
params: Promise.resolve({ provider: "google_calendar" }),
});
expect(response.status).toBe(500);
const body = await response.json();
expect(body.message).toBe("Feature flag service unavailable");
});
});
describe("Service instantiation", () => {
test("should instantiate all services with correct dependencies", async () => {
const request = new NextRequest("http://localhost/api/webhooks/calendar-subscription/google_calendar", {
method: "POST",
});
const mockIsCacheEnabled = vi.fn().mockResolvedValue(true);
const mockIsSyncEnabled = vi.fn().mockResolvedValue(false);
const mockProcessWebhook = vi.fn().mockResolvedValue(undefined);
mockCalendarSubscriptionService.prototype.isCacheEnabled = mockIsCacheEnabled;
mockCalendarSubscriptionService.prototype.isSyncEnabled = mockIsSyncEnabled;
mockCalendarSubscriptionService.prototype.processWebhook = mockProcessWebhook;
const { POST } = await import("../route");
await POST(request, {
params: Promise.resolve({ provider: "google_calendar" }),
});
expect(mockCalendarSubscriptionService).toHaveBeenCalledWith({
adapterFactory: expect.any(Object),
selectedCalendarRepository: expect.any(Object),
featuresRepository: expect.any(Object),
calendarSyncService: expect.any(Object),
calendarCacheEventService: expect.any(Object),
});
});
});
});
@@ -0,0 +1,87 @@
import type { Params } from "app/_types";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import type { CalendarSubscriptionProvider } from "@calcom/features/calendar-subscription/adapters/AdaptersFactory";
import { DefaultAdapterFactory } from "@calcom/features/calendar-subscription/adapters/AdaptersFactory";
import { CalendarSubscriptionService } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionService";
import { CalendarCacheEventRepository } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventRepository";
import { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
import { CalendarSyncService } from "@calcom/features/calendar-subscription/lib/sync/CalendarSyncService";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import logger from "@calcom/lib/logger";
import { SelectedCalendarRepository } from "@calcom/lib/server/repository/SelectedCalendarRepository";
import { BookingRepository } from "@calcom/lib/server/repository/booking";
import { prisma } from "@calcom/prisma";
import { defaultResponderForAppDir } from "@calcom/web/app/api/defaultResponderForAppDir";
const log = logger.getSubLogger({ prefix: ["calendar-webhook"] });
function extractAndValidateProviderFromParams(params: Params): CalendarSubscriptionProvider | null {
if (!("provider" in params)) {
return null;
}
const { provider } = params;
if (provider === "google_calendar" || provider === "office365_calendar") {
return provider;
}
return null;
}
/**
* Handles incoming POST requests for calendar webhooks.
* It processes the webhook based on the calendar provider specified in the URL.
* If the provider is unsupported, it returns a 400 response.
* If the webhook is processed successfully, it returns a 200 response.
* In case of errors during processing, it returns a 500 response with the error message.
* @param {NextRequest} request - The incoming request object.
* @param {Object} context - The context object containing route parameters.
* @param {Promise<Params>} context.params - A promise that resolves to the route parameters.
* @returns {Promise<NextResponse>} - A promise that resolves to the response object.
*/
async function postHandler(request: NextRequest, ctx: { params: Promise<Params> }) {
const providerFromParams = extractAndValidateProviderFromParams(await ctx.params);
if (!providerFromParams) {
return NextResponse.json({ message: "Unsupported provider" }, { status: 400 });
}
try {
// instantiate dependencies
const bookingRepository = new BookingRepository(prisma);
const calendarSyncService = new CalendarSyncService({
bookingRepository,
});
const calendarCacheEventRepository = new CalendarCacheEventRepository(prisma);
const calendarCacheEventService = new CalendarCacheEventService({
calendarCacheEventRepository,
});
const calendarSubscriptionService = new CalendarSubscriptionService({
adapterFactory: new DefaultAdapterFactory(),
selectedCalendarRepository: new SelectedCalendarRepository(prisma),
featuresRepository: new FeaturesRepository(prisma),
calendarSyncService,
calendarCacheEventService,
});
// are features globally enabled
const [isCacheEnabled, isSyncEnabled] = await Promise.all([
calendarSubscriptionService.isCacheEnabled(),
calendarSubscriptionService.isSyncEnabled(),
]);
if (!isCacheEnabled && !isSyncEnabled) {
log.debug("No cache or sync enabled");
return NextResponse.json({ message: "No cache or sync enabled" }, { status: 200 });
}
await calendarSubscriptionService.processWebhook(providerFromParams, request);
return NextResponse.json({ message: "Webhook processed" }, { status: 200 });
} catch (error) {
log.error("Error processing webhook", { error });
const message = error instanceof Error ? error.message : "Unknown error";
return NextResponse.json({ message }, { status: 500 });
}
}
export const POST = defaultResponderForAppDir(postHandler);
Regular → Executable
+2 -2
View File
@@ -23,9 +23,9 @@ try {
"*/5 * * * * *",
async function () {
await Promise.allSettled([
fetchCron("/calendar-cache/cron"),
fetchCron("/cron/calendar-subscriptions"),
// fetchCron("/calendar-cache/cron"),
// fetchCron("/cron/calVideoNoShowWebhookTriggers"),
//
fetchCron("/tasks/cron"),
]);
},
@@ -5,13 +5,13 @@ import {
getOrganizationSettings,
getVerifiedDomain,
} from "@calcom/features/ee/organizations/lib/orgSettings";
import { getTeamWithMembers } from "@calcom/features/ee/teams/lib/queries";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { IS_CALCOM } from "@calcom/lib/constants";
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
import logger from "@calcom/lib/logger";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { getTeamWithMembers } from "@calcom/features/ee/teams/lib/queries";
import slugify from "@calcom/lib/slugify";
import { stripMarkdown } from "@calcom/lib/stripMarkdown";
import prisma from "@calcom/prisma";
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "5.7.5",
"version": "5.7.7",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -138,7 +138,7 @@
"react-select": "^5.7.0",
"react-timezone-select": "^1.4.0",
"react-turnstile": "^1.1.3",
"react-use-intercom": "1.5.1",
"react-use-intercom": "^5.5.0",
"recoil": "^0.7.7",
"remove-markdown": "^0.5.0",
"retell-client-js-sdk": "^2.0.0",
+1 -1
View File
@@ -12,7 +12,7 @@ async function handler(req: NextApiRequest & { userId?: number }) {
const userIp = getIP(req);
await checkRateLimitAndThrowError({
rateLimitingType: "core",
rateLimitingType: "instantMeeting",
identifier: `instant.event-${piiHasher.hash(userIp)}`,
});
@@ -6,7 +6,7 @@ import stripe from "@calcom/features/ee/payments/server/stripe";
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";
import { prisma } from "@calcom/prisma";
export const config = {
api: {
@@ -27,7 +27,10 @@ const handleSubscriptionUpdate = async (event: Stripe.Event) => {
});
if (!app) {
throw new HttpCode({ statusCode: 202, message: "Received and discarded" });
throw new HttpCode({
statusCode: 202,
message: `No credential found with subscription ID ${subscription.id}`,
});
}
await prisma.credential.update({
@@ -51,7 +54,10 @@ const handleSubscriptionDeleted = async (event: Stripe.Event) => {
});
if (!app) {
throw new HttpCode({ statusCode: 202, message: "Received and discarded" });
throw new HttpCode({
statusCode: 202,
message: `No credential found with subscription ID ${subscription.id}`,
});
}
// should we delete the credential here rather than marking as inactive?
@@ -103,7 +109,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
} catch (_err) {
const err = getErrorFromUnknown(_err);
console.error(`Webhook Error: ${err.message}`);
if (!err.message.includes("No credential found with subscription ID")) {
console.error(`Webhook Error: ${err.message}`);
}
res.status(err.statusCode ?? 500).send({
message: err.message,
stack: IS_PRODUCTION ? undefined : err.stack,
+1 -2
View File
@@ -15,7 +15,6 @@ export async function expectInvitationEmailToBeReceived(
) {
if (!emails) return null;
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(2000);
const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail });
expect(receivedEmails?.total).toBe(1);
@@ -36,5 +35,5 @@ export async function expectExistingUserToBeInvitedToOrganization(
userEmail: string,
subject?: string | null
) {
return expectInvitationEmailToBeReceived(page, emails, userEmail, subject, "settings/team");
return expectInvitationEmailToBeReceived(page, emails, userEmail, subject, "teams?token");
}
+126 -3
View File
@@ -47,8 +47,7 @@ test.describe("Team", () => {
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
).toHaveCount(1);
// eslint-disable-next-line playwright/no-conditional-in-test
if (!inviteLink) return null;
expect(inviteLink).toBeTruthy();
// Follow invite link to new window
const context = await browser.newContext();
@@ -103,7 +102,7 @@ test.describe("Team", () => {
});
});
test("Invitation (verified)", async ({ browser, page, users, emails }) => {
test("Invitation (verified)", async ({ page, users, emails }) => {
const t = await localize("en");
const teamOwner = await users.create({ name: `team-owner-${Date.now()}` }, { hasTeam: true });
const { team } = await teamOwner.getFirstTeamMembership();
@@ -192,4 +191,128 @@ test.describe("Team", () => {
});
expect(parentEventType?.children.find((et) => et.userId === invitedMember.id)).toBeTruthy();
});
test("Auto-accept invitation for existing user", async ({ browser, page, users, emails }) => {
const t = await localize("en");
const teamOwner = await users.create({ name: "Invited User" }, { hasTeam: true });
const { team } = await teamOwner.getFirstTeamMembership();
const invitedUser = await users.create({
email: `invited-user-${Date.now()}@example.com`,
name: "Invited User",
});
await teamOwner.apiLogin();
await page.goto(`/settings/teams/${team.id}/members`);
let inviteLink: string;
await test.step("Send invitation to existing user", async () => {
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(500);
await page.getByTestId("new-member-button").click();
await page.locator('input[name="inviteUser"]').fill(invitedUser.email);
await page.getByText(t("send_invite")).click();
inviteLink = await expectInvitationEmailToBeReceived(
page,
emails,
invitedUser.email,
`${teamOwner.name} invited you to join the team ${team.name} on Cal.com`,
"teams?token"
);
expect(inviteLink).toContain("autoAccept=true");
const membership = await prisma.membership.findFirst({
where: {
userId: invitedUser.id,
teamId: team.id,
},
});
expect(membership?.accepted).toBe(false);
});
await test.step("Auto-accept invitation by clicking link", async () => {
const [secondContext, secondPage] = await invitedUser.apiLoginOnNewBrowser(browser);
await secondPage.goto(inviteLink);
await expect(secondPage.getByText("Successfully joined")).toBeVisible();
const membership = await prisma.membership.findFirst({
where: {
userId: invitedUser.id,
teamId: team.id,
},
});
expect(membership?.accepted).toBe(true);
await secondPage.close();
await secondContext.close();
});
});
test("Error when wrong user tries to use invitation link", async ({ browser, page, users, emails }) => {
const t = await localize("en");
const teamOwner = await users.create({ name: "Wrong User" }, { hasTeam: true });
const { team } = await teamOwner.getFirstTeamMembership();
const invitedUser = await users.create({
email: `invited-user-${Date.now()}@example.com`,
name: "Invited User",
});
const wrongUser = await users.create({
email: `wrong-user-${Date.now()}@example.com`,
name: "Wrong User",
});
await teamOwner.apiLogin();
await page.goto(`/settings/teams/${team.id}/members`);
let inviteLink: string;
await test.step("Send invitation to specific user", async () => {
await page.waitForLoadState("domcontentloaded");
await page.waitForTimeout(500);
await page.getByTestId("new-member-button").click();
await page.locator('input[name="inviteUser"]').fill(invitedUser.email);
await page.getByText(t("send_invite")).click();
inviteLink = await expectInvitationEmailToBeReceived(
page,
emails,
invitedUser.email,
`${teamOwner.name} invited you to join the team ${team.name} on Cal.com`,
"teams?token"
);
expect(inviteLink).toContain("autoAccept=true");
});
await test.step("Wrong user tries to use invitation link", async () => {
const [secondContext, secondPage] = await wrongUser.apiLoginOnNewBrowser(browser);
await secondPage.goto(inviteLink);
await expect(secondPage.getByText("This invitation is not for your account")).toBeVisible();
const membership = await prisma.membership.findFirst({
where: {
userId: wrongUser.id,
teamId: team.id,
},
});
expect(membership).toBeNull();
const invitedMembership = await prisma.membership.findFirst({
where: {
userId: invitedUser.id,
teamId: team.id,
},
});
expect(invitedMembership?.accepted).toBe(false);
await secondPage.close();
await secondContext.close();
});
});
});
+21 -3
View File
@@ -321,6 +321,7 @@
"secret_meeting": "اجتماع سري",
"login_instead": "تسجيل الدخول بدلًا من ذلك",
"create_account": "إنشاء حساب",
"get_started": "ابدأ الآن",
"confirm_password": "تأكيد كلمة المرور",
"reset_your_password": "عيّن كلمة المرور الجديدة متبعاً الإرشادات المرسلة إلى عنوان بريدك الإلكتروني.",
"org_banner_instructions": "الرجاء رفع صورة بعرض {{width}} وطول {{height}}.",
@@ -1019,6 +1020,7 @@
"attendees": "الحضور",
"add_attendees": "إضافة الحضور",
"show_advanced_settings": "عرض الإعدادات المتقدمة",
"hide_advanced_settings": "إخفاء الإعدادات المتقدمة",
"event_name": "اسم الحدث",
"event_name_in_calendar": "اسم الحدث في التقويم",
"event_name_tooltip": "الاسم الذي سيظهر في التقاويم",
@@ -1231,7 +1233,7 @@
"contact_sales": "تواصل مع قسم المبيعات",
"error_404": "خطأ 404",
"default": "الافتراضي",
"set_to_default": "تعيين إلى الافتراضي",
"set_to_default": "تعيين كافتراضي",
"new_schedule_btn": "جدول جديد",
"add_new_schedule": "إضافة جدول جديد",
"add_new_calendar": "إضافة تقويم جديد",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "لن يتمكن أعضاء فريقك من رؤية أعضاء الفريق الآخرين عند تشغيل هذا الإعداد.",
"you_cannot_see_team_members": "لا يمكنك رؤية جميع أعضاء الفريق السري.",
"you_cannot_see_teams_of_org": "لا يمكنك رؤية فرق المؤسسة الخاصة.",
"allow_booker_to_select_duration": "السماح لمن يقوم بالحجز بتحديد المدة",
"allow_multiple_durations": "السماح بمدد متعددة",
"impersonate_user_tip": "يتم مراجعة جميع استخدامات هذه الميزة.",
"impersonating_user_warning": "انتحال اسم مستخدم \"{{user}}\".",
"impersonating_stop_instructions": "انقر هنا للإيقاف",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "فشل تأكيد الحجز",
"not_enough_seats": "لا توجد مقاعد كافية",
"form_builder_field_already_exists": "يوجد حقل بهذا الاسم بالفعل",
"guests_field_must_be_multiemail": "يجب أن يكون حقل الضيوف من نوع 'بريد إلكتروني متعدد'",
"show_on_booking_page": "إظهار في صفحة الحجز",
"visit_cancelled_booking": "يمكنك زيارة صفحة الحجز الملغى",
"get_started_zapier_templates": "البدء في استخدام قوالب Zapier",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "مقفل من قبل المشرف",
"app_not_connected": "أنت غير متصل بحساب {{appName}}.",
"connect_now": "اتصل الآن",
"connect_now_unavailable_tooltip": "الاتصال الآن غير متاح حاليًا. يرجى الحجز من الفترات المتاحة أعلاه أو المحاولة مرة أخرى لاحقًا.",
"managed_event_dialog_confirm_button_one": "الاستبدال وإخطار {{count}} عضو",
"managed_event_dialog_confirm_button_other": "الاستبدال وإخطار {{count}} من الأعضاء",
"managed_event_dialog_title_one": "رابط /{{slug}} موجود بالفعل لـ {{count}} عضو. هل تريد استبداله؟",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "بالمتابعة، فإنك توافق على",
"terms": "الشروط",
"always_show_x_days": "دائمًا {{x}} أيام متاحة",
"always_show_x_days_description": "ينقل تلقائيًا نافذة الحجز لمدة {{x}} يوم إلى الأمام كل يوم، مما يضمن أن تقويمك يعرض دائمًا الفترات المتاحة للـ {{x}} يومًا القادمة.",
"team_subscription_cancelled_successfully": "تم إلغاء اشتراك الفريق بنجاح",
"team_subscription_cancellation_error": "حدث خطأ أثناء إلغاء اشتراك الفريق",
"unable_to_subscribe_to_the_platform": "حدث خطأ أثناء محاولة الاشتراك في خطة المنصة، يرجى المحاولة مرة أخرى لاحقًا",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "ينتهي بـ",
"filter_operator_is_empty": "فارغ",
"filter_operator_not_empty": "غير فارغ",
"translate_description_button": "ترجمة العنوان/الوصف إلى لغة متصفح الزائر باستخدام الذكاء الاصطناعي",
"translate_description_button": "ترجمة العنوان والوصف تلقائيًا",
"translate_description_button_description": "ترجمة العناوين والأوصاف تلقائيًا إلى لغة متصفح الزائر باستخدام الذكاء الاصطناعي.",
"rr_distribution_method": "التوزيع",
"rr_distribution_method_description": "يتيح تحسين التوزيع لتحقيق أقصى قدر من التوفر أو لتحقيق توزيع أكثر توازناً.",
"rr_distribution_method_availability_title": "تعظيم التوفر",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "ميزة المقاعد لا تدعم الفعاليات المتكررة. قم بتعطيل ميزة المقاعد لجعلها متكررة.",
"recurring_event_seats_error": "الفعالية المتكررة لا تدعم ميزة المقاعد. قم بتعطيل ميزة المقاعد أو اجعل الفعالية غير متكررة.",
"segment": "شريحة",
"saved_filters": "الفلاتر المحفوظة",
"rename": "إعادة تسمية",
"rename_segment": "إعادة تسمية الشريحة",
"delete_segment": "حذف الشريحة",
@@ -3338,6 +3345,7 @@
"picklist": "قائمة الاختيار",
"most_cancelled_bookings": "الأكثر إلغاءً للحجوزات",
"interface_language": "لغة الواجهة",
"interface_language_description": "اختر لغتك المفضلة لواجهة الحجز",
"visitors_browser_language": "لغة متصفح الزائر",
"name_or_email": "الاسم أو البريد الإلكتروني",
"salesforce_round_robin_skip_fallback_to_lead_owner": "إذا لم يتم العثور على جهة اتصال، يتم الرجوع إلى مالك العميل المحتمل إذا كان موجودًا",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "تعديل وتغيير إعدادات سير العمل",
"pbac_desc_delete_workflows": "إزالة سير العمل من النظام",
"pbac_resource_webhook": "ويب هوك",
"pbac_resource_availability": "الأوقات المتاحة",
"pbac_resource_out_of_office": "خارج المكتب",
"pbac_desc_create_webhooks": "إنشاء ويب هوكس",
"pbac_desc_view_webhooks": "عرض ويب هوكس",
"pbac_desc_update_webhooks": "تحديث الويب هوك",
"pbac_desc_delete_webhooks": "حذف الويب هوك",
"pbac_desc_create_availability": "إنشاء وقت متاح",
"pbac_desc_view_availability": "عرض الأوقات المتاحة",
"pbac_desc_update_availability": "تحديث الأوقات المتاحة",
"pbac_desc_delete_availability": "حذف الأوقات المتاحة",
"pbac_desc_create_out_of_office": "إنشاء حالة خارج المكتب",
"pbac_desc_view_out_of_office": "عرض حالة خارج المكتب",
"pbac_desc_update_out_of_office": "تحديث حالة خارج المكتب",
"pbac_desc_delete_out_of_office": "حذف حالة خارج المكتب",
"pbac_desc_manage_workflows": "وصول كامل لإدارة جميع سير العمل",
"pbac_desc_create_event_types": "إنشاء أنواع الأحداث",
"pbac_desc_view_event_types": "عرض أنواع الأحداث",
+23 -5
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Gizli Görüş",
"login_instead": "Əvəzinə daxil olun",
"create_account": "Hesab yaradın",
"get_started": "Başlayın",
"confirm_password": "Şifrəni təsdiqləyin",
"reset_your_password": "E-poçt ünvanınıza göndərilən təlimatlarla yeni şifrənizi təyin edin.",
"org_banner_instructions": "Zəhmət olmasa {{width}} enində və {{height}} hündürlüyündə şəkil yükləyin.",
@@ -958,7 +959,7 @@
"light_event_type_color": "Tədbir Tipi Rəngi (İşıq Mövzusu)",
"dark_event_type_color": "Tədbir Tipi Rəngi (Qaranlıq Mövzu)",
"file_not_named": "Fayl adlandırılmayıb [idOrSlug]/[user]",
"create_team": "Komanda Yarat",
"create_team": "Komanda yaradın",
"name": "Ad",
"nameless_team": "Adsız Komanda",
"oauth_clients": "OAuth Müştəriləri",
@@ -1019,6 +1020,7 @@
"attendees": "İştirakçılar",
"add_attendees": "İştirakçı əlavə et",
"show_advanced_settings": "Ətraflı parametrləri göstər",
"hide_advanced_settings": "Ətraflı parametrləri gizlədin",
"event_name": "Tədbirin Adı",
"event_name_in_calendar": "Tədbirin adı ilə",
"event_name_tooltip": "Təqvimlərdə görünəcək ad",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Bu aktiv olduqda komanda üzvləri digər komanda üzvlərini görə bilməyəcək.",
"you_cannot_see_team_members": "Gizli komandanın bütün üzvlərini görə bilməzsiniz.",
"you_cannot_see_teams_of_org": "Gizli təşkilatın komandalarını görə bilməzsiniz.",
"allow_booker_to_select_duration": "Rezervasiya edənə müddəti seçməyə icazə ver",
"allow_multiple_durations": "Çoxsaylı müddətlərə icazə verin",
"impersonate_user_tip": "Bu xüsusiyyətin bütün istifadələri auditi olunur.",
"impersonating_user_warning": "\"{{user}}\" istifadəçi adını təqlid edirsiniz.",
"impersonating_stop_instructions": "Dayandırmaq üçün bura klikləyin",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Rezervasiya təsdiqi uğursuz oldu",
"not_enough_seats": "Kifayət qədər oturacaq yoxdur",
"form_builder_field_already_exists": "Bu adla bir sahə artıq mövcuddur",
"guests_field_must_be_multiemail": "Qonaqlar sahəsi 'Çoxsaylı e-poçtlar' tipində olmalıdır",
"show_on_booking_page": "Rezervasiya səhifəsində göstər",
"visit_cancelled_booking": "Ləğv edilmiş rezervasiya səhifəsinə baxa bilərsiniz",
"get_started_zapier_templates": "Zapier şablonları ilə başlayın",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Komanda admini tərəfindən kilidlənib",
"app_not_connected": "Siz {{appName}} hesabını bağlamamısınız.",
"connect_now": "İndi bağlayın",
"connect_now_unavailable_tooltip": "İndi qoşulmaq hazırda əlçatan deyil. Zəhmət olmasa yuxarıdakı slotlardan rezervasiya edin və ya sonra yenidən cəhd edin.",
"managed_event_dialog_confirm_button_one": "Əvəz et və {{count}} üzvü xəbərdar et",
"managed_event_dialog_confirm_button_other": "Əvəz et və {{count}} üzvləri xəbərdar et",
"managed_event_dialog_title_one": "/{{slug}} url-si artıq {{count}} üzv üçün mövcuddur. Onu əvəz etmək istəyirsiniz?",
@@ -2688,7 +2692,7 @@
"enterprise": "Müəssisə",
"enterprise_description": "Təşkilatınızı yaratmaq üçün Enterprise-ə yüksəldin",
"create_your_org": "Təşkilatınızı Yaradın",
"create_org": "Təşkilat Yarat",
"create_org": "Təşkilat yaradın",
"create_your_org_description": "Təşkilatlara yüksəlin və subdomen, birləşdirilmiş ödəniş, Göstəricilər, geniş ağ etiketləmə və daha çoxunu əldə edin",
"create_your_enterprise_description": "Enterprise-ə yüksəlin və Active Directory Sync, SCIM Avtomatik İstifadəçi təminatı, Cal.ai Səs Agentləri, Admin API-ləri və daha çoxuna giriş əldə edin!",
"other_payment_app_enabled": "Hər tədbir növü üçün yalnız bir ödəniş tətbiqini aktivləşdirə bilərsiniz",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Davam etməklə, bizim",
"terms": "Şərtlər",
"always_show_x_days": "Həmişə {{x}} gün mövcuddur",
"always_show_x_days_description": "Avtomatik olaraq {{x}}-günlük rezervasiya pəncərəsini hər gün irəli aparır, təqviminizin həmişə növbəti {{x}} günün əlçatan slotlarını göstərməsini təmin edir.",
"team_subscription_cancelled_successfully": "Komanda abunəliyi uğurla ləğv edildi",
"team_subscription_cancellation_error": "Komanda abunəliyini ləğv edərkən xəta baş verdi",
"unable_to_subscribe_to_the_platform": "Platforma planına abunə olmağa çalışarkən bir səhv baş verdi, zəhmət olmasa bir az sonra yenidən cəhd edin",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "İlə bitir",
"filter_operator_is_empty": "Boşdur",
"filter_operator_not_empty": "Boş deyil",
"translate_description_button": "Süni intellekt vasitəsilə başlıq/təsviri ziyarətçinin brauzer dilinə tərcümə et",
"translate_description_button": "Başlıq və təsviri auto tərcümə et",
"translate_description_button_description": "Başlıqları və təsvirləri ziyarətçinin brauzer dilinə AI vasitəsilə avtomatik tərcümə edin.",
"rr_distribution_method": "Paylama",
"rr_distribution_method_description": "Maksimum əlçatanlıq üçün paylamanı optimallaşdırmağa və ya daha balanslı bir təyinat üçün çalışmağa imkan verir.",
"rr_distribution_method_availability_title": "Əlçatanlığı maksimuma çatdırmaq",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "Yerlər funksiyası təkrarlanan tədbiri dəstəkləmir. Təkrarlanma funksiyasını aktivləşdirmək üçün yerlər funksiyasını deaktiv edin.",
"recurring_event_seats_error": "Təkrarlanan tədbir yerlər funksiyasını dəstəkləmir. Yerlər funksiyasını deaktiv edin və ya tədbiri təkrarlanmayan edin.",
"segment": "Seqment",
"saved_filters": "Saxlanılmış filtrlər",
"rename": "Adını dəyişdir",
"rename_segment": "Seqmentin adını dəyişdir",
"delete_segment": "Seqmenti sil",
@@ -3338,6 +3345,7 @@
"picklist": "Seçim siyahısı",
"most_cancelled_bookings": "Ən Çox Ləğv Edilən",
"interface_language": "İnterfeys dili",
"interface_language_description": "Rezervasiya interfeysi üçün üstünlük verdiyiniz dili təyin edin",
"visitors_browser_language": "Ziyarətçinin brauzer dili",
"name_or_email": "Ad və ya E-poçt",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Əgər əlaqə tapılmazsa, mövcud olduğu təqdirdə potensial müştəri sahibinə qayıt",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "İş axını parametrlərini redaktə etmək və dəyişdirmək",
"pbac_desc_delete_workflows": "İş axınlarını sistemdən silmək",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Mövcudluq",
"pbac_resource_out_of_office": "Ofisdən kənarda",
"pbac_desc_create_webhooks": "Webhook yaradın",
"pbac_desc_view_webhooks": "Webhook-lara baxın",
"pbac_desc_update_webhooks": "Webhook-ləri yeniləyin",
"pbac_desc_delete_webhooks": "Webhook-ləri silin",
"pbac_desc_create_availability": "Mövcudluq yaratmaq",
"pbac_desc_view_availability": "Mövcudluğa baxmaq",
"pbac_desc_update_availability": "Mövcudluğu yeniləmək",
"pbac_desc_delete_availability": "Mövcudluğu silmək",
"pbac_desc_create_out_of_office": "Ofisdən kənar yaratmaq",
"pbac_desc_view_out_of_office": "Ofisdən kənara baxmaq",
"pbac_desc_update_out_of_office": "Ofisdən kənarı yeniləmək",
"pbac_desc_delete_out_of_office": "Ofisdən kənarı silmək",
"pbac_desc_manage_workflows": "Bütün iş axınlarına tam idarəetmə girişi",
"pbac_desc_create_event_types": "Tədbir növləri yaratmaq",
"pbac_desc_view_event_types": "Tədbir növlərinə baxmaq",
@@ -3561,7 +3579,7 @@
"round_robin_settings_description": "Bu komanda üçün standart növbəli təqvim parametrlərini fərdiləşdirin",
"round_robin_settings_updated_successfully": "Növbəli təqvim parametrləri uğurla yeniləndi",
"booking_creation_time": "Rezervasiya yaradılma vaxtı",
"meeting_start_time": "Görüş başlama vaxtı",
"meeting_start_time": "Görüşün başlama vaxtı",
"members_affected_by_disabling_delegation_credential": "Təsir edilən üzvlər",
"no_members_affected_by_disabling_delegation_credential": "Nümayəndəlik etimadnaməsinin deaktiv edilməsindən təsirlənən üzv yoxdur",
"download_expense_log": "Xərc jurnalını yüklə",
+22 -4
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Тайна среща",
"login_instead": "Вместо това влезте",
"create_account": "Създаване на акаунт",
"get_started": "Започнете",
"confirm_password": "Потвърдете паролата",
"reset_your_password": "Задайте новата си парола с инструкциите, изпратени на вашия имейл адрес.",
"org_banner_instructions": "Моля, качете изображение с ширина {{width}} и височина {{height}}.",
@@ -958,7 +959,7 @@
"light_event_type_color": "Цвят на типа събитие (светла тема)",
"dark_event_type_color": "Цвят на типа събитие (тъмна тема)",
"file_not_named": "Файлът не е именуван [idOrSlug]/[user]",
"create_team": "Създай екип",
"create_team": "Създаване на екип",
"name": "Име",
"nameless_team": "Безименен екип",
"oauth_clients": "OAuth клиенти",
@@ -1019,6 +1020,7 @@
"attendees": "Участници",
"add_attendees": "Добавяне на участници",
"show_advanced_settings": "Показване на разширени настройки",
"hide_advanced_settings": "Скриване на разширените настройки",
"event_name": "Име на събитието",
"event_name_in_calendar": "С име на събитието",
"event_name_tooltip": "Името, което ще се показва в календарите",
@@ -1231,7 +1233,7 @@
"contact_sales": "Свържи се с отдел продажби",
"error_404": "Грешка 404",
"default": "По подразбиране",
"set_to_default": "Задай по подразбиране",
"set_to_default": "Задаване по подразбиране",
"new_schedule_btn": "Нов график",
"add_new_schedule": "Добави нов график",
"add_new_calendar": "Добави нов календар",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Когато това е включено, членовете на екипа няма да могат да виждат другите членове.",
"you_cannot_see_team_members": "Не можеш да видиш всички членове на частния екип.",
"you_cannot_see_teams_of_org": "Не можеш да видиш екипите на частна организация.",
"allow_booker_to_select_duration": "Позволи на резервиращия да избере продължителност",
"allow_multiple_durations": "Разрешаване на множество продължителности",
"impersonate_user_tip": "Всяко използване на тази функция се одитира.",
"impersonating_user_warning": "Представяш се като потребител \"{{user}}\".",
"impersonating_stop_instructions": "Кликни тук, за да спреш",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Потвърждението на резервацията е неуспешно",
"not_enough_seats": "Няма достатъчно места",
"form_builder_field_already_exists": "Поле с това име вече съществува",
"guests_field_must_be_multiemail": "Полето за гости трябва да бъде от тип 'Множество имейли'",
"show_on_booking_page": "Покажи на страницата за резервации",
"visit_cancelled_booking": "Можете да посетите страницата с отменената резервация",
"get_started_zapier_templates": "Започнете със Zapier шаблони",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Заключено от администратора на екипа",
"app_not_connected": "Не сте свързали {{appName}} акаунт.",
"connect_now": "Свържете сега",
"connect_now_unavailable_tooltip": "Свързването сега не е налично. Моля, резервирайте от горните слотове или опитайте отново по-късно.",
"managed_event_dialog_confirm_button_one": "Замени и уведоми {{count}} член",
"managed_event_dialog_confirm_button_other": "Замени и уведоми {{count}} члена",
"managed_event_dialog_title_one": "URL адресът /{{slug}} вече съществува за {{count}} член. Искате ли да го замените?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "С продължаването си, вие се съгласявате с нашите",
"terms": "Условия",
"always_show_x_days": "Винаги показвай {{x}} свободни дни",
"always_show_x_days_description": "Автоматично премества {{x}}-дневния прозорец за резервации напред всеки ден, гарантирайки, че вашият календар винаги показва следващите {{x}} дни с налични слотове.",
"team_subscription_cancelled_successfully": "Абонаментът на екипа е отменен успешно",
"team_subscription_cancellation_error": "Грешка при отмяна на абонамента на екипа",
"unable_to_subscribe_to_the_platform": "Възникна грешка при опит за абониране за плана на платформата, моля опитайте отново по-късно",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Завършва с",
"filter_operator_is_empty": "Е празно",
"filter_operator_not_empty": "Не е празно",
"translate_description_button": "Преведи заглавието/описанието на езика на браузъра на посетителя чрез AI",
"translate_description_button": "Автоматичен превод на заглавие и описание",
"translate_description_button_description": "Автоматично превеждане на заглавия и описания на езика на браузъра на посетителя с помощта на изкуствен интелект.",
"rr_distribution_method": "Разпределение",
"rr_distribution_method_description": "Позволява оптимизиране на разпределението за максимална наличност или за по-балансирано разпределение.",
"rr_distribution_method_availability_title": "Максимизиране на наличността",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "Функцията за места не поддържа повтарящи се събития. Деактивирайте функцията за места, за да го направите повтарящо се.",
"recurring_event_seats_error": "Повтарящото се събитие не поддържа функцията за места. Деактивирайте функцията за места или направете събитието неповтарящо се.",
"segment": "Сегмент",
"saved_filters": "Запазени филтри",
"rename": "Преименуване",
"rename_segment": "Преименуване на сегмент",
"delete_segment": "Изтриване на сегмент",
@@ -3338,6 +3345,7 @@
"picklist": "Списък за избор",
"most_cancelled_bookings": "Най-много отменени",
"interface_language": "Език на интерфейса",
"interface_language_description": "Задайте предпочитания от вас език за интерфейса за резервации",
"visitors_browser_language": "Език на браузъра на посетителя",
"name_or_email": "Име или имейл",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Ако не е намерен контакт, връщане към собственика на лийда, ако съществува",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Редактиране и модифициране на настройките на работните процеси",
"pbac_desc_delete_workflows": "Премахване на работни процеси от системата",
"pbac_resource_webhook": "Уебхук",
"pbac_resource_availability": "Наличност",
"pbac_resource_out_of_office": "Извън офиса",
"pbac_desc_create_webhooks": "Създаване на уебхуци",
"pbac_desc_view_webhooks": "Преглед на уебхуци",
"pbac_desc_update_webhooks": "Актуализиране на уебхукове",
"pbac_desc_delete_webhooks": "Изтриване на уебхукове",
"pbac_desc_create_availability": "Създаване на наличност",
"pbac_desc_view_availability": "Преглед на наличност",
"pbac_desc_update_availability": "Актуализиране на наличност",
"pbac_desc_delete_availability": "Изтриване на наличност",
"pbac_desc_create_out_of_office": "Създаване на статус \"Извън офиса\"",
"pbac_desc_view_out_of_office": "Преглед на статус \"Извън офиса\"",
"pbac_desc_update_out_of_office": "Актуализиране на статус \"Извън офиса\"",
"pbac_desc_delete_out_of_office": "Изтриване на статус \"Извън офиса\"",
"pbac_desc_manage_workflows": "Пълен достъп за управление на всички работни процеси",
"pbac_desc_create_event_types": "Създаване на типове събития",
"pbac_desc_view_event_types": "Преглед на типове събития",
+22 -4
View File
@@ -321,6 +321,7 @@
"secret_meeting": "গোপন সভা",
"login_instead": "পরিবর্তে লগইন",
"create_account": "অ্যাকাউন্ট তৈরি করুন",
"get_started": "শুরু করুন",
"confirm_password": "পাসওয়ার্ড নিশ্চিত করুন",
"reset_your_password": "আপনার ইমেল ঠিকানায় প্রেরিত নির্দেশাবলী সহ আপনার নতুন পাসওয়ার্ড সেট করুন।",
"org_banner_instructions": "এর চিত্র আপলোড করুন {{width}} প্রস্থ এবং {{height}} উচ্চতা।",
@@ -1019,6 +1020,7 @@
"attendees": "অংশগ্রহণকারীরা",
"add_attendees": "অংশগ্রহণকারীদের যোগ করুন",
"show_advanced_settings": "উন্নত সেটিংস দেখান",
"hide_advanced_settings": "উন্নত সেটিংস লুকান",
"event_name": "ইভেন্টের নাম",
"event_name_in_calendar": "ইভেন্টের নাম সহ",
"event_name_tooltip": "নাম যে ক্যালেন্ডারে প্রদর্শিত হবে",
@@ -1231,7 +1233,7 @@
"contact_sales": "যোগাযোগ বিক্রয়",
"error_404": "ত্রুটি 404",
"default": "ডিফল্ট",
"set_to_default": "ডিফল্ট সেট",
"set_to_default": "ডিফল্ট হিসাবে সেট করুন",
"new_schedule_btn": "নতুন সময়সূচী",
"add_new_schedule": "একটি নতুন সময়সূচি যুক্ত করুন",
"add_new_calendar": "একটি নতুন ক্যালেন্ডার যুক্ত করুন",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "আপনার দলের সদস্যরা এটি চালু থাকলে অন্য দলের সদস্যদের দেখতে সক্ষম হবেন না।",
"you_cannot_see_team_members": "আপনি কোনও বেসরকারী দলের সমস্ত দলের সদস্য দেখতে পারবেন না।",
"you_cannot_see_teams_of_org": "আপনি কোনও বেসরকারী সংস্থার দল দেখতে পাচ্ছেন না।",
"allow_booker_to_select_duration": "বুকারকে সময়কাল নির্বাচন করার অনুমতি দিন",
"allow_multiple_durations": "একাধিক সময়কাল অনুমতি দিন",
"impersonate_user_tip": "এই বৈশিষ্ট্যের সমস্ত ব্যবহার নিরীক্ষণ করা হয়।",
"impersonating_user_warning": "ব্যবহারকারীর নাম \"{{user}}\"।",
"impersonating_stop_instructions": "থামাতে এখানে ক্লিক করুন",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "বুকিং নিশ্চিতকরণ ব্যর্থ হয়েছে",
"not_enough_seats": "পর্যাপ্ত আসন নেই",
"form_builder_field_already_exists": "এই নাম সহ একটি ক্ষেত্র ইতিমধ্যে বিদ্যমান",
"guests_field_must_be_multiemail": "অতিথি ক্ষেত্রটি অবশ্যই 'একাধিক ইমেইল' ধরনের হতে হবে",
"show_on_booking_page": "বুকিং পৃষ্ঠায় দেখান",
"visit_cancelled_booking": "আপনি বাতিল হওয়া বুকিং পৃষ্ঠাটি দেখতে পারেন",
"get_started_zapier_templates": "জ্যাপিয়ার টেম্পলেটগুলি দিয়ে শুরু করুন",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "টিম অ্যাডমিন দ্বারা লক",
"app_not_connected": "আপনি সংযুক্ত না একটি {{appName}} অ্যাকাউন্ট।",
"connect_now": "এখনই সংযুক্ত করুন",
"connect_now_unavailable_tooltip": "এখন সংযোগ করা বর্তমানে অনুপলব্ধ। অনুগ্রহ করে উপরের স্লট থেকে বুক করুন অথবা পরে আবার চেষ্টা করুন।",
"managed_event_dialog_confirm_button_one": "প্রতিস্থাপন এবং অবহিত {{count}} সদস্য",
"managed_event_dialog_confirm_button_other": "প্রতিস্থাপন এবং অবহিত {{count}} সদস্য",
"managed_event_dialog_title_one": "ইউআরএল /{{slug}} ইতিমধ্যে বিদ্যমান {{count}} সদস্য ",
@@ -2688,7 +2692,7 @@
"enterprise": "এন্টারপ্রাইজ",
"enterprise_description": "আপনার সংস্থা তৈরি করতে এন্টারপ্রাইজে আপগ্রেড করুন",
"create_your_org": "আপনার সংস্থা তৈরি করুন",
"create_org": "সংস্থা তৈরি করুন",
"create_org": "সংগঠন তৈরি করুন",
"create_your_org_description": "সংস্থাগুলিতে আপগ্রেড করুন এবং একটি সাবডোমেন, ইউনিফাইড বিলিং, অন্তর্দৃষ্টি, বিস্তৃত হোয়াইটেলবেলিং এবং আরও অনেক কিছু পান",
"create_your_enterprise_description": "এন্টারপ্রাইজে আপগ্রেড করুন এবং অ্যাক্টিভ ডিরেক্টরি সিঙ্ক, এসসিআইএম স্বয়ংক্রিয় ব্যবহারকারীর প্রভিশনিং, ক্যাল.এই ভয়েস এজেন্টস, অ্যাডমিন এপিআই এবং আরও অনেক কিছুতে অ্যাক্সেস পান!",
"other_payment_app_enabled": "আপনি প্রতি ইভেন্টের জন্য কেবল একটি পেমেন্ট অ্যাপ্লিকেশন সক্ষম করতে পারেন",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "এগিয়ে যাওয়ার মাধ্যমে, আপনি আমাদের নিম্নলিখিত শর্তাবলীতে সম্মত হচ্ছেন",
"terms": "শর্তাবলী",
"always_show_x_days": "সর্বদা {{x}} দিন উপলব্ধ",
"always_show_x_days_description": "প্রতিদিন স্বয়ংক্রিয়ভাবে {{x}}-দিনের বুকিং উইন্ডো সামনে সরিয়ে দেয়, যা নিশ্চিত করে যে আপনার ক্যালেন্ডার সবসময় পরবর্তী {{x}} দিনের উপলব্ধ স্লট দেখায়।",
"team_subscription_cancelled_successfully": "দলের সাবস্ক্রিপশন সফলভাবে বাতিল করা হয়েছে",
"team_subscription_cancellation_error": "দলের সাবস্ক্রিপশন বাতিল করার সময় ত্রুটি",
"unable_to_subscribe_to_the_platform": "প্ল্যাটফর্ম পরিকল্পনায় সাবস্ক্রাইব করার চেষ্টা করার সময় একটি ত্রুটি ঘটেছে, দয়া করে পরে আবার চেষ্টা করুন",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "সাথে শেষ",
"filter_operator_is_empty": "খালি আছে",
"filter_operator_not_empty": "খালি নয়",
"translate_description_button": "এআই ব্যবহার করে দর্শনার্থীর ব্রাউজার ভাষায় বর্ণনা অনুবাদ করুন",
"translate_description_button": "শিরোনাম এবং বিবরণ অটো অনুবাদ করুন",
"translate_description_button_description": "AI ব্যবহার করে দর্শকের ব্রাউজার ভাষায় শিরোনাম এবং বিবরণ স্বয়ংক্রিয়ভাবে অনুবাদ করুন।",
"rr_distribution_method": "বিতরণ",
"rr_distribution_method_description": "সর্বাধিক প্রাপ্যতার জন্য বিতরণকে অনুকূলকরণের জন্য বা আরও সুষম অ্যাসাইনমেন্টের জন্য লক্ষ্য করার অনুমতি দেয়।",
"rr_distribution_method_availability_title": "প্রাপ্যতা সর্বাধিক করুন",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "সিট ফিচার পুনরাবৃত্ত ইভেন্ট সমর্থন করে না। এটিকে পুনরাবৃত্ত করতে সিট ফিচার নিষ্ক্রিয় করুন।",
"recurring_event_seats_error": "পুনরাবৃত্ত ইভেন্ট সিট ফিচার সমর্থন করে না। সিট ফিচার নিষ্ক্রিয় করুন অথবা ইভেন্টটিকে অপুনরাবৃত্ত করুন।",
"segment": "সেগমেন্ট",
"saved_filters": "সংরক্ষিত ফিল্টার",
"rename": "পুনঃনামকরণ",
"rename_segment": "সেগমেন্ট পুনঃনামকরণ",
"delete_segment": "সেগমেন্ট মুছুন",
@@ -3338,6 +3345,7 @@
"picklist": "পিকলিস্ট",
"most_cancelled_bookings": "সবচেয়ে বেশি বাতিল করা বুকিং",
"interface_language": "ইন্টারফেস ভাষা",
"interface_language_description": "বুকিং ইন্টারফেসের জন্য আপনার পছন্দের ভাষা সেট করুন",
"visitors_browser_language": "দর্শনার্থীর ব্রাউজার ভাষা",
"name_or_email": "নাম বা ইমেইল",
"salesforce_round_robin_skip_fallback_to_lead_owner": "যদি কোন কন্টাক্ট না পাওয়া যায়, লিড মালিকে ফিরে যান যদি তা বিদ্যমান থাকে",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "কর্মপ্রবাহ সেটিংস সম্পাদনা ও পরিবর্তন করুন",
"pbac_desc_delete_workflows": "সিস্টেম থেকে কর্মপ্রবাহ সরান",
"pbac_resource_webhook": "ওয়েবহুক",
"pbac_resource_availability": "প্রাপ্যতা",
"pbac_resource_out_of_office": "অফিসের বাইরে",
"pbac_desc_create_webhooks": "ওয়েবহুক তৈরি করুন",
"pbac_desc_view_webhooks": "ওয়েবহুক দেখুন",
"pbac_desc_update_webhooks": "ওয়েবহুক আপডেট করুন",
"pbac_desc_delete_webhooks": "ওয়েবহুক মুছুন",
"pbac_desc_create_availability": "প্রাপ্যতা তৈরি করুন",
"pbac_desc_view_availability": "প্রাপ্যতা দেখুন",
"pbac_desc_update_availability": "প্রাপ্যতা আপডেট করুন",
"pbac_desc_delete_availability": "প্রাপ্যতা মুছুন",
"pbac_desc_create_out_of_office": "অফিসের বাইরে তৈরি করুন",
"pbac_desc_view_out_of_office": "অফিসের বাইরে দেখুন",
"pbac_desc_update_out_of_office": "অফিসের বাইরে আপডেট করুন",
"pbac_desc_delete_out_of_office": "অফিসের বাইরে মুছুন",
"pbac_desc_manage_workflows": "সমস্ত কর্মপ্রবাহের জন্য সম্পূর্ণ ব্যবস্থাপনা অ্যাক্সেস",
"pbac_desc_create_event_types": "ইভেন্টের ধরণ তৈরি করুন",
"pbac_desc_view_event_types": "ইভেন্টের ধরণ দেখুন",
+22 -4
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Reunió secreta",
"login_instead": "Inicia sessió en lloc seu",
"create_account": "Crea un compte",
"get_started": "Comença",
"confirm_password": "Confirma la contrasenya",
"reset_your_password": "Estableix la teva nova contrasenya amb les instruccions enviades al teu correu electrònic.",
"org_banner_instructions": "Si us plau, puja una imatge de {{width}} d'amplada i {{height}} d'alçada.",
@@ -958,7 +959,7 @@
"light_event_type_color": "Color del tipus d'esdeveniment (tema clar)",
"dark_event_type_color": "Color del tipus d'esdeveniment (tema fosc)",
"file_not_named": "L'arxiu no té el nom [idOrSlug]/[user]",
"create_team": "Crea un equip",
"create_team": "Crea equip",
"name": "Nom",
"nameless_team": "Equip sense nom",
"oauth_clients": "Clients OAuth",
@@ -1019,6 +1020,7 @@
"attendees": "Assistents",
"add_attendees": "Afegir assistents",
"show_advanced_settings": "Mostrar configuració avançada",
"hide_advanced_settings": "Amaga la configuració avançada",
"event_name": "Nom de l'esdeveniment",
"event_name_in_calendar": "Amb nom d'esdeveniment",
"event_name_tooltip": "El nom que apareixerà als calendaris",
@@ -1231,7 +1233,7 @@
"contact_sales": "Contacta amb vendes",
"error_404": "Error 404",
"default": "Per defecte",
"set_to_default": "Estableix per defecte",
"set_to_default": "Estableix com a predeterminat",
"new_schedule_btn": "Nou horari",
"add_new_schedule": "Afegeix un nou horari",
"add_new_calendar": "Afegeix un nou calendari",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Els membres del teu equip no podran veure altres membres quan això estigui activat.",
"you_cannot_see_team_members": "No pots veure tots els membres d'un equip privat.",
"you_cannot_see_teams_of_org": "No pots veure els equips d'una organització privada.",
"allow_booker_to_select_duration": "Permetre que el reservant seleccioni la durada",
"allow_multiple_durations": "Permet múltiples duracions",
"impersonate_user_tip": "Tots els usos d'aquesta funcionalitat són auditats.",
"impersonating_user_warning": "Suplantant l'usuari \"{{user}}\".",
"impersonating_stop_instructions": "Fes clic aquí per aturar",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "La confirmació de la reserva ha fallat",
"not_enough_seats": "No hi ha prou places",
"form_builder_field_already_exists": "Ja existeix un camp amb aquest nom",
"guests_field_must_be_multiemail": "El camp de convidats ha de ser del tipus 'Múltiples correus electrònics'",
"show_on_booking_page": "Mostra a la pàgina de reserva",
"visit_cancelled_booking": "Pots visitar la pàgina de reserva cancel·lada",
"get_started_zapier_templates": "Comença amb les plantilles de Zapier",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Bloquejat per l'administrador de l'equip",
"app_not_connected": "No has connectat cap compte de {{appName}}.",
"connect_now": "Connecta ara",
"connect_now_unavailable_tooltip": "Connectar ara no està disponible actualment. Si us plau, reserva des dels espais disponibles anteriors o torna-ho a provar més tard.",
"managed_event_dialog_confirm_button_one": "Reemplaça i notifica {{count}} membre",
"managed_event_dialog_confirm_button_other": "Reemplaça i notifica {{count}} membres",
"managed_event_dialog_title_one": "L'URL /{{slug}} ja existeix per a {{count}} membre. Vols reemplaçar-la?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "En continuar, acceptes les nostres",
"terms": "Condicions",
"always_show_x_days": "Sempre {{x}} dies disponibles",
"always_show_x_days_description": "Mou automàticament la finestra de reserva de {{x}} dies cap endavant cada dia, assegurant que el teu calendari sempre mostri els propers {{x}} dies d'espais disponibles.",
"team_subscription_cancelled_successfully": "Subscripció d'equip cancel·lada amb èxit",
"team_subscription_cancellation_error": "Error en cancel·lar la subscripció d'equip",
"unable_to_subscribe_to_the_platform": "S'ha produït un error en intentar subscriure's al pla de la plataforma, torna-ho a provar més tard",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Acaba amb",
"filter_operator_is_empty": "És buit",
"filter_operator_not_empty": "No és buit",
"translate_description_button": "Tradueix el títol/descripció a l'idioma del navegador del visitant utilitzant IA",
"translate_description_button": "Tradueix automàticament el títol i la descripció",
"translate_description_button_description": "Tradueix automàticament els títols i les descripcions a l'idioma del navegador del visitant utilitzant IA.",
"rr_distribution_method": "Distribució",
"rr_distribution_method_description": "Permet optimitzar la distribució per maximitzar la disponibilitat o aconseguir una assignació més equilibrada",
"rr_distribution_method_availability_title": "Maximitzar disponibilitat",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "La funció de seients no admet esdeveniments recurrents. Desactiveu la funció de seients per fer-lo recurrent.",
"recurring_event_seats_error": "Els esdeveniments recurrents no admeten la funció de seients. Desactiveu la funció de seients o feu que l'esdeveniment no sigui recurrent.",
"segment": "Segment",
"saved_filters": "Filtres desats",
"rename": "Canviar el nom",
"rename_segment": "Canviar el nom del segment",
"delete_segment": "Eliminar segment",
@@ -3338,6 +3345,7 @@
"picklist": "Llista de selecció",
"most_cancelled_bookings": "Més reserves cancel·lades",
"interface_language": "Idioma de la interfície",
"interface_language_description": "Estableix el teu idioma preferit per a la interfície de reserva",
"visitors_browser_language": "Idioma del navegador del visitant",
"name_or_email": "Nom o correu electrònic",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Si no es troba cap contacte, recorre al propietari del lead si existeix",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Editar i modificar la configuració dels fluxos de treball",
"pbac_desc_delete_workflows": "Eliminar fluxos de treball del sistema",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Disponibilitat",
"pbac_resource_out_of_office": "Fora de l'oficina",
"pbac_desc_create_webhooks": "Crea webhooks",
"pbac_desc_view_webhooks": "Visualitza webhooks",
"pbac_desc_update_webhooks": "Actualitza els webhooks",
"pbac_desc_delete_webhooks": "Elimina els webhooks",
"pbac_desc_create_availability": "Crear disponibilitat",
"pbac_desc_view_availability": "Veure disponibilitat",
"pbac_desc_update_availability": "Actualitzar disponibilitat",
"pbac_desc_delete_availability": "Eliminar disponibilitat",
"pbac_desc_create_out_of_office": "Crear fora de l'oficina",
"pbac_desc_view_out_of_office": "Veure fora de l'oficina",
"pbac_desc_update_out_of_office": "Actualitzar fora de l'oficina",
"pbac_desc_delete_out_of_office": "Eliminar fora de l'oficina",
"pbac_desc_manage_workflows": "Accés complet de gestió a tots els fluxos de treball",
"pbac_desc_create_event_types": "Crear tipus d'esdeveniments",
"pbac_desc_view_event_types": "Veure tipus d'esdeveniments",
+20 -2
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Tajná schůzka",
"login_instead": "Namísto toho se přihlásit",
"create_account": "Vytvořit účet",
"get_started": "Začít",
"confirm_password": "Potvrdit heslo",
"reset_your_password": "Nastavte si nové heslo podle pokynů odeslaných na vaši e-mailovou adresu.",
"org_banner_instructions": "Nahrajte prosím obrázek o šířce {{width}} a výšce {{height}}.",
@@ -1019,6 +1020,7 @@
"attendees": "Účastníci",
"add_attendees": "Přidat účastníky",
"show_advanced_settings": "Zobrazit pokročilá nastavení",
"hide_advanced_settings": "Skrýt pokročilá nastavení",
"event_name": "Název události",
"event_name_in_calendar": "Název události v kalendáři",
"event_name_tooltip": "Název, který se zobrazí v kalendářích",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Po zapnutí této funkce neuvidí členové vašeho týmu ostatní členy týmu.",
"you_cannot_see_team_members": "V soukromém týmu nelze vidět všechny členy týmu.",
"you_cannot_see_teams_of_org": "Nemůžete vidět týmy soukromé organizace.",
"allow_booker_to_select_duration": "Umožnit rezervující osobě vybrat dobu trvání",
"allow_multiple_durations": "Povolit více délek trvání",
"impersonate_user_tip": "Všechna použití této funkce jsou kontrolována.",
"impersonating_user_warning": "Zosobnění uživatelského jména „{{user}}“.",
"impersonating_stop_instructions": "Kliknutím sem zastavíte",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Potvrzení rezervace se nezdařilo",
"not_enough_seats": "Nedostatek míst",
"form_builder_field_already_exists": "Pole s tímto názvem již existuje",
"guests_field_must_be_multiemail": "Pole pro hosty musí být typu 'Více e-mailů'",
"show_on_booking_page": "Zobrazit na stránce rezervace",
"visit_cancelled_booking": "Můžete navštívit stránku zrušené rezervace",
"get_started_zapier_templates": "Začněte používat šablony Zapier",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Uzamčeno správcem týmu",
"app_not_connected": "Nemáte připojený účet {{appName}}.",
"connect_now": "Připojte se",
"connect_now_unavailable_tooltip": "Připojit nyní je momentálně nedostupné. Prosím rezervujte si z výše uvedených termínů nebo to zkuste později.",
"managed_event_dialog_confirm_button_one": "Nahradit a upozornit {{count}} člena",
"managed_event_dialog_confirm_button_other": "Nahradit a upozornit členy {{count}}",
"managed_event_dialog_title_one": "Adresa URL /{{slug}} již existuje pro {{count}} člena. Chcete ji nahradit?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Pokračováním souhlasíte s našimi",
"terms": "Podmínkami",
"always_show_x_days": "Vždy dostupné {{x}} dní",
"always_show_x_days_description": "Automaticky posouvá {{x}}-denní rezervační okno každý den vpřed, což zajišťuje, že váš kalendář vždy zobrazuje dostupné termíny na příštích {{x}} dní.",
"team_subscription_cancelled_successfully": "Předplatné týmu bylo úspěšně zrušeno",
"team_subscription_cancellation_error": "Chyba při rušení předplatného týmu",
"unable_to_subscribe_to_the_platform": "Při pokusu o přihlášení k plánu platformy došlo k chybě, zkuste to prosím znovu později",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Končí na",
"filter_operator_is_empty": "Je prázdné",
"filter_operator_not_empty": "Není prázdné",
"translate_description_button": "Přeložit název/popis do jazyka prohlížeče návštěvníka pomocí AI",
"translate_description_button": "Automaticky přeložit název a popis",
"translate_description_button_description": "Automaticky překládá názvy a popisy do jazyka prohlížeče návštěvníka pomocí umělé inteligence.",
"rr_distribution_method": "Distribuce",
"rr_distribution_method_description": "Umožňuje optimalizovat distribuci pro maximální dostupnost nebo pro vyváženější přidělování.",
"rr_distribution_method_availability_title": "Maximalizovat dostupnost",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "Funkce míst nepodporuje opakující se události. Zakažte funkci míst pro vytvoření opakující se události.",
"recurring_event_seats_error": "Opakující se událost nepodporuje funkci míst. Zakažte funkci míst nebo změňte událost na neopakující se.",
"segment": "Segment",
"saved_filters": "Uložené filtry",
"rename": "Přejmenovat",
"rename_segment": "Přejmenovat segment",
"delete_segment": "Smazat segment",
@@ -3338,6 +3345,7 @@
"picklist": "Výběrový seznam",
"most_cancelled_bookings": "Nejvíce zrušených rezervací",
"interface_language": "Jazyk rozhraní",
"interface_language_description": "Nastavte preferovaný jazyk pro rezervační rozhraní",
"visitors_browser_language": "Jazyk prohlížeče návštěvníka",
"name_or_email": "Jméno nebo e-mail",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Pokud není nalezen žádný kontakt, použít vlastníka leadu, pokud existuje",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Upravovat a měnit nastavení pracovních postupů",
"pbac_desc_delete_workflows": "Odstraňovat pracovní postupy ze systému",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Dostupnost",
"pbac_resource_out_of_office": "Mimo kancelář",
"pbac_desc_create_webhooks": "Vytvářet webhooky",
"pbac_desc_view_webhooks": "Zobrazit webhooky",
"pbac_desc_update_webhooks": "Aktualizovat webhooky",
"pbac_desc_delete_webhooks": "Smazat webhooky",
"pbac_desc_create_availability": "Vytvořit dostupnost",
"pbac_desc_view_availability": "Zobrazit dostupnost",
"pbac_desc_update_availability": "Aktualizovat dostupnost",
"pbac_desc_delete_availability": "Smazat dostupnost",
"pbac_desc_create_out_of_office": "Vytvořit mimo kancelář",
"pbac_desc_view_out_of_office": "Zobrazit mimo kancelář",
"pbac_desc_update_out_of_office": "Aktualizovat mimo kancelář",
"pbac_desc_delete_out_of_office": "Smazat mimo kancelář",
"pbac_desc_manage_workflows": "Plný přístup ke správě všech pracovních postupů",
"pbac_desc_create_event_types": "Vytvářet typy událostí",
"pbac_desc_view_event_types": "Zobrazit typy událostí",
+22 -4
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Skjult Møde",
"login_instead": "Log ind i stedet",
"create_account": "Opret Konto",
"get_started": "Kom i gang",
"confirm_password": "Bekræft adgangskode",
"reset_your_password": "Indstil din nye adgangskode med instruktionerne sendt til din e-mailadresse.",
"org_banner_instructions": "Upload venligst et billede med {{width}} bredde og {{height}} højde.",
@@ -958,7 +959,7 @@
"light_event_type_color": "Begivenhedstype farve (Lyst tema)",
"dark_event_type_color": "Begivenhedstype farve (Mørkt tema)",
"file_not_named": "Filen er ikke navngivet [idOrSlug]/[user]",
"create_team": "Opret Team",
"create_team": "Opret team",
"name": "Navn",
"nameless_team": "Unavngivet team",
"oauth_clients": "OAuth-klienter",
@@ -1019,6 +1020,7 @@
"attendees": "Deltagere",
"add_attendees": "Tilføj deltagere",
"show_advanced_settings": "Vis avancerede indstillinger",
"hide_advanced_settings": "Skjul avancerede indstillinger",
"event_name": "Begivenhedsnavn",
"event_name_in_calendar": "Begivenhedsnavn i kalender",
"event_name_tooltip": "Navnet, der vises i kalendere",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Dine teammedlemmer vil ikke kunne se andre teammedlemmer, når dette er slået til.",
"you_cannot_see_team_members": "Du kan ikke se alle teammedlemmer i et privat team.",
"you_cannot_see_teams_of_org": "Du kan ikke se teams i en privat organisation.",
"allow_booker_to_select_duration": "Tillad bookeren at vælge varighed",
"allow_multiple_durations": "Tillad flere varigheder",
"impersonate_user_tip": "Al brug af denne funktion er revideret.",
"impersonating_user_warning": "Efterligner brugernavnet \"{{user}}\".",
"impersonating_stop_instructions": "Klik her for at stoppe",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Reservationsbekræftelse mislykkedes",
"not_enough_seats": "Ikke nok pladser",
"form_builder_field_already_exists": "Et felt med dette navn findes allerede",
"guests_field_must_be_multiemail": "Gæstefeltet skal være af typen 'Flere e-mails'",
"show_on_booking_page": "Vis på bookingsiden",
"visit_cancelled_booking": "Du kan besøge siden for den aflyste booking",
"get_started_zapier_templates": "Kom i gang med Zapier skabeloner",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Låst af teamadministrator",
"app_not_connected": "Du har ikke forbundet en {{appName}}-konto.",
"connect_now": "Forbind nu",
"connect_now_unavailable_tooltip": "Forbind nu er i øjeblikket ikke tilgængelig. Book venligst fra ovenstående tidsrum eller prøv igen senere.",
"managed_event_dialog_confirm_button_one": "Erstat og underret {{count}} medlem",
"managed_event_dialog_confirm_button_other": "Erstat og underret {{count}} medlemmer",
"managed_event_dialog_title_one": "URL'en /{{slug}} findes allerede for {{count}} medlem. Vil du erstatte den?",
@@ -2688,7 +2692,7 @@
"enterprise": "Virksomhed",
"enterprise_description": "Opgrader til Enterprise for at oprette din organisation",
"create_your_org": "Opret din Organisation",
"create_org": "Opret Organisation",
"create_org": "Opret organisation",
"create_your_org_description": "Opgrader til Organisationer og få et subdomæne, samlet fakturering, Insights, omfattende whitelabeling og mere",
"create_your_enterprise_description": "Opgrader til Enterprise og få adgang til Active Directory Sync, SCIM Automatisk Brugerprovisionering, Cal.ai Voice Agents, Admin API'er og mere!",
"other_payment_app_enabled": "Du kan kun aktivere én betalingsapp pr. begivenhedstype",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Ved at fortsætte accepterer du vores",
"terms": "Vilkår",
"always_show_x_days": "Vis altid {{x}} dage tilgængelige",
"always_show_x_days_description": "Flytter automatisk {{x}}-dages bookingvinduet frem hver dag, så din kalender altid viser de næste {{x}} dages tilgængelige tidsrum.",
"team_subscription_cancelled_successfully": "Teamabonnement annulleret med succes",
"team_subscription_cancellation_error": "Fejl under annullering af teamabonnement",
"unable_to_subscribe_to_the_platform": "Der opstod en fejl under forsøget på at abonnere på platformplanen, prøv venligst igen senere",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Slutter med",
"filter_operator_is_empty": "Er tom",
"filter_operator_not_empty": "Er ikke tom",
"translate_description_button": "Oversæt titel/beskrivelse til besøgendes browsersprog ved hjælp af AI",
"translate_description_button": "Autooversæt titel og beskrivelse",
"translate_description_button_description": "Oversæt automatisk titler og beskrivelser til besøgendes browsersprog ved hjælp af AI.",
"rr_distribution_method": "Distribution",
"rr_distribution_method_description": "Gør det muligt at optimere distributionen for maksimal tilgængelighed eller for at opnå en mere balanceret tildeling.",
"rr_distribution_method_availability_title": "Maksimer tilgængelighed",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "Sædefunktionen understøtter ikke tilbagevendende begivenheder. Deaktiver sædefunktionen for at gøre den tilbagevendende.",
"recurring_event_seats_error": "Tilbagevendende begivenheder understøtter ikke sædefunktionen. Deaktiver sædefunktionen eller gør begivenheden ikke-tilbagevendende.",
"segment": "Segment",
"saved_filters": "Gemte filtre",
"rename": "Omdøb",
"rename_segment": "Omdøb segment",
"delete_segment": "Slet segment",
@@ -3338,6 +3345,7 @@
"picklist": "Valgliste",
"most_cancelled_bookings": "Flest annullerede",
"interface_language": "Grænsefladesprog",
"interface_language_description": "Indstil dit foretrukne sprog for bookinggrænsefladen",
"visitors_browser_language": "Besøgerens browsersprog",
"name_or_email": "Navn eller e-mail",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Hvis ingen kontakt findes, fald tilbage til lead-ejeren, hvis denne eksisterer",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Rediger og tilpas workflowindstillinger",
"pbac_desc_delete_workflows": "Fjern workflows fra systemet",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Tilgængelighed",
"pbac_resource_out_of_office": "Ude af kontoret",
"pbac_desc_create_webhooks": "Opret webhooks",
"pbac_desc_view_webhooks": "Se webhooks",
"pbac_desc_update_webhooks": "Opdater webhooks",
"pbac_desc_delete_webhooks": "Slet webhooks",
"pbac_desc_create_availability": "Opret tilgængelighed",
"pbac_desc_view_availability": "Se tilgængelighed",
"pbac_desc_update_availability": "Opdater tilgængelighed",
"pbac_desc_delete_availability": "Slet tilgængelighed",
"pbac_desc_create_out_of_office": "Opret fravær",
"pbac_desc_view_out_of_office": "Se fravær",
"pbac_desc_update_out_of_office": "Opdater fravær",
"pbac_desc_delete_out_of_office": "Slet fravær",
"pbac_desc_manage_workflows": "Fuld administrationsadgang til alle workflows",
"pbac_desc_create_event_types": "Opret begivenhedstyper",
"pbac_desc_view_event_types": "Vis begivenhedstyper",
+24 -6
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Geheimer Termin",
"login_instead": "Stattdessen einloggen",
"create_account": "Konto erstellen",
"get_started": "Loslegen",
"confirm_password": "Passwort bestätigen",
"reset_your_password": "Legen Sie Ihr neues Passwort mit den Anweisungen fest, die an Ihre E-Mail-Adresse gesendet wurden.",
"org_banner_instructions": "Bitte laden Sie Bild mit einer Breite von {{width}} und einer Höhe von {{height}} hoch.",
@@ -958,7 +959,7 @@
"light_event_type_color": "Ereignistyp-Farbe (Helles Design)",
"dark_event_type_color": "Ereignistyp-Farbe (Dunkles Design)",
"file_not_named": "Datei heißt nicht [idOrSlug]/[user]",
"create_team": "Team anlegen",
"create_team": "Team erstellen",
"name": "Name",
"nameless_team": "Namenloses Team",
"oauth_clients": "OAuth-Clients",
@@ -1019,6 +1020,7 @@
"attendees": "Teilnehmende",
"add_attendees": "Teilnehmende hinzufügen",
"show_advanced_settings": "Erweiterte Einstellungen anzeigen",
"hide_advanced_settings": "Erweiterte Einstellungen ausblenden",
"event_name": "Termin-Name",
"event_name_in_calendar": "Ereignisname im Kalender",
"event_name_tooltip": "Der Name, der in Kalendern angezeigt wird",
@@ -1231,7 +1233,7 @@
"contact_sales": "Vertrieb kontaktieren",
"error_404": "Fehler 404",
"default": "Standard",
"set_to_default": "Auf Standard setzen",
"set_to_default": "Als Standard festlegen",
"new_schedule_btn": "Neuer Verfügbarkeitsplan",
"add_new_schedule": "Neuen Verfügbarkeitsplan hinzufügen",
"add_new_calendar": "Neuen Kalender hinzufügen",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Wenn diese Einstellung aktiv ist, können deine Teammitglieder keine anderen Teammitglieder sehen.",
"you_cannot_see_team_members": "Sie können nicht alle Teammitglieder eines privaten Teams sehen.",
"you_cannot_see_teams_of_org": "Du kannst keine Teams einer privaten Organisation sehen.",
"allow_booker_to_select_duration": "Bucher erlauben, die Länge zu wählen",
"allow_multiple_durations": "Mehrere Dauern erlauben",
"impersonate_user_tip": "Alle Verwendungen dieser Funktion werden geprüft.",
"impersonating_user_warning": "Imitiere den Account {{user}}.",
"impersonating_stop_instructions": "Klicken Sie hier, um zu stoppen",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Buchungsbestätigung fehlgeschlagen",
"not_enough_seats": "Nicht genügend Plätze",
"form_builder_field_already_exists": "Ein Feld mit diesem Namen ist bereits vorhanden",
"guests_field_must_be_multiemail": "Das Gästefeld muss vom Typ 'Mehrere E-Mails' sein",
"show_on_booking_page": "Auf der Buchungsseite anzeigen",
"visit_cancelled_booking": "Sie können die Seite der stornierten Buchung besuchen",
"get_started_zapier_templates": "Legen Sie mit Zapier-Vorlagen los",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Vom Team-Admin gesperrt",
"app_not_connected": "Sie haben kein {{appName}}-Konto verbunden.",
"connect_now": "Jetzt verbinden",
"connect_now_unavailable_tooltip": "Sofortige Verbindung ist derzeit nicht verfügbar. Bitte buchen Sie aus den obigen Zeitfenstern oder versuchen Sie es später erneut.",
"managed_event_dialog_confirm_button_one": "{{count}} Mitglied ersetzen & benachrichtigen",
"managed_event_dialog_confirm_button_other": "{{count}} Mitglieder ersetzen & benachrichtigen",
"managed_event_dialog_title_one": "Die URL /{{slug}} wird bereits von {{count}} Mitglied genutzt. Möchten Sie sie ersetzen?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Indem Sie fortfahren, stimmen Sie unseren",
"terms": "Nutzungsbedingungen",
"always_show_x_days": "Immer für {{x}} Tage verfügbar",
"always_show_x_days_description": "Verschiebt das {{x}}-Tage-Buchungsfenster automatisch jeden Tag nach vorne und stellt sicher, dass Ihr Kalender immer die nächsten {{x}} Tage mit verfügbaren Zeitfenstern anzeigt.",
"team_subscription_cancelled_successfully": "Team-Abonnement erfolgreich gekündigt",
"team_subscription_cancellation_error": "Fehler bei der Kündigung des Team-Abonnements",
"unable_to_subscribe_to_the_platform": "Beim Abonnieren des Platform-Plans ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Endet mit",
"filter_operator_is_empty": "Ist leer",
"filter_operator_not_empty": "Ist nicht leer",
"translate_description_button": "Titel/Beschreibung mit KI in die Browsersprache des Besuchers übersetzen",
"translate_description_button": "Titel und Beschreibung automatisch übersetzen",
"translate_description_button_description": "Titel und Beschreibungen automatisch in die Browsersprache des Besuchers mittels KI übersetzen.",
"rr_distribution_method": "Verteilung",
"rr_distribution_method_description": "Ermöglicht die Optimierung der Verteilung für maximale Verfügbarkeit oder eine ausgewogenere Zuweisung.",
"rr_distribution_method_availability_title": "Verfügbarkeit maximieren",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "Die Sitzplatzfunktion unterstützt keine wiederkehrenden Events. Deaktivieren Sie die Sitzplatzfunktion, um es wiederkehrend zu machen.",
"recurring_event_seats_error": "Wiederkehrende Events unterstützen die Sitzplatzfunktion nicht. Deaktivieren Sie die Sitzplatzfunktion oder machen Sie das Event nicht wiederkehrend.",
"segment": "Segment",
"saved_filters": "Gespeicherte Filter",
"rename": "Umbenennen",
"rename_segment": "Segment umbenennen",
"delete_segment": "Segment löschen",
@@ -3338,6 +3345,7 @@
"picklist": "Auswahlliste",
"most_cancelled_bookings": "Meiste stornierte Buchungen",
"interface_language": "Oberflächensprache",
"interface_language_description": "Legen Sie Ihre bevorzugte Sprache für die Buchungsoberfläche fest",
"visitors_browser_language": "Browsersprache des Besuchers",
"name_or_email": "Name oder E-Mail",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Wenn kein Kontakt gefunden wird, auf den Lead-Eigentümer zurückgreifen, falls vorhanden",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Workflow-Einstellungen bearbeiten und ändern",
"pbac_desc_delete_workflows": "Workflows aus dem System entfernen",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Verfügbarkeit",
"pbac_resource_out_of_office": "Abwesenheit",
"pbac_desc_create_webhooks": "Webhooks erstellen",
"pbac_desc_view_webhooks": "Webhooks anzeigen",
"pbac_desc_update_webhooks": "Webhooks aktualisieren",
"pbac_desc_delete_webhooks": "Webhooks löschen",
"pbac_desc_create_availability": "Verfügbarkeit erstellen",
"pbac_desc_view_availability": "Verfügbarkeit anzeigen",
"pbac_desc_update_availability": "Verfügbarkeit aktualisieren",
"pbac_desc_delete_availability": "Verfügbarkeit löschen",
"pbac_desc_create_out_of_office": "Abwesenheit erstellen",
"pbac_desc_view_out_of_office": "Abwesenheit anzeigen",
"pbac_desc_update_out_of_office": "Abwesenheit aktualisieren",
"pbac_desc_delete_out_of_office": "Abwesenheit löschen",
"pbac_desc_manage_workflows": "Vollständiger Verwaltungszugriff auf alle Workflows",
"pbac_desc_create_event_types": "Ereignistypen erstellen",
"pbac_desc_view_event_types": "Ereignistypen anzeigen",
@@ -3560,8 +3578,8 @@
"rr_load_balancing_disabled_with_groups": "Lastausgleich ist mit Round-Robin-Gruppen nicht verfügbar",
"round_robin_settings_description": "Passen Sie die Standard-Round-Robin-Einstellungen für dieses Team an",
"round_robin_settings_updated_successfully": "Round-Robin-Einstellungen erfolgreich aktualisiert",
"booking_creation_time": "Zeitpunkt der Buchungserstellung",
"meeting_start_time": "Beginn des Meetings",
"booking_creation_time": "Erstellungszeit der Buchung",
"meeting_start_time": "Besprechungsstartzeit",
"members_affected_by_disabling_delegation_credential": "Betroffene Mitglieder",
"no_members_affected_by_disabling_delegation_credential": "Keine Mitglieder von der Deaktivierung der Delegationsanmeldedaten betroffen",
"download_expense_log": "Ausgabenprotokoll herunterladen",
+22 -4
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Μυστική συνάντηση",
"login_instead": "Σύνδεση αντ' αυτού",
"create_account": "Δημιουργία Λογαριασμού",
"get_started": "Ξεκινήστε",
"confirm_password": "Επιβεβαίωση κωδικού πρόσβασης",
"reset_your_password": "Ορίστε τον νέο σας κωδικό πρόσβασης με τις οδηγίες που στάλθηκαν στη διεύθυνση email σας.",
"org_banner_instructions": "Παρακαλούμε ανεβάστε εικόνα με πλάτος {{width}} και ύψος {{height}}.",
@@ -1019,6 +1020,7 @@
"attendees": "Συμμετέχοντες",
"add_attendees": "Προσθήκη συμμετεχόντων",
"show_advanced_settings": "Εμφάνιση προχωρημένων ρυθμίσεων",
"hide_advanced_settings": "Απόκρυψη προχωρημένων ρυθμίσεων",
"event_name": "Όνομα εκδήλωσης",
"event_name_in_calendar": "Με όνομα εκδήλωσης",
"event_name_tooltip": "Το όνομα που θα εμφανίζεται στα ημερολόγια",
@@ -1231,7 +1233,7 @@
"contact_sales": "Επικοινωνία με Πωλήσεις",
"error_404": "Σφάλμα 404",
"default": "Προεπιλογή",
"set_to_default": "Ορισμός ως Προεπιλογή",
"set_to_default": "Ορισμός ως προεπιλογή",
"new_schedule_btn": "Νέο πρόγραμμα",
"add_new_schedule": "Προσθήκη νέου προγράμματος",
"add_new_calendar": "Προσθήκη νέου ημερολογίου",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Τα μέλη της ομάδας σας δεν θα μπορούν να βλέπουν άλλα μέλη της ομάδας όταν αυτό είναι ενεργοποιημένο.",
"you_cannot_see_team_members": "Δεν μπορείτε να δείτε όλα τα μέλη μιας ιδιωτικής ομάδας.",
"you_cannot_see_teams_of_org": "Δεν μπορείτε να δείτε τις ομάδες ενός ιδιωτικού οργανισμού.",
"allow_booker_to_select_duration": "Επιτρέψτε στον χρήστη να επιλέξει διάρκεια",
"allow_multiple_durations": "Επιτρέψτε πολλαπλές διάρκειες",
"impersonate_user_tip": "Όλες οι χρήσεις αυτής της λειτουργίας καταγράφονται.",
"impersonating_user_warning": "Μιμείστε τον χρήστη \"{{user}}\".",
"impersonating_stop_instructions": "Κάντε κλικ εδώ για διακοπή",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Η επιβεβαίωση της κράτησης απέτυχε",
"not_enough_seats": "Δεν υπάρχουν αρκετές θέσεις",
"form_builder_field_already_exists": "Υπάρχει ήδη ένα πεδίο με αυτό το όνομα",
"guests_field_must_be_multiemail": "Το πεδίο επισκεπτών πρέπει να είναι τύπου 'Πολλαπλά emails'",
"show_on_booking_page": "Εμφάνιση στη σελίδα κράτησης",
"visit_cancelled_booking": "Μπορείτε να επισκεφθείτε τη σελίδα ακυρωμένων κρατήσεων",
"get_started_zapier_templates": "Ξεκινήστε με τα πρότυπα του Zapier",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Κλειδωμένο από τον διαχειριστή ομάδας",
"app_not_connected": "Δεν έχετε συνδέσει λογαριασμό {{appName}}.",
"connect_now": "Σύνδεση τώρα",
"connect_now_unavailable_tooltip": "Η άμεση σύνδεση δεν είναι διαθέσιμη αυτή τη στιγμή. Παρακαλώ κάντε κράτηση από τις παραπάνω διαθέσιμες ώρες ή δοκιμάστε αργότερα.",
"managed_event_dialog_confirm_button_one": "Αντικατάσταση & ειδοποίηση {{count}} μέλους",
"managed_event_dialog_confirm_button_other": "Αντικατάσταση & ειδοποίηση {{count}} μελών",
"managed_event_dialog_title_one": "Το url /{{slug}} υπάρχει ήδη για {{count}} μέλος. Θέλετε να το αντικαταστήσετε;",
@@ -2688,7 +2692,7 @@
"enterprise": "Enterprise",
"enterprise_description": "Αναβαθμίστε σε Enterprise για να δημιουργήσετε τον οργανισμό σας",
"create_your_org": "Δημιουργήστε τον Οργανισμό σας",
"create_org": "Δημιουργία Οργανισμού",
"create_org": "Δημιουργία οργανισμού",
"create_your_org_description": "Αναβαθμίστε σε Οργανισμούς και λάβετε υποτομέα, ενοποιημένη τιμολόγηση, Αναλύσεις, εκτεταμένο white-labeling και άλλα",
"create_your_enterprise_description": "Αναβαθμίστε σε Enterprise και αποκτήστε πρόσβαση σε Συγχρονισμό Active Directory, Αυτόματη παροχή χρηστών SCIM, Φωνητικούς πράκτορες Cal.ai, APIs διαχειριστή και άλλα!",
"other_payment_app_enabled": "Μπορείτε να ενεργοποιήσετε μόνο μία εφαρμογή πληρωμών ανά τύπο εκδήλωσης",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Προχωρώντας, συμφωνείτε με τους",
"terms": "Όρους",
"always_show_x_days": "Πάντα {{x}} διαθέσιμες ημέρες",
"always_show_x_days_description": "Μετακινεί αυτόματα το παράθυρο κρατήσεων {{x}} ημερών κάθε μέρα, διασφαλίζοντας ότι το ημερολόγιό σας δείχνει πάντα τις επόμενες {{x}} ημέρες διαθέσιμων χρονοθυρίδων.",
"team_subscription_cancelled_successfully": "Η συνδρομή ομάδας ακυρώθηκε με επιτυχία",
"team_subscription_cancellation_error": "Σφάλμα κατά την ακύρωση της συνδρομής ομάδας",
"unable_to_subscribe_to_the_platform": "Παρουσιάστηκε σφάλμα κατά την εγγραφή στο πρόγραμμα της πλατφόρμας, παρακαλώ δοκιμάστε ξανά αργότερα",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Τελειώνει με",
"filter_operator_is_empty": "Είναι κενό",
"filter_operator_not_empty": "Δεν είναι κενό",
"translate_description_button": "Μετάφραση τίτλου/περιγραφής στη γλώσσα του προγράμματος περιήγησης του επισκέπτη με χρήση AI",
"translate_description_button": "Αυτόματη μετάφραση τίτλου και περιγραφής",
"translate_description_button_description": "Αυτόματη μετάφραση τίτλων και περιγραφών στη γλώσσα του προγράμματος περιήγησης του επισκέπτη χρησιμοποιώντας AI.",
"rr_distribution_method": "Κατανομή",
"rr_distribution_method_description": "Επιτρέπει τη βελτιστοποίηση της κατανομής για μέγιστη διαθεσιμότητα ή για πιο ισορροπημένη ανάθεση.",
"rr_distribution_method_availability_title": "Μεγιστοποίηση διαθεσιμότητας",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "Η λειτουργία θέσεων δεν υποστηρίζει επαναλαμβανόμενη εκδήλωση. Απενεργοποιήστε τη λειτουργία θέσεων για να την κάνετε επαναλαμβανόμενη.",
"recurring_event_seats_error": "Η επαναλαμβανόμενη εκδήλωση δεν υποστηρίζει τη λειτουργία θέσεων. Απενεργοποιήστε τη λειτουργία θέσεων ή κάντε την εκδήλωση μη επαναλαμβανόμενη.",
"segment": "Τμήμα",
"saved_filters": "Αποθηκευμένα φίλτρα",
"rename": "Μετονομασία",
"rename_segment": "Μετονομασία τμήματος",
"delete_segment": "Διαγραφή τμήματος",
@@ -3338,6 +3345,7 @@
"picklist": "Λίστα επιλογών",
"most_cancelled_bookings": "Περισσότερες ακυρωμένες κρατήσεις",
"interface_language": "Γλώσσα διεπαφής",
"interface_language_description": "Ορίστε την προτιμώμενη γλώσσα για το περιβάλλον κρατήσεων",
"visitors_browser_language": "Γλώσσα προγράμματος περιήγησης επισκέπτη",
"name_or_email": "Όνομα ή Email",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Αν δεν βρεθεί επαφή, επιστροφή στον ιδιοκτήτη του lead εάν υπάρχει",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Επεξεργασία και τροποποίηση ρυθμίσεων ροών εργασίας",
"pbac_desc_delete_workflows": "Αφαίρεση ροών εργασίας από το σύστημα",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Διαθεσιμότητα",
"pbac_resource_out_of_office": "Εκτός Γραφείου",
"pbac_desc_create_webhooks": "Δημιουργία webhooks",
"pbac_desc_view_webhooks": "Προβολή webhooks",
"pbac_desc_update_webhooks": "Ενημέρωση webhooks",
"pbac_desc_delete_webhooks": "Διαγραφή webhooks",
"pbac_desc_create_availability": "Δημιουργία διαθεσιμότητας",
"pbac_desc_view_availability": "Προβολή διαθεσιμότητας",
"pbac_desc_update_availability": "Ενημέρωση διαθεσιμότητας",
"pbac_desc_delete_availability": "Διαγραφή διαθεσιμότητας",
"pbac_desc_create_out_of_office": "Δημιουργία απουσίας",
"pbac_desc_view_out_of_office": "Προβολή απουσίας",
"pbac_desc_update_out_of_office": "Ενημέρωση απουσίας",
"pbac_desc_delete_out_of_office": "Διαγραφή απουσίας",
"pbac_desc_manage_workflows": "Πλήρης πρόσβαση διαχείρισης σε όλες τις ροές εργασίας",
"pbac_desc_create_event_types": "Δημιουργία τύπων εκδηλώσεων",
"pbac_desc_view_event_types": "Προβολή τύπων εκδηλώσεων",
@@ -2152,6 +2152,9 @@
"delete_sso_configuration_confirmation_description": "Are you sure you want to delete the {{connectionType}} configuration? Your team members who use {{connectionType}} login will no longer be able to access Cal.com.",
"organizer_timezone": "Organizer timezone",
"email_user_cta": "View Invitation",
"accept_invite": "Accept Invite",
"join_now": "Join Now",
"successfully_joined": "Successfully joined",
"email_no_user_invite_heading_subteam": "You've been invited to join a team of {{parentTeamName}} organization",
"email_no_user_invite_heading_org": "You've been invited to join a {{appName}} organization",
"email_no_user_invite_subheading": "{{invitedBy}} has invited you to join their team on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.",
@@ -2301,6 +2304,7 @@
"locked_by_team_admin": "Locked by team admin",
"app_not_connected": "You have not connected a {{appName}} account.",
"connect_now": "Connect now",
"connect_now_unavailable_tooltip": "Connect now is currently unavailable. Please book from above slots or try again later.",
"managed_event_dialog_confirm_button_one": "Replace & notify {{count}} member",
"managed_event_dialog_confirm_button_other": "Replace & notify {{count}} members",
"managed_event_dialog_title_one": "The url /{{slug}} already exists for {{count}} member. Do you want to replace it?",
@@ -321,6 +321,7 @@
"secret_meeting": "Reunión Secreta",
"login_instead": "Inicia sesión en su lugar",
"create_account": "Crear cuenta",
"get_started": "Comenzar",
"confirm_password": "Confirmar contraseña",
"reset_your_password": "Establece tu nueva contraseña con las instrucciones enviadas a tu dirección de correo electrónico.",
"org_banner_instructions": "Por favor, sube una imagen de {{width}} de ancho y {{height}} de alto.",
@@ -958,7 +959,7 @@
"light_event_type_color": "Color del Tipo de Evento (Tema Claro)",
"dark_event_type_color": "Color del Tipo de Evento (Tema Oscuro)",
"file_not_named": "El archivo no está nombrado [idOrSlug]/[user]",
"create_team": "Crear Equipo",
"create_team": "Crear equipo",
"name": "Nombre",
"nameless_team": "Equipo sin nombre",
"oauth_clients": "Clientes OAuth",
@@ -1019,6 +1020,7 @@
"attendees": "Asistentes",
"add_attendees": "Agregar asistentes",
"show_advanced_settings": "Mostrar configuraciones avanzadas",
"hide_advanced_settings": "Ocultar configuración avanzada",
"event_name": "Nombre del evento",
"event_name_in_calendar": "Con nombre del evento",
"event_name_tooltip": "El nombre que aparecerá en los calendarios",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Los miembros de tu equipo no podrán ver a otros miembros del equipo cuando esto esté activado.",
"you_cannot_see_team_members": "No puedes ver a todos los miembros de un equipo privado.",
"you_cannot_see_teams_of_org": "No puedes ver los equipos de una organización privada.",
"allow_booker_to_select_duration": "Permitir al reservador seleccionar la duración",
"allow_multiple_durations": "Permitir múltiples duraciones",
"impersonate_user_tip": "Todos los usos de esta función son auditados.",
"impersonating_user_warning": "Suplantando al usuario \"{{user}}\".",
"impersonating_stop_instructions": "Haz clic aquí para detener",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "La confirmación de la reserva falló",
"not_enough_seats": "No hay suficientes asientos",
"form_builder_field_already_exists": "Ya existe un campo con este nombre",
"guests_field_must_be_multiemail": "El campo de invitados debe ser de tipo 'Múltiples correos electrónicos'",
"show_on_booking_page": "Mostrar en la página de reserva",
"visit_cancelled_booking": "Puedes visitar la página de la reserva cancelada",
"get_started_zapier_templates": "Comienza con las plantillas de Zapier",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Bloqueado por el administrador del equipo",
"app_not_connected": "No has conectado una cuenta de {{appName}}.",
"connect_now": "Conectar ahora",
"connect_now_unavailable_tooltip": "Conectar ahora no está disponible actualmente. Por favor, reserva desde los espacios anteriores o intenta más tarde.",
"managed_event_dialog_confirm_button_one": "Reemplazar y notificar a {{count}} miembro",
"managed_event_dialog_confirm_button_other": "Reemplazar y notificar a {{count}} miembros",
"managed_event_dialog_title_one": "La URL /{{slug}} ya existe para {{count}} miembro. ¿Quieres reemplazarla?",
@@ -2688,7 +2692,7 @@
"enterprise": "Empresa",
"enterprise_description": "Actualiza a Enterprise para crear tu Organización",
"create_your_org": "Crea tu Organización",
"create_org": "Crear Organización",
"create_org": "Crear organización",
"create_your_org_description": "Actualiza a Organizaciones y recibe un subdominio, facturación unificada, Insights, etiquetado blanco extensivo y más",
"create_your_enterprise_description": "Actualiza a Enterprise y obtén acceso a la sincronización de Active Directory, aprovisionamiento automático de usuarios SCIM, agentes de voz Cal.ai, APIs de administración y más",
"other_payment_app_enabled": "Solo puedes habilitar una aplicación de pago por tipo de evento",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Al continuar, aceptas nuestros",
"terms": "Términos",
"always_show_x_days": "Siempre {{x}} días disponibles",
"always_show_x_days_description": "Mueve automáticamente la ventana de reservas de {{x}} días hacia adelante cada día, asegurando que tu calendario siempre muestre los próximos {{x}} días de espacios disponibles.",
"team_subscription_cancelled_successfully": "Suscripción del equipo cancelada con éxito",
"team_subscription_cancellation_error": "Error al cancelar la suscripción del equipo",
"unable_to_subscribe_to_the_platform": "Ocurrió un error al intentar suscribirse al plan de la plataforma, por favor intenta de nuevo más tarde",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Termina con",
"filter_operator_is_empty": "Está vacío",
"filter_operator_not_empty": "No está vacío",
"translate_description_button": "Traducir título/descripción al idioma del navegador del visitante usando IA",
"translate_description_button": "Traducir automáticamente título y descripción",
"translate_description_button_description": "Traduce automáticamente títulos y descripciones al idioma del navegador del visitante usando IA.",
"rr_distribution_method": "Distribución",
"rr_distribution_method_description": "Permite optimizar la distribución para maximizar la disponibilidad o lograr una asignación más equilibrada.",
"rr_distribution_method_availability_title": "Maximizar disponibilidad",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "La función de asientos no admite eventos recurrentes. Desactiva la función de asientos para hacerlo recurrente.",
"recurring_event_seats_error": "Los eventos recurrentes no admiten la función de asientos. Desactiva la función de asientos o haz que el evento no sea recurrente.",
"segment": "Segmento",
"saved_filters": "Filtros guardados",
"rename": "Renombrar",
"rename_segment": "Renombrar segmento",
"delete_segment": "Eliminar segmento",
@@ -3338,6 +3345,7 @@
"picklist": "Lista de selección",
"most_cancelled_bookings": "Más cancelaciones",
"interface_language": "Idioma de la interfaz",
"interface_language_description": "Establece tu idioma preferido para la interfaz de reservas",
"visitors_browser_language": "Idioma del navegador del visitante",
"name_or_email": "Nombre o correo electrónico",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Si no se encuentra ningún contacto, recurrir al propietario del lead si existe",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Editar y modificar configuraciones de flujos de trabajo",
"pbac_desc_delete_workflows": "Eliminar flujos de trabajo del sistema",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Disponibilidad",
"pbac_resource_out_of_office": "Fuera de la oficina",
"pbac_desc_create_webhooks": "Crear webhooks",
"pbac_desc_view_webhooks": "Ver webhooks",
"pbac_desc_update_webhooks": "Actualizar webhooks",
"pbac_desc_delete_webhooks": "Eliminar webhooks",
"pbac_desc_create_availability": "Crear disponibilidad",
"pbac_desc_view_availability": "Ver disponibilidad",
"pbac_desc_update_availability": "Actualizar disponibilidad",
"pbac_desc_delete_availability": "Eliminar disponibilidad",
"pbac_desc_create_out_of_office": "Crear ausencia",
"pbac_desc_view_out_of_office": "Ver ausencia",
"pbac_desc_update_out_of_office": "Actualizar ausencia",
"pbac_desc_delete_out_of_office": "Eliminar ausencia",
"pbac_desc_manage_workflows": "Acceso completo de gestión a todos los flujos de trabajo",
"pbac_desc_create_event_types": "Crear tipos de eventos",
"pbac_desc_view_event_types": "Ver tipos de eventos",
+22 -4
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Reunión Secreta",
"login_instead": "Iniciar Sesión en su lugar",
"create_account": "Crear Cuenta",
"get_started": "Comenzar",
"confirm_password": "Confirmar Contraseña",
"reset_your_password": "Establezca su nueva contraseña con las instrucciones enviadas a su dirección de correo electrónico.",
"org_banner_instructions": "Por favor, sube una imagen de {{width}} de ancho y {{height}} de alto.",
@@ -958,7 +959,7 @@
"light_event_type_color": "Color del tipo de evento (Tema claro)",
"dark_event_type_color": "Color del tipo de evento (Tema oscuro)",
"file_not_named": "El archivo no se llama [idOrSlug] / [user]",
"create_team": "Crear Equipo",
"create_team": "Crear equipo",
"name": "Nombre",
"nameless_team": "Equipo sin nombre",
"oauth_clients": "Clientes OAuth",
@@ -1019,6 +1020,7 @@
"attendees": "Asistentes",
"add_attendees": "Añadir Asistentes",
"show_advanced_settings": "Mostrar Opciones Avanzadas",
"hide_advanced_settings": "Ocultar ajustes avanzados",
"event_name": "Nombre del Evento",
"event_name_in_calendar": "Nombre del evento en el calendario",
"event_name_tooltip": "Nombre que aparecerá en los calendarios",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Los miembros de su equipo no podrán ver a otros miembros del equipo cuando esto esté activado.",
"you_cannot_see_team_members": "No puede ver a todos los miembros de un equipo privado.",
"you_cannot_see_teams_of_org": "No puedes ver los equipos de una organización privada.",
"allow_booker_to_select_duration": "Permitir que el encargado de las reservas seleccione la duración",
"allow_multiple_durations": "Permitir múltiples duraciones",
"impersonate_user_tip": "Todos los usos de esta función son revisados.",
"impersonating_user_warning": "Suplantando nombre de usuario \"{{user}}\".",
"impersonating_stop_instructions": "Haga clic aquí para detener.",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Error al confirmar la reserva",
"not_enough_seats": "No hay suficientes plazas",
"form_builder_field_already_exists": "Ya existe un campo con este nombre",
"guests_field_must_be_multiemail": "El campo de invitados debe ser de tipo 'Múltiples correos electrónicos'",
"show_on_booking_page": "Mostrar en la página de reserva",
"visit_cancelled_booking": "Puedes visitar la página de la reserva cancelada",
"get_started_zapier_templates": "Comience con las plantillas de Zapier",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Bloqueado por el administrador del equipo",
"app_not_connected": "No ha conectado una cuenta de {{appName}}.",
"connect_now": "Conectar ahora",
"connect_now_unavailable_tooltip": "Conectar ahora no está disponible actualmente. Por favor, reserve desde los espacios anteriores o inténtelo más tarde.",
"managed_event_dialog_confirm_button_one": "Reemplazar y notificar a {{count}} miembro",
"managed_event_dialog_confirm_button_other": "Reemplazar y notificar a {{count}} miembros",
"managed_event_dialog_title_one": "La url /{{slug}} ya existe para {{count}} miembro. ¿Desea reemplazarla?",
@@ -2688,7 +2692,7 @@
"enterprise": "Empresa",
"enterprise_description": "Actualiza a Enterprise para crear tu Organización",
"create_your_org": "Crea tu Organización",
"create_org": "Crear Organización",
"create_org": "Crear organización",
"create_your_org_description": "Actualiza a Organizaciones y recibe un subdominio, facturación unificada, Insights, personalización extensa y más",
"create_your_enterprise_description": "Actualiza a Enterprise y obtén acceso a la sincronización de Active Directory, aprovisionamiento automático de usuarios SCIM, agentes de voz de Cal.ai, APIs de administración y más",
"other_payment_app_enabled": "Solo puedes habilitar una aplicación de pago por tipo de evento",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Al continuar, aceptas nuestros",
"terms": "Términos",
"always_show_x_days": "Siempre {{x}} días disponibles",
"always_show_x_days_description": "Mueve automáticamente la ventana de reservas de {{x}} días hacia adelante cada día, asegurando que tu calendario siempre muestre los próximos {{x}} días de espacios disponibles.",
"team_subscription_cancelled_successfully": "Suscripción del equipo cancelada con éxito",
"team_subscription_cancellation_error": "Error al cancelar la suscripción del equipo",
"unable_to_subscribe_to_the_platform": "Ocurrió un error al intentar suscribirse al plan de la plataforma, por favor intente de nuevo más tarde",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Termina con",
"filter_operator_is_empty": "Está vacío",
"filter_operator_not_empty": "No está vacío",
"translate_description_button": "Traducir título/descripción al idioma del navegador del visitante usando IA",
"translate_description_button": "Auto traducir título y descripción",
"translate_description_button_description": "Traduce automáticamente títulos y descripciones al idioma del navegador del visitante usando IA.",
"rr_distribution_method": "Distribución",
"rr_distribution_method_description": "Permite optimizar la distribución para maximizar la disponibilidad o lograr una asignación más equilibrada.",
"rr_distribution_method_availability_title": "Maximizar disponibilidad",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "La función de asientos no admite eventos recurrentes. Desactiva la función de asientos para hacerlo recurrente.",
"recurring_event_seats_error": "Los eventos recurrentes no admiten la función de asientos. Desactiva la función de asientos o haz que el evento no sea recurrente.",
"segment": "Segmento",
"saved_filters": "Filtros guardados",
"rename": "Renombrar",
"rename_segment": "Renombrar segmento",
"delete_segment": "Eliminar segmento",
@@ -3338,6 +3345,7 @@
"picklist": "Lista de selección",
"most_cancelled_bookings": "Más cancelaciones",
"interface_language": "Idioma de la interfaz",
"interface_language_description": "Establece tu idioma preferido para la interfaz de reservas",
"visitors_browser_language": "Idioma del navegador del visitante",
"name_or_email": "Nombre o correo electrónico",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Si no se encuentra ningún contacto, recurrir al propietario del lead si existe",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Editar y modificar ajustes de flujos de trabajo",
"pbac_desc_delete_workflows": "Eliminar flujos de trabajo del sistema",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Disponibilidad",
"pbac_resource_out_of_office": "Fuera de la oficina",
"pbac_desc_create_webhooks": "Crear webhooks",
"pbac_desc_view_webhooks": "Ver webhooks",
"pbac_desc_update_webhooks": "Actualizar webhooks",
"pbac_desc_delete_webhooks": "Eliminar webhooks",
"pbac_desc_create_availability": "Crear disponibilidad",
"pbac_desc_view_availability": "Ver disponibilidad",
"pbac_desc_update_availability": "Actualizar disponibilidad",
"pbac_desc_delete_availability": "Eliminar disponibilidad",
"pbac_desc_create_out_of_office": "Crear fuera de la oficina",
"pbac_desc_view_out_of_office": "Ver fuera de la oficina",
"pbac_desc_update_out_of_office": "Actualizar fuera de la oficina",
"pbac_desc_delete_out_of_office": "Eliminar fuera de la oficina",
"pbac_desc_manage_workflows": "Acceso completo de gestión a todos los flujos de trabajo",
"pbac_desc_create_event_types": "Crear tipos de eventos",
"pbac_desc_view_event_types": "Ver tipos de eventos",
+22 -4
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Salajane koosolek",
"login_instead": "Selle asemel logi sisse",
"create_account": "Loo konto",
"get_started": "Alusta",
"confirm_password": "Kinnita salasõna",
"reset_your_password": "Seadke oma uus parool teie e-posti aadressile saadetud juhiste järgi.",
"org_banner_instructions": "Palun laadige üles pilt laiusega {{width}} ja kõrgusega {{height}}.",
@@ -1019,6 +1020,7 @@
"attendees": "Osalejad",
"add_attendees": "Lisa osalejaid",
"show_advanced_settings": "Kuva täpsemad seaded",
"hide_advanced_settings": "Peida täpsemad seaded",
"event_name": "Sündmuse nimi",
"event_name_in_calendar": "Sündmuse nimega",
"event_name_tooltip": "Kalendrites kuvatav nimi",
@@ -1231,7 +1233,7 @@
"contact_sales": "Võtke ühendust müügiga",
"error_404": "Viga 404",
"default": "Vaikimisi",
"set_to_default": "Set to Default",
"set_to_default": "Määra vaikimisi",
"new_schedule_btn": "Uus ajakava",
"add_new_schedule": "Lisa uus ajakava",
"add_new_calendar": "Lisa uus kalender",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Teie meeskonnaliikmed ei näe teisi meeskonnaliikmeid, kui see on sisse lülitatud.",
"you_cannot_see_team_members": "Te ei saa näha erameeskonna kõiki meeskonnaliikmeid.",
"you_cannot_see_teams_of_org": "Te ei saa näha eraorganisatsiooni meeskondi.",
"allow_booker_to_select_duration": "Luba broneerijal valida kestus",
"allow_multiple_durations": "Luba mitu kestust",
"impersonate_user_tip": "Kõik selle funktsiooni kasutused on auditeeritud.",
"impersonating_user_warning": "Esitan end kasutajanimena \"{{user}}\".",
"impersonating_stop_instructions": "Peatamiseks klõpsake siin",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Broneeringu kinnitamine ebaõnnestus",
"not_enough_seats": "Pole piisavalt kohti",
"form_builder_field_already_exists": "Selle nimega väli on juba olemas",
"guests_field_must_be_multiemail": "Külaliste väli peab olema tüüpi 'Mitu e-posti'",
"show_on_booking_page": "Näita broneerimislehel",
"visit_cancelled_booking": "Saate külastada tühistatud broneeringu lehte",
"get_started_zapier_templates": "Alustage Zapieri mallidega",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Meeskonna administraatori poolt lukustatud",
"app_not_connected": "Te pole rakenduse {{appName}} kontot ühendanud.",
"connect_now": "Ühenda kohe",
"connect_now_unavailable_tooltip": "Funktsioon 'Ühenda kohe' ei ole praegu saadaval. Palun broneerige ülaltoodud ajavahemikest või proovige hiljem uuesti.",
"managed_event_dialog_confirm_button_one": "Asenda ja teavita {{count}} liiget",
"managed_event_dialog_confirm_button_other": "Asenda ja teavitage {{count}} liiget",
"managed_event_dialog_title_one": "URL /{{slug}} on {{count}} liikme jaoks juba olemas. Kas soovite selle asendada?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Jätkates nõustute meie",
"terms": "Tingimustega",
"always_show_x_days": "Alati {{x}} päeva saadaval",
"always_show_x_days_description": "Liigutab automaatselt {{x}}-päevase broneerimisakna iga päev edasi, tagades, et teie kalender näitab alati järgmise {{x}} päeva saadavalolevaid aegu.",
"team_subscription_cancelled_successfully": "Meeskonna tellimus tühistati edukalt",
"team_subscription_cancellation_error": "Viga meeskonna tellimuse tühistamisel",
"unable_to_subscribe_to_the_platform": "Platvormipaketi tellimisel ilmnes viga, proovige hiljem uuesti",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Lõpeb",
"filter_operator_is_empty": "On tühi",
"filter_operator_not_empty": "Ei ole tühi",
"translate_description_button": "Tõlgi pealkiri/kirjeldus tehisintellekti abil külastaja brauseri keelde",
"translate_description_button": "Tõlgi pealkiri ja kirjeldus automaatselt",
"translate_description_button_description": "Tõlgib pealkirjad ja kirjeldused automaatselt külastaja brauseri keelde, kasutades tehisintellekti.",
"rr_distribution_method": "Jaotamine",
"rr_distribution_method_description": "Võimaldab optimeerida jaotust maksimaalseks kättesaadavuseks või tasakaalustatumaks määramiseks.",
"rr_distribution_method_availability_title": "Maksimeeri kättesaadavus",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "Kohtade funktsioon ei toeta korduvat sündmust. Keelake kohtade funktsioon, et muuta see korduvaks.",
"recurring_event_seats_error": "Korduv sündmus ei toeta kohtade funktsiooni. Keelake kohtade funktsioon või muutke sündmus mittekorduvaks.",
"segment": "Segment",
"saved_filters": "Salvestatud filtrid",
"rename": "Nimeta ümber",
"rename_segment": "Nimeta segment ümber",
"delete_segment": "Kustuta segment",
@@ -3338,6 +3345,7 @@
"picklist": "Valikloend",
"most_cancelled_bookings": "Kõige rohkem tühistatud broneeringuid",
"interface_language": "Liidese keel",
"interface_language_description": "Määra broneerimisliidese eelistatud keel",
"visitors_browser_language": "Külastaja brauseri keel",
"name_or_email": "Nimi või e-post",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Kui kontakti ei leita, kasuta võimalusel liidi omanikku",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Muuda ja redigeeri töövoo seadeid",
"pbac_desc_delete_workflows": "Eemalda töövood süsteemist",
"pbac_resource_webhook": "Veebihaak",
"pbac_resource_availability": "Saadavus",
"pbac_resource_out_of_office": "Kontorist väljas",
"pbac_desc_create_webhooks": "Loo veebihaake",
"pbac_desc_view_webhooks": "Vaata veebihaake",
"pbac_desc_update_webhooks": "Uuenda veebihaake",
"pbac_desc_delete_webhooks": "Kustuta veebihaake",
"pbac_desc_create_availability": "Loo saadavus",
"pbac_desc_view_availability": "Vaata saadavust",
"pbac_desc_update_availability": "Uuenda saadavust",
"pbac_desc_delete_availability": "Kustuta saadavus",
"pbac_desc_create_out_of_office": "Loo kontorist väljas olek",
"pbac_desc_view_out_of_office": "Vaata kontorist väljas olekut",
"pbac_desc_update_out_of_office": "Uuenda kontorist väljas olekut",
"pbac_desc_delete_out_of_office": "Kustuta kontorist väljas olek",
"pbac_desc_manage_workflows": "Täielik haldusõigus kõikidele töövoogudele",
"pbac_desc_create_event_types": "Loo sündmuse tüüpe",
"pbac_desc_view_event_types": "Vaata sündmuse tüüpe",
@@ -3560,7 +3578,7 @@
"rr_load_balancing_disabled_with_groups": "Koormuse tasakaalustamine ei ole saadaval Round Robin gruppidega",
"round_robin_settings_description": "Kohanda selle meeskonna vaikimisi round robin'i seadeid",
"round_robin_settings_updated_successfully": "Round robin'i seaded on edukalt uuendatud",
"booking_creation_time": "Broneeringu loomise aeg",
"booking_creation_time": "Broneerimise loomise aeg",
"meeting_start_time": "Kohtumise algusaeg",
"members_affected_by_disabling_delegation_credential": "Mõjutatud liikmed",
"no_members_affected_by_disabling_delegation_credential": "Delegatsiooni mandaadi keelamine ei mõjuta ühtegi liiget",
+23 -5
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Bilera sekretua",
"login_instead": "Saioa hasi ordez",
"create_account": "Sortu kontua",
"get_started": "Hasi",
"confirm_password": "Baieztatu pasahitza",
"reset_your_password": "Ezarri zure pasahitz berria zure email helbidera bidalitako argibideak jarraituz.",
"org_banner_instructions": "Mesedez, igo {{width}} zabalera eta {{height}} altuera duen irudia.",
@@ -1019,6 +1020,7 @@
"attendees": "Partaideak",
"add_attendees": "Gehitu partaideak",
"show_advanced_settings": "Erakutsi ezarpen aurreratuak",
"hide_advanced_settings": "Ezkutatu ezarpen aurreratuak",
"event_name": "Gertaeraren izena",
"event_name_in_calendar": "Gertaeraren izenarekin",
"event_name_tooltip": "Egutegian agertuko den izena",
@@ -1231,7 +1233,7 @@
"contact_sales": "Jarri harremanetan salmenta taldearekin",
"error_404": "404 errorea",
"default": "Lehenetsitakoa",
"set_to_default": "Ezarri lehenetsitako gisa",
"set_to_default": "Ezarri lehenetsi gisa",
"new_schedule_btn": "Programazio berria",
"add_new_schedule": "Gehitu programazio berria",
"add_new_calendar": "Gehitu egutegi berria",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Hau aktibatuta dagoenean, zure taldeko kideek ezin izango dituzte beste taldekideak ikusi.",
"you_cannot_see_team_members": "Ezin dituzu talde pribatu bateko kide guztiak ikusi.",
"you_cannot_see_teams_of_org": "Ezin dituzu erakunde pribatu bateko taldeak ikusi.",
"allow_booker_to_select_duration": "Erreserbatzaileari iraupena aukeratzen utzi",
"allow_multiple_durations": "Baimendu iraupen anitzak",
"impersonate_user_tip": "Ezaugarri honen erabilera guztiak auditatzen dira.",
"impersonating_user_warning": "\"{{user}}\" erabiltzailea ordezkatzen.",
"impersonating_stop_instructions": "Klikatu hemen gelditzeko",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Erreserba berrespena huts egin du",
"not_enough_seats": "Ez dago nahikoa eserleku",
"form_builder_field_already_exists": "Izen hori duen eremu bat existitzen da dagoeneko",
"guests_field_must_be_multiemail": "Gonbidatuen eremuak 'Helbide elektroniko anitzak' motakoa izan behar du",
"show_on_booking_page": "Erakutsi erreserba orrian",
"visit_cancelled_booking": "Bertan behera utzitako erreserba orria bisitatu dezakezu",
"get_started_zapier_templates": "Hasi Zapier txantiloiekin",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Taldeko administratzaileak blokeatuta",
"app_not_connected": "Ez duzu {{appName}} konturik konektatu.",
"connect_now": "Konektatu orain",
"connect_now_unavailable_tooltip": "Orain konektatzea ez dago eskuragarri. Mesedez, erreserbatu goiko txandetatik edo saiatu berriro geroago.",
"managed_event_dialog_confirm_button_one": "Ordezkatu eta {{count}} kideari jakinarazi",
"managed_event_dialog_confirm_button_other": "Ordezkatu eta {{count}} kideei jakinarazi",
"managed_event_dialog_title_one": "/{{slug}} URLa dagoeneko existitzen da {{count}} kiderentzat. Ordezkatu nahi duzu?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Jarraituz gero, gure honekin ados zaudela adierazten duzu",
"terms": "Baldintzak",
"always_show_x_days": "Beti {{x}} egun erabilgarri",
"always_show_x_days_description": "Automatikoki mugitzen du {{x}} eguneko erreserba-leihoa egunero, zure egutegiak beti hurrengo {{x}} egunetako txanda erabilgarriak erakusten dituela ziurtatuz.",
"team_subscription_cancelled_successfully": "Taldearen harpidetza behar bezala ezeztatu da",
"team_subscription_cancellation_error": "Errorea gertatu da taldearen harpidetza ezeztatzean",
"unable_to_subscribe_to_the_platform": "Errore bat gertatu da plataformaren planera harpidetzen saiatzean, saiatu berriro geroago",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Honekin amaitzen da",
"filter_operator_is_empty": "Hutsik dago",
"filter_operator_not_empty": "Ez dago hutsik",
"translate_description_button": "Bisitariaren nabigatzailearen hizkuntzara itzuli titulua/deskribapena AI erabiliz",
"translate_description_button": "Auto itzuli izenburua eta deskribapena",
"translate_description_button_description": "Automatikoki itzuli izenburuak eta deskribapenak bisitariaren nabigatzailearen hizkuntzara adimen artifizialaren bidez.",
"rr_distribution_method": "Banaketa",
"rr_distribution_method_description": "Banaketa optimizatzeko aukera ematen du, erabilgarritasun maximoa edo esleipen orekatuagoa lortzeko",
"rr_distribution_method_availability_title": "Erabilgarritasuna maximizatu",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "Eserlekuen ezaugarriak ez du ekitaldi errepikakorrik onartzen. Desgaitu eserlekuen ezaugarria errepikakor egiteko.",
"recurring_event_seats_error": "Ekitaldi errepikakorrak ez du eserlekuen ezaugarria onartzen. Desgaitu eserlekuen ezaugarria edo egin ekitaldia ez-errepikakor.",
"segment": "Segmentua",
"saved_filters": "Gordetako iragazkiak",
"rename": "Berrizendatu",
"rename_segment": "Berrizendatu segmentua",
"delete_segment": "Ezabatu segmentua",
@@ -3338,6 +3345,7 @@
"picklist": "Hautaketa-zerrenda",
"most_cancelled_bookings": "Bertan behera utzitako erreserba gehien",
"interface_language": "Interfazearen hizkuntza",
"interface_language_description": "Ezarri zure hizkuntza hobetsia erreserba interfazerako",
"visitors_browser_language": "Bisitariaren nabigatzailearen hizkuntza",
"name_or_email": "Izena edo helbide elektronikoa",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Kontakturik aurkitzen ez bada, itzuli bezeroen arduradunera existitzen bada",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Editatu eta aldatu lan-fluxuen ezarpenak",
"pbac_desc_delete_workflows": "Kendu lan-fluxuak sistematik",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Erabilgarritasuna",
"pbac_resource_out_of_office": "Bulegotik kanpo",
"pbac_desc_create_webhooks": "Sortu webhook-ak",
"pbac_desc_view_webhooks": "Ikusi webhook-ak",
"pbac_desc_update_webhooks": "Webhookak eguneratu",
"pbac_desc_delete_webhooks": "Webhookak ezabatu",
"pbac_desc_create_availability": "Sortu erabilgarritasuna",
"pbac_desc_view_availability": "Ikusi erabilgarritasuna",
"pbac_desc_update_availability": "Eguneratu erabilgarritasuna",
"pbac_desc_delete_availability": "Ezabatu erabilgarritasuna",
"pbac_desc_create_out_of_office": "Sortu bulegotik kanpo",
"pbac_desc_view_out_of_office": "Ikusi bulegotik kanpo",
"pbac_desc_update_out_of_office": "Eguneratu bulegotik kanpo",
"pbac_desc_delete_out_of_office": "Ezabatu bulegotik kanpo",
"pbac_desc_manage_workflows": "Lan-fluxu guztien kudeaketa-sarbide osoa",
"pbac_desc_create_event_types": "Gertaera motak sortu",
"pbac_desc_view_event_types": "Gertaera motak ikusi",
@@ -3560,8 +3578,8 @@
"rr_load_balancing_disabled_with_groups": "Karga-orekatzea ez dago eskuragarri Round Robin taldeekin",
"round_robin_settings_description": "Pertsonalizatu talde honentzako txandakako ezarpen lehenetsiak",
"round_robin_settings_updated_successfully": "Txandakako ezarpenak ondo eguneratu dira",
"booking_creation_time": "Erreserba sortze-ordua",
"meeting_start_time": "Bilera hasiera-ordua",
"booking_creation_time": "Erreserba sortzeko ordua",
"meeting_start_time": "Bilera hasteko ordua",
"members_affected_by_disabling_delegation_credential": "Eragindako kideak",
"no_members_affected_by_disabling_delegation_credential": "Ez dago kiderik delegazio kredentzialak desgaitzeak eraginda",
"download_expense_log": "Deskargatu gastuen erregistroa",
+20 -2
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Salainen tapaaminen",
"login_instead": "Kirjaudu sen sijaan",
"create_account": "Luo tili",
"get_started": "Aloita",
"confirm_password": "Vahvista salasana",
"reset_your_password": "Aseta uusi salasanasi sähköpostiisi lähetettyjen ohjeiden mukaisesti.",
"org_banner_instructions": "Lataa kuva, jonka leveys on {{width}} ja korkeus {{height}}.",
@@ -1019,6 +1020,7 @@
"attendees": "Osallistujat",
"add_attendees": "Lisää osallistujia",
"show_advanced_settings": "Näytä lisäasetukset",
"hide_advanced_settings": "Piilota lisäasetukset",
"event_name": "Tapahtuman nimi",
"event_name_in_calendar": "Tapahtuman nimellä",
"event_name_tooltip": "Nimi, joka näkyy kalentereissa",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Tiimisi jäsenet eivät näe muita tiimin jäseniä, kun tämä on käytössä.",
"you_cannot_see_team_members": "Et voi nähdä yksityisen tiimin kaikkia jäseniä.",
"you_cannot_see_teams_of_org": "Et voi nähdä yksityisen organisaation tiimejä.",
"allow_booker_to_select_duration": "Salli varaajan valita kesto",
"allow_multiple_durations": "Salli useita kestoja",
"impersonate_user_tip": "Kaikki tämän toiminnon käyttö kirjataan.",
"impersonating_user_warning": "Esiinnyt käyttäjänä \"{{user}}\".",
"impersonating_stop_instructions": "Klikkaa tästä lopettaaksesi",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Varauksen vahvistaminen epäonnistui",
"not_enough_seats": "Ei tarpeeksi paikkoja",
"form_builder_field_already_exists": "Tämänniminen kenttä on jo olemassa",
"guests_field_must_be_multiemail": "Vieraiden kentän täytyy olla tyyppiä 'Useita sähköposteja'",
"show_on_booking_page": "Näytä varaussivulla",
"visit_cancelled_booking": "Voit tarkastella peruutettua varausta",
"get_started_zapier_templates": "Aloita Zapier-malleilla",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Tiimin ylläpitäjän lukitsema",
"app_not_connected": "Et ole yhdistänyt {{appName}}-tiliä.",
"connect_now": "Yhdistä nyt",
"connect_now_unavailable_tooltip": "Yhdistä nyt -toiminto ei ole tällä hetkellä käytettävissä. Varaa yllä olevista ajoista tai yritä myöhemmin uudelleen.",
"managed_event_dialog_confirm_button_one": "Korvaa ja ilmoita {{count}} jäsenelle",
"managed_event_dialog_confirm_button_other": "Korvaa ja ilmoita {{count}} jäsenelle",
"managed_event_dialog_title_one": "URL-osoite /{{slug}} on jo käytössä {{count}} jäsenellä. Haluatko korvata sen?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Jatkamalla hyväksyt meidän",
"terms": "Käyttöehdot",
"always_show_x_days": "Aina {{x}} päivää saatavilla",
"always_show_x_days_description": "Siirtää automaattisesti {{x}} päivän varausikkunaa eteenpäin joka päivä, varmistaen että kalenterisi näyttää aina seuraavat {{x}} päivää vapaista ajoista.",
"team_subscription_cancelled_successfully": "Tiimin tilaus peruutettu onnistuneesti",
"team_subscription_cancellation_error": "Virhe tiimin tilauksen peruuttamisessa",
"unable_to_subscribe_to_the_platform": "Tilausta alustaan ei voitu tehdä, yritä myöhemmin uudelleen",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Päättyy",
"filter_operator_is_empty": "On tyhjä",
"filter_operator_not_empty": "Ei tyhjä",
"translate_description_button": "Käännä otsikko/kuvaus kävijän selaimen kielelle tekoälyn avulla",
"translate_description_button": "Käännä otsikko ja kuvaus automaattisesti",
"translate_description_button_description": "Käännä otsikot ja kuvaukset automaattisesti vierailijan selaimen kielelle tekoälyn avulla.",
"rr_distribution_method": "Jakotapa",
"rr_distribution_method_description": "Mahdollistaa jakelun optimoinnin joko maksimaalisen saatavuuden tai tasapainoisen jaon saavuttamiseksi.",
"rr_distribution_method_availability_title": "Maksimoi saatavuus",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "Paikkaominaisuus ei tue toistuvaa tapahtumaa. Poista paikkaominaisuus käytöstä tehdäksesi siitä toistuvan.",
"recurring_event_seats_error": "Toistuva tapahtuma ei tue paikkaominaisuutta. Poista paikkaominaisuus käytöstä tai tee tapahtumasta ei-toistuva.",
"segment": "Segmentti",
"saved_filters": "Tallennetut suodattimet",
"rename": "Nimeä uudelleen",
"rename_segment": "Nimeä segmentti uudelleen",
"delete_segment": "Poista segmentti",
@@ -3338,6 +3345,7 @@
"picklist": "Valintalistaus",
"most_cancelled_bookings": "Eniten peruutettuja varauksia",
"interface_language": "Käyttöliittymän kieli",
"interface_language_description": "Aseta varausliittymän haluamasi kieli",
"visitors_browser_language": "Vierailijan selaimen kieli",
"name_or_email": "Nimi tai sähköposti",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Jos yhteyshenkilöä ei löydy, käytä liidin omistajaa, jos sellainen on olemassa",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Muokkaa työnkulkujen asetuksia",
"pbac_desc_delete_workflows": "Poista työnkulkuja järjestelmästä",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Saatavuus",
"pbac_resource_out_of_office": "Poissa toimistolta",
"pbac_desc_create_webhooks": "Luo webhookeja",
"pbac_desc_view_webhooks": "Tarkastele webhookeja",
"pbac_desc_update_webhooks": "Päivitä webhookit",
"pbac_desc_delete_webhooks": "Poista webhookit",
"pbac_desc_create_availability": "Luo saatavuus",
"pbac_desc_view_availability": "Näytä saatavuus",
"pbac_desc_update_availability": "Päivitä saatavuus",
"pbac_desc_delete_availability": "Poista saatavuus",
"pbac_desc_create_out_of_office": "Luo poissaolo",
"pbac_desc_view_out_of_office": "Näytä poissaolo",
"pbac_desc_update_out_of_office": "Päivitä poissaolo",
"pbac_desc_delete_out_of_office": "Poista poissaolo",
"pbac_desc_manage_workflows": "Täydet hallintaoikeudet kaikkiin työnkulkuihin",
"pbac_desc_create_event_types": "Luo tapahtumatyyppejä",
"pbac_desc_view_event_types": "Näytä tapahtumatyypit",
+20 -2
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Rendez-vous secret",
"login_instead": "Se connecter à la place",
"create_account": "Créer un compte",
"get_started": "Commencer",
"confirm_password": "Confirmer le mot de passe",
"reset_your_password": "Définissez votre nouveau mot de passe avec les instructions envoyées à votre adresse e-mail.",
"org_banner_instructions": "Veuillez télécharger une image d'une largeur de {{width}} et d'une hauteur de {{height}}.",
@@ -1019,6 +1020,7 @@
"attendees": "Participants",
"add_attendees": "Ajouter des participants",
"show_advanced_settings": "Afficher les paramètres avancés",
"hide_advanced_settings": "Masquer les paramètres avancés",
"event_name": "Nom de l'événement",
"event_name_in_calendar": "Nom de l'événement dans le calendrier",
"event_name_tooltip": "Le nom qui apparaîtra dans les calendriers",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Les membres de votre équipe ne pourront pas voir les autres membres de l'équipe si cette option est activée.",
"you_cannot_see_team_members": "Vous ne pouvez pas voir tous les membres d'une équipe privée.",
"you_cannot_see_teams_of_org": "Vous ne pouvez pas voir les équipes d'une organisation privée.",
"allow_booker_to_select_duration": "Autoriser l'utilisateur à sélectionner la durée",
"allow_multiple_durations": "Autoriser plusieurs durées",
"impersonate_user_tip": "Toutes les utilisations de cette fonctionnalité sont vérifiées.",
"impersonating_user_warning": "Emprunt du nom d'utilisateur « {{user}} ».",
"impersonating_stop_instructions": "Cliquez ici pour arrêter",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Échec de la confirmation de la réservation",
"not_enough_seats": "Pas assez de places",
"form_builder_field_already_exists": "Un champ avec ce nom existe déjà",
"guests_field_must_be_multiemail": "Le champ invités doit être de type 'Emails multiples'",
"show_on_booking_page": "Afficher sur la page de réservation",
"visit_cancelled_booking": "Vous pouvez consulter la page de réservation annulée",
"get_started_zapier_templates": "Démarrer avec les modèles Zapier",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Verrouillé par l'administrateur de l'équipe",
"app_not_connected": "Vous n'avez pas connecté de compte {{appName}}.",
"connect_now": "Connecter maintenant",
"connect_now_unavailable_tooltip": "La connexion immédiate n'est pas disponible actuellement. Veuillez réserver parmi les créneaux ci-dessus ou réessayer plus tard.",
"managed_event_dialog_confirm_button_one": "Remplacer et notifier {{count}} membre",
"managed_event_dialog_confirm_button_other": "Remplacer et notifier {{count}} membres",
"managed_event_dialog_title_one": "Le lien /{{slug}} existe déjà pour {{count}} membre. Voulez-vous le remplacer ?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "En continuant, vous acceptez nos",
"terms": "Conditions",
"always_show_x_days": "Tout disponible pendant {{x}} jours",
"always_show_x_days_description": "Déplace automatiquement la fenêtre de réservation de {{x}} jours chaque jour, garantissant que votre calendrier affiche toujours les {{x}} prochains jours de créneaux disponibles.",
"team_subscription_cancelled_successfully": "Abonnement d'équipe annulé avec succès",
"team_subscription_cancellation_error": "Erreur lors de l'annulation de l'abonnement d'équipe",
"unable_to_subscribe_to_the_platform": "Une erreur est survenue lors de la souscription à l'offre de plateforme. Veuillez réessayer plus tard",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Se termine par",
"filter_operator_is_empty": "Est vide",
"filter_operator_not_empty": "N'est pas vide",
"translate_description_button": "Traduire le titre/la description dans la langue du navigateur du visiteur grâce à l'IA",
"translate_description_button": "Traduction automatique du titre et de la description",
"translate_description_button_description": "Traduit automatiquement les titres et descriptions dans la langue du navigateur du visiteur en utilisant l'IA.",
"rr_distribution_method": "Distribution",
"rr_distribution_method_description": "Permet d'optimiser la distribution pour une disponibilité maximale ou pour viser une répartition plus équilibrée.",
"rr_distribution_method_availability_title": "Maximiser la disponibilité",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "La fonctionnalité de places ne prend pas en charge les événements récurrents. Désactivez la fonctionnalité de places pour le rendre récurrent.",
"recurring_event_seats_error": "Les événements récurrents ne prennent pas en charge la fonctionnalité de places. Désactivez la fonctionnalité de places ou rendez l'événement non récurrent.",
"segment": "Segment",
"saved_filters": "Filtres enregistrés",
"rename": "Renommer",
"rename_segment": "Renommer le segment",
"delete_segment": "Supprimer le segment",
@@ -3338,6 +3345,7 @@
"picklist": "Liste de sélection",
"most_cancelled_bookings": "Plus d'annulations",
"interface_language": "Langue de l'interface",
"interface_language_description": "Définissez votre langue préférée pour l'interface de réservation",
"visitors_browser_language": "Langue du navigateur du visiteur",
"name_or_email": "Nom ou e-mail",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Si aucun contact n'est trouvé, se rabattre sur le propriétaire du lead s'il existe",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Éditer et modifier les paramètres des workflows",
"pbac_desc_delete_workflows": "Supprimer des workflows du système",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Disponibilités",
"pbac_resource_out_of_office": "Absence du bureau",
"pbac_desc_create_webhooks": "Créer des webhooks",
"pbac_desc_view_webhooks": "Consulter les webhooks",
"pbac_desc_update_webhooks": "Mettre à jour les webhooks",
"pbac_desc_delete_webhooks": "Supprimer les webhooks",
"pbac_desc_create_availability": "Créer des disponibilités",
"pbac_desc_view_availability": "Voir les disponibilités",
"pbac_desc_update_availability": "Mettre à jour les disponibilités",
"pbac_desc_delete_availability": "Supprimer des disponibilités",
"pbac_desc_create_out_of_office": "Créer une absence",
"pbac_desc_view_out_of_office": "Voir les absences",
"pbac_desc_update_out_of_office": "Mettre à jour une absence",
"pbac_desc_delete_out_of_office": "Supprimer une absence",
"pbac_desc_manage_workflows": "Accès complet à la gestion de tous les workflows",
"pbac_desc_create_event_types": "Créer des types d'événements",
"pbac_desc_view_event_types": "Voir les types d'événements",
+21 -3
View File
@@ -321,6 +321,7 @@
"secret_meeting": "פגישה חשאית",
"login_instead": "להיכנס במקום זאת",
"create_account": "צור חשבון",
"get_started": "להתחיל",
"confirm_password": "אישור סיסמה",
"reset_your_password": "הגדר/י את הסיסמה החדשה שלך לפי ההוראות שנשלחו אל כתובת הדוא\"ל שלך.",
"org_banner_instructions": "נא להעלות תמונה ברוחב {{width}} וגובה {{height}}.",
@@ -1019,6 +1020,7 @@
"attendees": "משתתפים",
"add_attendees": "הוסף משתתפים",
"show_advanced_settings": "הצגת הגדרות מתקדמות",
"hide_advanced_settings": "הסתרת הגדרות מתקדמות",
"event_name": "שם האירוע",
"event_name_in_calendar": "שם הארוע בלוח השנה",
"event_name_tooltip": "השם שיופיע בלוחות השנה",
@@ -1231,7 +1233,7 @@
"contact_sales": "פנה למחלקת המכירות",
"error_404": "שגיאת 404",
"default": "ברירת מחדל",
"set_to_default": "הגדר לברירת המחדל",
"set_to_default": "הגדרה כברירת מחדל",
"new_schedule_btn": "תזמון חדש",
"add_new_schedule": "הוסף מועד חדש",
"add_new_calendar": "הוספת לוח שנה חדש",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "כשההגדרה הזו מופעלת, חברי הצוות שלך לא יוכלו לראות חברי צוות אחרים.",
"you_cannot_see_team_members": "אין לך אפשרות לראות את כל חברי הצוות של צוות פרטי.",
"you_cannot_see_teams_of_org": "אי אפשר לצפות בצוותים של ארגון פרטי.",
"allow_booker_to_select_duration": "אפשר למזמין לבחור את משך הזמן",
"allow_multiple_durations": "אפשר משכי זמן מרובים",
"impersonate_user_tip": "כל השימושים בתכונה זו נבדקים.",
"impersonating_user_warning": "בהתחזות לשם המשתמש \"{{user}}\".",
"impersonating_stop_instructions": "לחץ/י כאן כדי להפסיק",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "אישור ההזמנה נכשל",
"not_enough_seats": "אין מספיק מושבים",
"form_builder_field_already_exists": "שדה עם שם זה כבר קיים",
"guests_field_must_be_multiemail": "שדה האורחים חייב להיות מסוג 'מספר אימיילים'",
"show_on_booking_page": "להציג בדף ההזמנות",
"visit_cancelled_booking": "ניתן לבקר בדף ההזמנה שבוטלה",
"get_started_zapier_templates": "התחל עם תבניות Zapier",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "ננעל על ידי מנהל/ת הצוות",
"app_not_connected": "לא קישרת חשבון {{appName}}.",
"connect_now": "להתחבר עכשיו",
"connect_now_unavailable_tooltip": "התחברות מיידית אינה זמינה כרגע. אנא הזמן מהמשבצות למעלה או נסה שוב מאוחר יותר.",
"managed_event_dialog_confirm_button_one": "החלפה ועדכון של חבר/ה {{count}}",
"managed_event_dialog_confirm_button_other": "החלפה ועדכון של {{count}} חברים",
"managed_event_dialog_title_one": "כתובת ה-URL /‏{{slug}} כבר קיימת עבור חבר/ה {{count}}. האם ברצונך להחליף אותה?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "בהמשך התהליך, הנך מסכים/ה ל",
"terms": "תנאי השימוש",
"always_show_x_days": "תמיד {{x}} ימים זמינים",
"always_show_x_days_description": "מזיז אוטומטית את חלון ההזמנות של {{x}} ימים קדימה בכל יום, ומבטיח שהלוח שלך תמיד מציג את {{x}} הימים הבאים של משבצות זמינות.",
"team_subscription_cancelled_successfully": "מנוי הצוות בוטל בהצלחה",
"team_subscription_cancellation_error": "שגיאה בעת ביטול מנוי הצוות",
"unable_to_subscribe_to_the_platform": "אירעה שגיאה בעת ניסיון להירשם לתוכנית הפלטפורמה, אנא נסה שוב מאוחר יותר",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "מסתיים ב",
"filter_operator_is_empty": "ריק",
"filter_operator_not_empty": "לא ריק",
"translate_description_button": "תרגם כותרת/תיאור לשפת הדפדפן של המבקר באמצעות בינה מלאכותית",
"translate_description_button": "תרגום אוטומטי של כותרת ותיאור",
"translate_description_button_description": "תרגם אוטומטית כותרות ותיאורים לשפת הדפדפן של המבקר באמצעות בינה מלאכותית.",
"rr_distribution_method": "שיטת חלוקה",
"rr_distribution_method_description": "מאפשר אופטימיזציה של החלוקה למקסימום זמינות או לחלוקה מאוזנת יותר",
"rr_distribution_method_availability_title": "מקסום זמינות",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "תכונת המושבים אינה תומכת באירוע חוזר. השבת את תכונת המושבים כדי להפוך אותו לחוזר.",
"recurring_event_seats_error": "אירוע חוזר אינו תומך בתכונת המושבים. השבת את תכונת המושבים או הפוך את האירוע ללא חוזר.",
"segment": "מקטע",
"saved_filters": "מסננים שמורים",
"rename": "שנה שם",
"rename_segment": "שנה שם מקטע",
"delete_segment": "מחק מקטע",
@@ -3338,6 +3345,7 @@
"picklist": "רשימת בחירה",
"most_cancelled_bookings": "הכי הרבה ביטולים",
"interface_language": "שפת ממשק",
"interface_language_description": "הגדר את השפה המועדפת עליך לממשק ההזמנות",
"visitors_browser_language": "שפת הדפדפן של המבקר",
"name_or_email": "שם או אימייל",
"salesforce_round_robin_skip_fallback_to_lead_owner": "אם לא נמצא איש קשר, חזור לבעל הליד אם הוא קיים",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "עריכה ושינוי הגדרות תהליכים",
"pbac_desc_delete_workflows": "הסרת תהליכים מהמערכת",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "זמינות",
"pbac_resource_out_of_office": "מחוץ למשרד",
"pbac_desc_create_webhooks": "יצירת רכיבי webhook",
"pbac_desc_view_webhooks": "צפייה ברכיבי webhook",
"pbac_desc_update_webhooks": "עדכון webhooks",
"pbac_desc_delete_webhooks": "מחיקת webhooks",
"pbac_desc_create_availability": "יצירת זמינות",
"pbac_desc_view_availability": "צפייה בזמינות",
"pbac_desc_update_availability": "עדכון זמינות",
"pbac_desc_delete_availability": "מחיקת זמינות",
"pbac_desc_create_out_of_office": "יצירת מצב מחוץ למשרד",
"pbac_desc_view_out_of_office": "צפייה במצב מחוץ למשרד",
"pbac_desc_update_out_of_office": "עדכון מצב מחוץ למשרד",
"pbac_desc_delete_out_of_office": "מחיקת מצב מחוץ למשרד",
"pbac_desc_manage_workflows": "גישת ניהול מלאה לכל התהליכים",
"pbac_desc_create_event_types": "יצירת סוגי אירועים",
"pbac_desc_view_event_types": "צפייה בסוגי אירועים",
+22 -4
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Titkos megbeszélés",
"login_instead": "Jelentkezzen be helyette",
"create_account": "Fiók létrehozása",
"get_started": "Kezdjük el",
"confirm_password": "Jelszó megerősítése",
"reset_your_password": "Állítsa be új jelszavát az e-mail címére küldött utasítások alapján.",
"org_banner_instructions": "Kérjük, töltsön fel {{width}} szélességű és {{height}} magasságú képet.",
@@ -1019,6 +1020,7 @@
"attendees": "Résztvevők",
"add_attendees": "Résztvevők hozzáadása",
"show_advanced_settings": "Speciális beállítások megjelenítése",
"hide_advanced_settings": "Speciális beállítások elrejtése",
"event_name": "Esemény neve",
"event_name_in_calendar": "Eseménynévvel",
"event_name_tooltip": "A naptárban megjelenő név",
@@ -1231,7 +1233,7 @@
"contact_sales": "Lépjen kapcsolatba az értékesítéssel",
"error_404": "404-es hiba",
"default": "Alapértelmezett",
"set_to_default": "Állítsa alapértelmezettre",
"set_to_default": "Beállítás alapértelmezettként",
"new_schedule_btn": "Új ütemezés",
"add_new_schedule": "Új ütemezés hozzáadása",
"add_new_calendar": "Új naptár hozzáadása",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Ha ez be van kapcsolva, a csapattagok nem láthatják a többi csapattagot.",
"you_cannot_see_team_members": "Nem láthatja a privát csapat összes csapattagját.",
"you_cannot_see_teams_of_org": "Nem láthatja a magánszervezetek csapatait.",
"allow_booker_to_select_duration": "Engedélyezze a foglalónak az időtartam kiválasztását",
"allow_multiple_durations": "Több időtartam engedélyezése",
"impersonate_user_tip": "Ennek a funkciónak minden használata auditált.",
"impersonating_user_warning": "\"{{user}}\" felhasználónév megszemélyesítése.",
"impersonating_stop_instructions": "Kattintson ide a leállításhoz",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "A foglalás megerősítése nem sikerült",
"not_enough_seats": "Nincs elég hely",
"form_builder_field_already_exists": "Már létezik ilyen nevű mező",
"guests_field_must_be_multiemail": "A vendégek mezőnek 'Több e-mail cím' típusúnak kell lennie",
"show_on_booking_page": "Megjelenítés a foglalási oldalon",
"visit_cancelled_booking": "Megtekintheti a lemondott foglalás oldalát",
"get_started_zapier_templates": "Kezdje el a Zapier-sablonokat",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Csapatadminisztrátor zárolta",
"app_not_connected": "Nem csatlakoztatott {{appName}}-fiókot.",
"connect_now": "Csatlakozzon most",
"connect_now_unavailable_tooltip": "A Csatlakozás most funkció jelenleg nem elérhető. Kérjük, foglaljon időpontot a fenti időpontokból, vagy próbálja újra később.",
"managed_event_dialog_confirm_button_one": "{{count}} tag cseréje és értesítése",
"managed_event_dialog_confirm_button_other": "{{count}} tagok cseréje és értesítése",
"managed_event_dialog_title_one": "A /{{slug}} URL már létezik {{count}} tagok számára. Ki akarod cserélni?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "A folytatással elfogadod a",
"terms": "Feltételeinket",
"always_show_x_days": "Mindig {{x}} nap áll rendelkezésre",
"always_show_x_days_description": "Automatikusan előrelépteti a {{x}} napos foglalási ablakot minden nap, így a naptárad mindig a következő {{x}} nap elérhető időpontjait mutatja.",
"team_subscription_cancelled_successfully": "A csapat előfizetése sikeresen lemondva",
"team_subscription_cancellation_error": "Hiba történt a csapat előfizetésének lemondása közben",
"unable_to_subscribe_to_the_platform": "Hiba történt a platformcsomagra való előfizetés során. Kérjük, próbálja újra később",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Ezzel végződik",
"filter_operator_is_empty": "Üres",
"filter_operator_not_empty": "Nem üres",
"translate_description_button": "A cím/leírás automatikus fordítása a látogató böngészőjének nyelvére MI segítségével",
"translate_description_button": "Cím és leírás automatikus fordítása",
"translate_description_button_description": "Automatikusan lefordítja a címeket és leírásokat a látogató böngészőjének nyelvére mesterséges intelligencia segítségével.",
"rr_distribution_method": "Elosztás",
"rr_distribution_method_description": "Lehetővé teszi az elosztás optimalizálását a maximális rendelkezésre állás érdekében, vagy a kiegyensúlyozottabb feladatkiosztás eléréséhez.",
"rr_distribution_method_availability_title": "Rendelkezésre állás maximalizálása",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "A helyek funkció nem támogatja az ismétlődő eseményeket. Kapcsolja ki a helyek funkciót az ismétlődés engedélyezéséhez.",
"recurring_event_seats_error": "Az ismétlődő esemény nem támogatja a helyek funkciót. Kapcsolja ki a helyek funkciót vagy állítsa be nem ismétlődőre az eseményt.",
"segment": "Szegmens",
"saved_filters": "Mentett szűrők",
"rename": "Átnevezés",
"rename_segment": "Szegmens átnevezése",
"delete_segment": "Szegmens törlése",
@@ -3338,6 +3345,7 @@
"picklist": "Választólista",
"most_cancelled_bookings": "Legtöbb lemondott foglalás",
"interface_language": "Felhasználói felület nyelve",
"interface_language_description": "Állítsd be a foglalási felület preferált nyelvét",
"visitors_browser_language": "Látogató böngészőjének nyelve",
"name_or_email": "Név vagy e-mail",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Ha nem található kapcsolat, visszalépés a lead tulajdonoshoz, ha létezik",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Munkafolyamat-beállítások szerkesztése és módosítása",
"pbac_desc_delete_workflows": "Munkafolyamatok eltávolítása a rendszerből",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Elérhetőség",
"pbac_resource_out_of_office": "Irodán kívül",
"pbac_desc_create_webhooks": "Webhookok létrehozása",
"pbac_desc_view_webhooks": "Webhookok megtekintése",
"pbac_desc_update_webhooks": "Webhookok frissítése",
"pbac_desc_delete_webhooks": "Webhookok törlése",
"pbac_desc_create_availability": "Elérhetőség létrehozása",
"pbac_desc_view_availability": "Elérhetőség megtekintése",
"pbac_desc_update_availability": "Elérhetőség frissítése",
"pbac_desc_delete_availability": "Elérhetőség törlése",
"pbac_desc_create_out_of_office": "Irodán kívüli állapot létrehozása",
"pbac_desc_view_out_of_office": "Irodán kívüli állapot megtekintése",
"pbac_desc_update_out_of_office": "Irodán kívüli állapot frissítése",
"pbac_desc_delete_out_of_office": "Irodán kívüli állapot törlése",
"pbac_desc_manage_workflows": "Teljes kezelési hozzáférés minden munkafolyamathoz",
"pbac_desc_create_event_types": "Eseménytípusok létrehozása",
"pbac_desc_view_event_types": "Eseménytípusok megtekintése",
@@ -3561,7 +3579,7 @@
"round_robin_settings_description": "Testreszabhatja az alapértelmezett körforgó beállításokat ehhez a csapathoz",
"round_robin_settings_updated_successfully": "A körforgó beállítások sikeresen frissítve",
"booking_creation_time": "Foglalás létrehozásának ideje",
"meeting_start_time": "Találkozó kezdési ideje",
"meeting_start_time": "Megbeszélés kezdési ideje",
"members_affected_by_disabling_delegation_credential": "Érintett tagok",
"no_members_affected_by_disabling_delegation_credential": "Nincsenek tagok, akiket érintene a delegálási hitelesítő adatok letiltása",
"download_expense_log": "Költségnapló letöltése",
+24 -6
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Riunione Segreta",
"login_instead": "Effettua il login invece",
"create_account": "Crea Account",
"get_started": "Inizia",
"confirm_password": "Conferma password",
"reset_your_password": "Imposta la nuova password seguendo le istruzioni che ti sono state inviate via e-mail.",
"org_banner_instructions": "Carica un'immagine larga {{width}} e alta {{height}}.",
@@ -958,7 +959,7 @@
"light_event_type_color": "Colore del tipo di evento (Tema chiaro)",
"dark_event_type_color": "Colore del tipo di evento (Tema scuro)",
"file_not_named": "Il file non è chiamato [idOrSlug]/[user]",
"create_team": "Crea Team",
"create_team": "Crea team",
"name": "Nome",
"nameless_team": "Team senza nome",
"oauth_clients": "Client OAuth",
@@ -1019,6 +1020,7 @@
"attendees": "Partecipanti",
"add_attendees": "Aggiungi partecipanti",
"show_advanced_settings": "Mostra impostazioni avanzate",
"hide_advanced_settings": "Nascondi impostazioni avanzate",
"event_name": "Nome Dell'Evento",
"event_name_in_calendar": "Nome dell'evento nel calendario",
"event_name_tooltip": "Il nome che apparirà nei calendari",
@@ -1231,7 +1233,7 @@
"contact_sales": "Contatta reparto commerciale",
"error_404": "Errore 404",
"default": "Predefinito",
"set_to_default": "Impostazione predefinita",
"set_to_default": "Imposta come predefinito",
"new_schedule_btn": "Nuovo programma",
"add_new_schedule": "Aggiungi un nuovo programma",
"add_new_calendar": "Aggiungi un nuovo calendario",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Quando questa opzione è attivata, i membri del tuo team non potranno vedere gli altri membri del team.",
"you_cannot_see_team_members": "Non è possibile vedere tutti i membri di un team privato.",
"you_cannot_see_teams_of_org": "Non puoi vedere i team di un'organizzazione privata.",
"allow_booker_to_select_duration": "Consenti alla persona che prenota di selezionare la durata",
"allow_multiple_durations": "Consenti durate multiple",
"impersonate_user_tip": "Ogni utilizzo di questa funzionalità viene controllato.",
"impersonating_user_warning": "Accesso con l'identità dell'utente \"{{user}}\".",
"impersonating_stop_instructions": "Fai clic qui per interrompere",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Conferma prenotazione non riuscita",
"not_enough_seats": "Posti insufficienti",
"form_builder_field_already_exists": "Esiste già un campo con questo nome",
"guests_field_must_be_multiemail": "Il campo ospiti deve essere di tipo 'Email multiple'",
"show_on_booking_page": "Mostra nella pagina di prenotazione",
"visit_cancelled_booking": "Puoi visitare la pagina della prenotazione annullata",
"get_started_zapier_templates": "Inizia con i modelli Zapier",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Bloccato dall'amministratore del team",
"app_not_connected": "Non è stato connesso un account di {{appName}}.",
"connect_now": "Connetti ora",
"connect_now_unavailable_tooltip": "Connetti ora è attualmente non disponibile. Prenota dagli slot sopra o riprova più tardi.",
"managed_event_dialog_confirm_button_one": "Sostituisci e invia notifica a {{count}} membro",
"managed_event_dialog_confirm_button_other": "Sostituisci e invia notifica a {{count}} membri",
"managed_event_dialog_title_one": "L'URL /{{slug}} esiste già per {{count}} membro. Vuoi sostituirlo?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Procedendo, accetti i nostri",
"terms": "Termini",
"always_show_x_days": "Sempre {{x}} giorni disponibili",
"always_show_x_days_description": "Sposta automaticamente avanti ogni giorno la finestra di prenotazione di {{x}} giorni, assicurando che il tuo calendario mostri sempre i prossimi {{x}} giorni di slot disponibili.",
"team_subscription_cancelled_successfully": "Abbonamento del team annullato con successo",
"team_subscription_cancellation_error": "Errore durante l'annullamento dell'abbonamento del team",
"unable_to_subscribe_to_the_platform": "Si è verificato un errore durante il tentativo di abbonamento al piano Platform, riprova più tardi",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Finisce con",
"filter_operator_is_empty": "È vuoto",
"filter_operator_not_empty": "Non è vuoto",
"translate_description_button": "Traduci titolo/descrizione nella lingua del browser del visitatore usando l'IA",
"translate_description_button": "Auto traduci titolo e descrizione",
"translate_description_button_description": "Traduci automaticamente titoli e descrizioni nella lingua del browser del visitatore utilizzando l'IA.",
"rr_distribution_method": "Distribuzione",
"rr_distribution_method_description": "Permette di ottimizzare la distribuzione per massimizzare la disponibilità o per ottenere un'assegnazione più bilanciata.",
"rr_distribution_method_availability_title": "Massimizza disponibilità",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "La funzionalità dei posti non supporta eventi ricorrenti. Disabilita la funzionalità dei posti per renderlo ricorrente.",
"recurring_event_seats_error": "L'evento ricorrente non supporta la funzionalità dei posti. Disabilita la funzionalità dei posti o rendi l'evento non ricorrente.",
"segment": "Segmento",
"saved_filters": "Filtri salvati",
"rename": "Rinomina",
"rename_segment": "Rinomina segmento",
"delete_segment": "Elimina segmento",
@@ -3338,6 +3345,7 @@
"picklist": "Lista di selezione",
"most_cancelled_bookings": "Prenotazioni più cancellate",
"interface_language": "Lingua dell'interfaccia",
"interface_language_description": "Imposta la tua lingua preferita per l'interfaccia di prenotazione",
"visitors_browser_language": "Lingua del browser del visitatore",
"name_or_email": "Nome o email",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Se non viene trovato alcun contatto, passa al proprietario del lead se esiste",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Modifica e cambia le impostazioni dei flussi di lavoro",
"pbac_desc_delete_workflows": "Rimuovi flussi di lavoro dal sistema",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Disponibilità",
"pbac_resource_out_of_office": "Fuori sede",
"pbac_desc_create_webhooks": "Crea webhook",
"pbac_desc_view_webhooks": "Visualizza webhook",
"pbac_desc_update_webhooks": "Aggiorna webhook",
"pbac_desc_delete_webhooks": "Elimina webhook",
"pbac_desc_create_availability": "Crea disponibilità",
"pbac_desc_view_availability": "Visualizza disponibilità",
"pbac_desc_update_availability": "Aggiorna disponibilità",
"pbac_desc_delete_availability": "Elimina disponibilità",
"pbac_desc_create_out_of_office": "Crea fuori sede",
"pbac_desc_view_out_of_office": "Visualizza fuori sede",
"pbac_desc_update_out_of_office": "Aggiorna fuori sede",
"pbac_desc_delete_out_of_office": "Elimina fuori sede",
"pbac_desc_manage_workflows": "Accesso completo alla gestione di tutti i flussi di lavoro",
"pbac_desc_create_event_types": "Crea tipi di evento",
"pbac_desc_view_event_types": "Visualizza tipi di evento",
@@ -3560,8 +3578,8 @@
"rr_load_balancing_disabled_with_groups": "Il bilanciamento del carico non è disponibile con i gruppi Round Robin",
"round_robin_settings_description": "Personalizza le impostazioni predefinite del round robin per questo team",
"round_robin_settings_updated_successfully": "Impostazioni Round Robin aggiornate con successo",
"booking_creation_time": "Orario di creazione della prenotazione",
"meeting_start_time": "Orario di inizio della riunione",
"booking_creation_time": "Orario di creazione prenotazione",
"meeting_start_time": "Orario di inizio meeting",
"members_affected_by_disabling_delegation_credential": "Membri interessati",
"no_members_affected_by_disabling_delegation_credential": "Nessun membro interessato dalla disabilitazione delle credenziali di delega",
"download_expense_log": "Scarica il registro delle spese",
+20 -2
View File
@@ -321,6 +321,7 @@
"secret_meeting": "秘密のミーティング",
"login_instead": "代わりにログインする",
"create_account": "アカウントを作成",
"get_started": "始める",
"confirm_password": "パスワードを確認",
"reset_your_password": "メールアドレスに送信された手順に従って新しいパスワードを設定します。",
"org_banner_instructions": "{{width}}幅と{{height}}高さの画像をアップロードしてください。",
@@ -1019,6 +1020,7 @@
"attendees": "出席者",
"add_attendees": "出席者を追加",
"show_advanced_settings": "詳細設定を表示",
"hide_advanced_settings": "詳細設定を隠す",
"event_name": "イベント名",
"event_name_in_calendar": "カレンダー内のイベント名",
"event_name_tooltip": "カレンダーに表示される名前",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "これをオンにすると、チームメンバーは他のチームメンバーを確認することができなくなります。",
"you_cannot_see_team_members": "非公開チームでは、チームメンバーを確認することができません。",
"you_cannot_see_teams_of_org": "非公開の組織のチームを見ることはできません。",
"allow_booker_to_select_duration": "予約者が時間を選択できるようにする",
"allow_multiple_durations": "複数の時間枠を許可",
"impersonate_user_tip": "この機能の使用はすべて監査されています。",
"impersonating_user_warning": "ユーザー名 \"{{user}}\" になりすましています。",
"impersonating_stop_instructions": "こちらをクリックして停止",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "予約の確認に失敗しました",
"not_enough_seats": "座席が不足しています",
"form_builder_field_already_exists": "この名前のフィールドは既に存在します",
"guests_field_must_be_multiemail": "ゲストフィールドは「複数のメール」タイプである必要があります",
"show_on_booking_page": "予約ページに表示",
"visit_cancelled_booking": "キャンセルされた予約ページを訪問できます",
"get_started_zapier_templates": "Zapier テンプレートの使用を開始する",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "チームの管理者によりロックされています",
"app_not_connected": "{{appName}} のアカウントに接続していません。",
"connect_now": "今すぐ接続",
"connect_now_unavailable_tooltip": "今すぐ接続は現在利用できません。上記の空き枠から予約するか、後でもう一度お試しください。",
"managed_event_dialog_confirm_button_one": "{{count}} 人のメンバーを置き換えて通知する",
"managed_event_dialog_confirm_button_other": "{{count}} 人のメンバーを置き換えて通知する",
"managed_event_dialog_title_one": "{{count}} 人のメンバーがすでに URL /{{slug}} を使用しています。置き換えますか?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "続行することで、当社の",
"terms": "利用規約",
"always_show_x_days": "常に{{x}}日間利用可能",
"always_show_x_days_description": "予約ウィンドウを毎日自動的に{{x}}日分前進させ、カレンダーに常に今後{{x}}日間の空き枠を表示します。",
"team_subscription_cancelled_successfully": "チームサブスクリプションが正常にキャンセルされました",
"team_subscription_cancellation_error": "チームサブスクリプションのキャンセル中にエラーが発生しました",
"unable_to_subscribe_to_the_platform": "プラットフォームプランの登録中にエラーが発生しました。後でもう一度お試しください。",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "で終わる",
"filter_operator_is_empty": "が空である",
"filter_operator_not_empty": "が空ではない",
"translate_description_button": "AIを使用して訪問者のブラウザ言語にタイトル/説明を翻訳",
"translate_description_button": "タイトルと説明を自動翻訳",
"translate_description_button_description": "AIを使用して、訪問者のブラウザ言語にタイトルと説明を自動的に翻訳します。",
"rr_distribution_method": "配分方式",
"rr_distribution_method_description": "最大限の予約可能性を確保するため、または均等な配分を目指すための最適化が可能です。",
"rr_distribution_method_availability_title": "予約可能性を最大化",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "座席機能は定期的なイベントをサポートしていません。定期的にするには座席機能を無効にしてください。",
"recurring_event_seats_error": "定期的なイベントは座席機能をサポートしていません。座席機能を無効にするか、イベントを非定期的にしてください。",
"segment": "セグメント",
"saved_filters": "保存済みフィルター",
"rename": "名前を変更",
"rename_segment": "セグメント名を変更",
"delete_segment": "セグメントを削除",
@@ -3338,6 +3345,7 @@
"picklist": "選択リスト",
"most_cancelled_bookings": "最もキャンセルが多い",
"interface_language": "インターフェース言語",
"interface_language_description": "予約インターフェースの希望言語を設定します",
"visitors_browser_language": "訪問者のブラウザ言語",
"name_or_email": "名前またはメールアドレス",
"salesforce_round_robin_skip_fallback_to_lead_owner": "連絡先が見つからない場合、リード所有者が存在すればそちらにフォールバックする",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "ワークフロー設定を編集および変更する",
"pbac_desc_delete_workflows": "システムからワークフローを削除する",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "空き状況",
"pbac_resource_out_of_office": "不在",
"pbac_desc_create_webhooks": "Webhookを作成",
"pbac_desc_view_webhooks": "Webhookを表示",
"pbac_desc_update_webhooks": "ウェブフックを更新する",
"pbac_desc_delete_webhooks": "ウェブフックを削除する",
"pbac_desc_create_availability": "空き状況を作成",
"pbac_desc_view_availability": "空き状況を表示",
"pbac_desc_update_availability": "空き状況を更新",
"pbac_desc_delete_availability": "空き状況を削除",
"pbac_desc_create_out_of_office": "不在を作成",
"pbac_desc_view_out_of_office": "不在を表示",
"pbac_desc_update_out_of_office": "不在を更新",
"pbac_desc_delete_out_of_office": "不在を削除",
"pbac_desc_manage_workflows": "すべてのワークフローへの完全な管理アクセス",
"pbac_desc_create_event_types": "イベントタイプを作成する",
"pbac_desc_view_event_types": "イベントタイプを表示する",
+21 -3
View File
@@ -321,6 +321,7 @@
"secret_meeting": "ការជួបជុំសម្ងាត់",
"login_instead": "ចូលប្រើប្រាស់វិញ",
"create_account": "បង្កើតគណនី",
"get_started": "ចាប់ផ្តើម",
"confirm_password": "បញ្ជាក់ពាក្យសម្ងាត់",
"reset_your_password": "កំណត់ពាក្យសម្ងាត់ថ្មីរបស់អ្នកដោយអនុវត្តតាមសេចក្តីណែនាំដែលបានផ្ញើទៅអាសយដ្ឋានអ៊ីមែលរបស់អ្នក។",
"org_banner_instructions": "សូមផ្ទុករូបភាពដែលមានទទឹង {{width}} និងកម្ពស់ {{height}}។",
@@ -1019,6 +1020,7 @@
"attendees": "អ្នកចូលរួម",
"add_attendees": "បន្ថែមអ្នកចូលរួម",
"show_advanced_settings": "បង្ហាញការកំណត់កម្រិតខ្ពស់",
"hide_advanced_settings": "លាក់ការកំណត់កម្រិតខ្ពស់",
"event_name": "ឈ្មោះព្រឹត្តិការណ៍",
"event_name_in_calendar": "ជាមួយឈ្មោះព្រឹត្តិការណ៍",
"event_name_tooltip": "ឈ្មោះដែលនឹងបង្ហាញនៅក្នុងប្រតិទិន",
@@ -1231,7 +1233,7 @@
"contact_sales": "ទាក់ទងផ្នែកលក់",
"error_404": "កំហុស 404",
"default": "លំនាំដើម",
"set_to_default": "កំណត់ទៅលំនាំដើម",
"set_to_default": "កំណត់ជាលំនាំដើម",
"new_schedule_btn": "កាលវិភាគថ្មី",
"add_new_schedule": "បន្ថែមកាលវិភាគថ្មី",
"add_new_calendar": "បន្ថែមប្រតិទិនថ្មី",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "សមាជិកក្រុមរបស់អ្នកនឹងមិនអាចមើលឃើញសមាជិកក្រុមផ្សេងទៀតនៅពេលដែលបើកមុខងារនេះ។",
"you_cannot_see_team_members": "អ្នកមិនអាចមើលឃើញសមាជិកទាំងអស់នៃក្រុមសម្ងាត់បានទេ។",
"you_cannot_see_teams_of_org": "អ្នកមិនអាចមើលឃើញក្រុមនៃអង្គការសម្ងាត់បានទេ។",
"allow_booker_to_select_duration": "អនុញ្ញាតឱ្យអ្នកកក់ជ្រើសរើសរយៈពេល",
"allow_multiple_durations": "អនុញ្ញាតឱ្យមានរយៈពេលច្រើន",
"impersonate_user_tip": "ការប្រើប្រាស់មុខងារនេះទាំងអស់ត្រូវបានកត់ត្រា។",
"impersonating_user_warning": "កំពុងសម្តែងជាអ្នកប្រើប្រាស់ \"{{user}}\"។",
"impersonating_stop_instructions": "ចុចទីនេះដើម្បីបញ្ឈប់",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "ការបញ្ជាក់ការកក់បរាជ័យ",
"not_enough_seats": "កៅអីមិនគ្រប់គ្រាន់",
"form_builder_field_already_exists": "វាលដែលមានឈ្មោះនេះមានរួចហើយ",
"guests_field_must_be_multiemail": "វាលភ្ញៀវត្រូវតែជាប្រភេទ 'អ៊ីមែលច្រើន'",
"show_on_booking_page": "បង្ហាញនៅលើទំព័រការកក់",
"visit_cancelled_booking": "អ្នកអាចចូលទៅកាន់ទំព័រការកក់ដែលបានលុបចោល",
"get_started_zapier_templates": "ចាប់ផ្តើមជាមួយពុម្ព Zapier",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "បានចាក់សោដោយអ្នកគ្រប់គ្រងក្រុម",
"app_not_connected": "អ្នកមិនបានភ្ជាប់គណនី {{appName}} ទេ។",
"connect_now": "ភ្ជាប់ឥឡូវនេះ",
"connect_now_unavailable_tooltip": "ការភ្ជាប់ឥឡូវនេះមិនអាចប្រើបានទេ។ សូមកក់ពីពេលវេលាខាងលើ ឬព្យាយាមម្តងទៀតនៅពេលក្រោយ។",
"managed_event_dialog_confirm_button_one": "ជំនួស និងជូនដំណឹងសមាជិក {{count}}",
"managed_event_dialog_confirm_button_other": "ជំនួស និងជូនដំណឹងសមាជិក {{count}} នាក់",
"managed_event_dialog_title_one": "URL /{{slug}} មានស្រាប់សម្រាប់សមាជិក {{count}}។ តើអ្នកចង់ជំនួសវាឬ?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "ដោយការបន្ត អ្នកយល់ព្រមនឹង",
"terms": "លក្ខខណ្ឌ",
"always_show_x_days": "តែងតែបង្ហាញ {{x}} ថ្ងៃដែលអាចប្រើបាន",
"always_show_x_days_description": "ផ្លាស់ទីបង្អួចកក់រយៈពេល {{x}} ថ្ងៃដោយស្វ័យប្រវត្តិរៀងរាល់ថ្ងៃ ដើម្បីធានាថាប្រតិទិនរបស់អ្នកតែងតែបង្ហាញពេលវេលាដែលអាចប្រើបានសម្រាប់ {{x}} ថ្ងៃបន្ទាប់។",
"team_subscription_cancelled_successfully": "បានលុបចោលការជាវរបស់ក្រុមដោយជោគជ័យ",
"team_subscription_cancellation_error": "មានបញ្ហាពេលលុបចោលការជាវរបស់ក្រុម",
"unable_to_subscribe_to_the_platform": "មានកំហុសមួយបានកើតឡើងខណៈពេលព្យាយាមជាវផែនការក្នុងវេទិកា សូមព្យាយាមម្តងទៀតនៅពេលក្រោយ",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "បញ្ចប់ដោយ",
"filter_operator_is_empty": "ទទេ",
"filter_operator_not_empty": "មិនទទេ",
"translate_description_button": "បកប្រែចំណងជើង/ការពិពណ៌នាទៅជាភាសាកម្មវិធីរុករករបស់ភ្ញៀវដោយប្រើបញ្ញាសិប្បនិម្មិត",
"translate_description_button": "បកប្រែចំណងជើងនិងការពិពណ៌នាដោយស្វ័យប្រវត្តិ",
"translate_description_button_description": "បកប្រែចំណងជើងនិងការពិពណ៌នាដោយស្វ័យប្រវត្តិទៅជាភាសាកម្មវិធីរុករករបស់អ្នកទស្សនាដោយប្រើ AI។",
"rr_distribution_method": "ការចែកចាយ",
"rr_distribution_method_description": "អនុញ្ញាតឱ្យធ្វើការបែងចែកប្រសិទ្ធភាពដើម្បីបង្កើនភាពអាចរកបានអតិបរមា ឬដើម្បីសម្រេចបាននូវការចាត់តាំងដែលមានតុល្យភាពជាងមុន។",
"rr_distribution_method_availability_title": "បង្កើនភាពអាចរកបាន",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "មុខងារកន្លែងអង្គុយមិនគាំទ្រព្រឹត្តិការណ៍ដែលកើតឡើងជាប្រចាំទេ។ បិទមុខងារកន្លែងអង្គុយដើម្បីធ្វើឱ្យវាកើតឡើងជាប្រចាំ។",
"recurring_event_seats_error": "ព្រឹត្តិការណ៍ដែលកើតឡើងជាប្រចាំមិនគាំទ្រមុខងារកន្លែងអង្គុយទេ។ បិទមុខងារកន្លែងអង្គុយឬធ្វើឱ្យព្រឹត្តិការណ៍មិនកើតឡើងជាប្រចាំ។",
"segment": "ផ្នែក",
"saved_filters": "តម្រងដែលបានរក្សាទុក",
"rename": "ប្តូរឈ្មោះ",
"rename_segment": "ប្តូរឈ្មោះផ្នែក",
"delete_segment": "លុបផ្នែក",
@@ -3338,6 +3345,7 @@
"picklist": "បញ្ជីជម្រើស",
"most_cancelled_bookings": "ការកក់ដែលត្រូវបានលុបចោលច្រើនបំផុត",
"interface_language": "ភាសាចំណុចប្រទាក់",
"interface_language_description": "កំណត់ភាសាដែលអ្នកចូលចិត្តសម្រាប់ចំណុចប្រទាក់នៃការកក់",
"visitors_browser_language": "ភាសាកម្មវិធីរុករករបស់អ្នកទស្សនា",
"name_or_email": "ឈ្មោះ ឬអ៊ីមែល",
"salesforce_round_robin_skip_fallback_to_lead_owner": "ប្រសិនបើមិនមានទំនាក់ទំនងត្រូវបានរកឃើញទេ ត្រឡប់ទៅម្ចាស់ Lead ប្រសិនបើមាន",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "កែសម្រួលនិងកែប្រែការកំណត់លំហូរការងារ",
"pbac_desc_delete_workflows": "លុបលំហូរការងារចេញពីប្រព័ន្ធ",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "ភាពអាចប្រើបាន",
"pbac_resource_out_of_office": "ក្រៅការិយាល័យ",
"pbac_desc_create_webhooks": "បង្កើត webhooks",
"pbac_desc_view_webhooks": "មើល webhooks",
"pbac_desc_update_webhooks": "ធ្វើបច្ចុប្បន្នភាព webhooks",
"pbac_desc_delete_webhooks": "លុប webhooks",
"pbac_desc_create_availability": "បង្កើតភាពអាចប្រើបាន",
"pbac_desc_view_availability": "មើលភាពអាចប្រើបាន",
"pbac_desc_update_availability": "ធ្វើបច្ចុប្បន្នភាពភាពអាចប្រើបាន",
"pbac_desc_delete_availability": "លុបភាពអាចប្រើបាន",
"pbac_desc_create_out_of_office": "បង្កើតស្ថានភាពក្រៅការិយាល័យ",
"pbac_desc_view_out_of_office": "មើលស្ថានភាពក្រៅការិយាល័យ",
"pbac_desc_update_out_of_office": "ធ្វើបច្ចុប្បន្នភាពស្ថានភាពក្រៅការិយាល័យ",
"pbac_desc_delete_out_of_office": "លុបស្ថានភាពក្រៅការិយាល័យ",
"pbac_desc_manage_workflows": "ការចូលប្រើប្រាស់គ្រប់គ្រងពេញលេញទៅលើលំហូរការងារទាំងអស់",
"pbac_desc_create_event_types": "បង្កើតប្រភេទព្រឹត្តិការណ៍",
"pbac_desc_view_event_types": "មើលប្រភេទព្រឹត្តិការណ៍",
+20 -2
View File
@@ -321,6 +321,7 @@
"secret_meeting": "비밀 회의",
"login_instead": "로그인하십시오",
"create_account": "계정 만들기",
"get_started": "시작하기",
"confirm_password": "비밀번호 확인",
"reset_your_password": "이메일 주소로 전송된 지침에 따라 새 비밀번호를 설정하십시오.",
"org_banner_instructions": "{{width}} 너비와 {{height}} 높이의 이미지를 업로드해 주세요.",
@@ -1019,6 +1020,7 @@
"attendees": "참석자",
"add_attendees": "참석자 추가하기",
"show_advanced_settings": "고급 설정 표시",
"hide_advanced_settings": "고급 설정 숨기기",
"event_name": "이벤트 이름",
"event_name_in_calendar": "캘린더의 이벤트 이름",
"event_name_tooltip": "캘린더에 표시될 이름",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "이 기능을 켜면 팀원이 다른 팀원을 볼 수 없게 됩니다.",
"you_cannot_see_team_members": "비공개 팀의 모든 팀원을 볼 수 없습니다.",
"you_cannot_see_teams_of_org": "비공개 조직의 팀을 볼 수 없습니다.",
"allow_booker_to_select_duration": "예약자가 기간을 선택하도록 허용",
"allow_multiple_durations": "여러 시간 옵션 허용",
"impersonate_user_tip": "이 기능의 사용은 모두 감사됩니다.",
"impersonating_user_warning": "\"{{user}}\"의 사용자 이름을 가장하는 중입니다.",
"impersonating_stop_instructions": "중지하려면 여기를 클릭",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "예약 확인 실패",
"not_enough_seats": "좌석이 부족합니다",
"form_builder_field_already_exists": "이 이름을 가진 필드가 이미 존재합니다",
"guests_field_must_be_multiemail": "게스트 필드는 '여러 이메일' 유형이어야 합니다",
"show_on_booking_page": "예약 페이지에 표시",
"visit_cancelled_booking": "취소된 예약 페이지를 방문할 수 있습니다",
"get_started_zapier_templates": "Zapier 템플릿 시작하기",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "팀 관리자에 의해 잠김",
"app_not_connected": "{{appName}} 계정을 연결하지 않았습니다.",
"connect_now": "지금 연결",
"connect_now_unavailable_tooltip": "지금 연결하기는 현재 사용할 수 없습니다. 위의 시간대에서 예약하거나 나중에 다시 시도해 주세요.",
"managed_event_dialog_confirm_button_one": "회원 {{count}}명 바꾸기 및 알림",
"managed_event_dialog_confirm_button_other": "회원 {{count}}명 바꾸기 및 알림",
"managed_event_dialog_title_one": "URL /{{slug}}은(는) {{count}}명의 회원에 대해 이미 존재합니다. 바꾸시겠습니까?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "계속 진행하시면 당사의",
"terms": "이용약관",
"always_show_x_days": "항상 {{x}}일 사용 가능",
"always_show_x_days_description": "매일 {{x}}일 예약 창을 자동으로 앞당겨 캘린더에 항상 다음 {{x}}일의 가용 시간대를 표시합니다.",
"team_subscription_cancelled_successfully": "팀 구독이 성공적으로 취소되었습니다",
"team_subscription_cancellation_error": "팀 구독 취소 중 오류가 발생했습니다",
"unable_to_subscribe_to_the_platform": "플랫폼 플랜 구독 중 오류가 발생했습니다. 나중에 다시 시도해 주세요.",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "다음으로 끝남",
"filter_operator_is_empty": "비어 있음",
"filter_operator_not_empty": "비어 있지 않음",
"translate_description_button": "AI를 사용하여 방문자의 브라우저 언어로 제목/설명 번역하기",
"translate_description_button": "제목 및 설명 자동 번역",
"translate_description_button_description": "AI를 사용하여 방문자의 브라우저 언어로 제목과 설명을 자동 번역합니다.",
"rr_distribution_method": "분배 방식",
"rr_distribution_method_description": "최대 가용성을 위한 분배 최적화 또는 균형 잡힌 할당을 위한 설정이 가능합니다.",
"rr_distribution_method_availability_title": "가용성 최대화",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "좌석 기능은 반복 이벤트를 지원하지 않습니다. 반복으로 설정하려면 좌석 기능을 비활성화하세요.",
"recurring_event_seats_error": "반복 이벤트는 좌석 기능을 지원하지 않습니다. 좌석 기능을 비활성화하거나 이벤트를 비반복으로 설정하세요.",
"segment": "세그먼트",
"saved_filters": "저장된 필터",
"rename": "이름 변경",
"rename_segment": "세그먼트 이름 변경",
"delete_segment": "세그먼트 삭제",
@@ -3338,6 +3345,7 @@
"picklist": "선택 목록",
"most_cancelled_bookings": "가장 많이 취소된 예약",
"interface_language": "인터페이스 언어",
"interface_language_description": "예약 인터페이스의 선호 언어 설정",
"visitors_browser_language": "방문자의 브라우저 언어",
"name_or_email": "이름 또는 이메일",
"salesforce_round_robin_skip_fallback_to_lead_owner": "연락처를 찾을 수 없는 경우, 리드 소유자가 있으면 대체",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "워크플로 설정 편집 및 수정",
"pbac_desc_delete_workflows": "시스템에서 워크플로 제거",
"pbac_resource_webhook": "웹훅",
"pbac_resource_availability": "가용성",
"pbac_resource_out_of_office": "부재중",
"pbac_desc_create_webhooks": "웹훅 생성",
"pbac_desc_view_webhooks": "웹훅 보기",
"pbac_desc_update_webhooks": "웹훅 업데이트",
"pbac_desc_delete_webhooks": "웹훅 삭제",
"pbac_desc_create_availability": "가용성 생성",
"pbac_desc_view_availability": "가용성 보기",
"pbac_desc_update_availability": "가용성 업데이트",
"pbac_desc_delete_availability": "가용성 삭제",
"pbac_desc_create_out_of_office": "부재중 생성",
"pbac_desc_view_out_of_office": "부재중 보기",
"pbac_desc_update_out_of_office": "부재중 업데이트",
"pbac_desc_delete_out_of_office": "부재중 삭제",
"pbac_desc_manage_workflows": "모든 워크플로에 대한 전체 관리 액세스",
"pbac_desc_create_event_types": "이벤트 타입 생성",
"pbac_desc_view_event_types": "이벤트 타입 보기",
+24 -6
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Geheime Afspraak",
"login_instead": "In plaats daarvan inloggen",
"create_account": "Account Aanmaken",
"get_started": "Aan de slag",
"confirm_password": "Wachtwoord bevestigen",
"reset_your_password": "Stel uw nieuwe wachtwoord in met de instructies die u per e-mail zijn toegestuurd.",
"org_banner_instructions": "Upload een afbeelding van {{width}} breedte en {{height}} hoogte.",
@@ -958,7 +959,7 @@
"light_event_type_color": "Kleur van evenementtype (Licht Thema)",
"dark_event_type_color": "Kleur van evenementtype (Donker Thema)",
"file_not_named": "Bestandsnaam is niet [idOrSlug]/[user]",
"create_team": "Maak Team",
"create_team": "Team aanmaken",
"name": "Naam",
"nameless_team": "Naamloos team",
"oauth_clients": "OAuth-clients",
@@ -1019,6 +1020,7 @@
"attendees": "Deelnemers",
"add_attendees": "Deelnemers toevoegen",
"show_advanced_settings": "Toon geavanceerde instellingen",
"hide_advanced_settings": "Geavanceerde instellingen verbergen",
"event_name": "Naam van evenement",
"event_name_in_calendar": "Gebeurtenisnaam in agenda",
"event_name_tooltip": "De naam die in kalenders wordt weergegeven",
@@ -1231,7 +1233,7 @@
"contact_sales": "Contact met Sales",
"error_404": "Fout 404",
"default": "standaard keuze",
"set_to_default": "Zet als standaard keuze",
"set_to_default": "Als standaard instellen",
"new_schedule_btn": "Nieuwe planning",
"add_new_schedule": "Voeg een nieuwe planning toe",
"add_new_calendar": "Voeg een nieuwe agenda toe",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Uw teamleden kunnen geen andere teamleden zien wanneer dit ingeschakeld is.",
"you_cannot_see_team_members": "U kunt niet alle teamleden van een privéteam zien.",
"you_cannot_see_teams_of_org": "Je kunt de teams van een privéorganisatie niet zien.",
"allow_booker_to_select_duration": "Booker toestaan om een duur te selecteren",
"allow_multiple_durations": "Meerdere tijdsduren toestaan",
"impersonate_user_tip": "Alle gebruik van deze functie wordt gecontroleerd.",
"impersonating_user_warning": "Gebruikersnaam \"{{user}}\" wordt geïmiteerd.",
"impersonating_stop_instructions": "Klik hier om te stoppen",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Boekingsbevestiging mislukt",
"not_enough_seats": "Onvoldoende plaatsen",
"form_builder_field_already_exists": "Een veld met deze naam bestaat al",
"guests_field_must_be_multiemail": "Gastveld moet van het type 'Meerdere e-mails' zijn",
"show_on_booking_page": "Weergeven op boekingspagina",
"visit_cancelled_booking": "Je kunt de pagina van de geannuleerde boeking bezoeken",
"get_started_zapier_templates": "Aan de slag met Zapier-sjablonen",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Vergrendeld door teambeheerder",
"app_not_connected": "U hebt geen {{appName}}-account gekoppeld.",
"connect_now": "Nu koppelen",
"connect_now_unavailable_tooltip": "Direct verbinden is momenteel niet beschikbaar. Boek alstublieft via bovenstaande tijdslots of probeer het later opnieuw.",
"managed_event_dialog_confirm_button_one": "{{count}} lid vervangen en informeren",
"managed_event_dialog_confirm_button_other": "{{count}} leden vervangen en informeren",
"managed_event_dialog_title_one": "De URL /{{slug}} bestaat al voor {{count}} lid. Wilt u deze vervangen?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Door verder te gaan ga je akkoord met onze",
"terms": "Voorwaarden",
"always_show_x_days": "Altijd {{x}} dagen beschikbaar",
"always_show_x_days_description": "Verplaatst het {{x}}-daagse boekingsvenster elke dag automatisch vooruit, zodat je agenda altijd de komende {{x}} dagen aan beschikbare tijdslots toont.",
"team_subscription_cancelled_successfully": "Teamabonnement succesvol opgezegd",
"team_subscription_cancellation_error": "Fout bij het opzeggen van het teamabonnement",
"unable_to_subscribe_to_the_platform": "Er is een fout opgetreden bij het proberen te abonneren op het platformplan, probeer het later opnieuw",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Eindigt met",
"filter_operator_is_empty": "Is leeg",
"filter_operator_not_empty": "Is niet leeg",
"translate_description_button": "Vertaal titel/beschrijving naar de browsertaal van de bezoeker met behulp van AI",
"translate_description_button": "Titel en beschrijving automatisch vertalen",
"translate_description_button_description": "Titels en beschrijvingen automatisch vertalen naar de browsertaal van de bezoeker met behulp van AI.",
"rr_distribution_method": "Distributie",
"rr_distribution_method_description": "Maakt het mogelijk om de distributie te optimaliseren voor maximale beschikbaarheid of voor een meer gebalanceerde toewijzing.",
"rr_distribution_method_availability_title": "Maximale beschikbaarheid",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "Stoelfunctie ondersteunt geen terugkerend evenement. Schakel stoelfunctie uit om het terugkerend te maken.",
"recurring_event_seats_error": "Terugkerend evenement ondersteunt geen stoelfunctie. Schakel stoelfunctie uit of maak het evenement niet-terugkerend.",
"segment": "Segment",
"saved_filters": "Opgeslagen filters",
"rename": "Hernoemen",
"rename_segment": "Segment hernoemen",
"delete_segment": "Segment verwijderen",
@@ -3338,6 +3345,7 @@
"picklist": "Keuzelijst",
"most_cancelled_bookings": "Meest geannuleerd",
"interface_language": "Interfacetaal",
"interface_language_description": "Stel je voorkeurstaal in voor de boekingsinterface",
"visitors_browser_language": "Taal van de browser van de bezoeker",
"name_or_email": "Naam of e-mail",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Als er geen contactpersoon wordt gevonden, val dan terug op de lead-eigenaar als deze bestaat",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Werkstroominstellingen bewerken en wijzigen",
"pbac_desc_delete_workflows": "Werkstromen uit het systeem verwijderen",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Beschikbaarheid",
"pbac_resource_out_of_office": "Afwezigheid",
"pbac_desc_create_webhooks": "Webhooks maken",
"pbac_desc_view_webhooks": "Webhooks bekijken",
"pbac_desc_update_webhooks": "Webhooks bijwerken",
"pbac_desc_delete_webhooks": "Webhooks verwijderen",
"pbac_desc_create_availability": "Beschikbaarheid aanmaken",
"pbac_desc_view_availability": "Beschikbaarheid bekijken",
"pbac_desc_update_availability": "Beschikbaarheid bijwerken",
"pbac_desc_delete_availability": "Beschikbaarheid verwijderen",
"pbac_desc_create_out_of_office": "Afwezigheid aanmaken",
"pbac_desc_view_out_of_office": "Afwezigheid bekijken",
"pbac_desc_update_out_of_office": "Afwezigheid bijwerken",
"pbac_desc_delete_out_of_office": "Afwezigheid verwijderen",
"pbac_desc_manage_workflows": "Volledige beheertoegang tot alle werkstromen",
"pbac_desc_create_event_types": "Gebeurtenistypes aanmaken",
"pbac_desc_view_event_types": "Gebeurtenistypes bekijken",
@@ -3560,8 +3578,8 @@
"rr_load_balancing_disabled_with_groups": "Load balancing is niet beschikbaar met round robin groepen",
"round_robin_settings_description": "Pas de standaard round robin-instellingen aan voor dit team",
"round_robin_settings_updated_successfully": "Round Robin-instellingen succesvol bijgewerkt",
"booking_creation_time": "Tijdstip van boekingscreatie",
"meeting_start_time": "Starttijd van de vergadering",
"booking_creation_time": "Aanmaaktijd boeking",
"meeting_start_time": "Starttijd vergadering",
"members_affected_by_disabling_delegation_credential": "Getroffen leden",
"no_members_affected_by_disabling_delegation_credential": "Geen leden getroffen door het uitschakelen van delegatiereferentie",
"download_expense_log": "Download uitgavenlogboek",
+24 -6
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Hemmelig Møte",
"login_instead": "Logg på i stedet",
"create_account": "Opprett Bruker",
"get_started": "Kom i gang",
"confirm_password": "Bekreft passord",
"reset_your_password": "Sett ditt nye passord med instruksjonene sendt til din e-postadresse.",
"org_banner_instructions": "Vennligst last opp et bilde med {{width}} bredde og {{height}} høyde.",
@@ -1019,6 +1020,7 @@
"attendees": "Deltakere",
"add_attendees": "Legg til deltaker",
"show_advanced_settings": "Vis avanserte innstillinger",
"hide_advanced_settings": "Skjul avanserte innstillinger",
"event_name": "Hendelses navn",
"event_name_in_calendar": "Med hendelsesnavn",
"event_name_tooltip": "Navnet som vises i kalenderne",
@@ -1231,7 +1233,7 @@
"contact_sales": "Kontakt Salg",
"error_404": "Feil 404",
"default": "Standard",
"set_to_default": "Sett til Standard",
"set_to_default": "Sett som standard",
"new_schedule_btn": "Ny tidsplan",
"add_new_schedule": "Legg til ny tidsplan",
"add_new_calendar": "Legg til en ny kalender",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Medlemmene i teamet ditt vil ikke kunne se andre teammedlemmer når dette er slått på.",
"you_cannot_see_team_members": "Du kan ikke se alle teammedlemmene i et privat team.",
"you_cannot_see_teams_of_org": "Du kan ikke se teamene til en privat organisasjon.",
"allow_booker_to_select_duration": "La bookeren velge varighet",
"allow_multiple_durations": "Tillat flere varigheter",
"impersonate_user_tip": "All bruk av denne funksjonen er revidert.",
"impersonating_user_warning": "Etterligner brukernavnet \"{{user}}\".",
"impersonating_stop_instructions": "Klikk her for å stoppe",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Booking-bekreftelse feilet",
"not_enough_seats": "Ikke nok seter",
"form_builder_field_already_exists": "Et felt med dette navnet eksisterer allerede",
"guests_field_must_be_multiemail": "Gjestfeltet må være av typen 'Flere e-poster'",
"show_on_booking_page": "Vis på bestillingssiden",
"visit_cancelled_booking": "Du kan besøke siden for avbestilt booking",
"get_started_zapier_templates": "Kom i gang med Zapier-maler",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Låst av teamadministrator",
"app_not_connected": "Du har ikke koblet til en {{appName}}-konto.",
"connect_now": "Koble til nå",
"connect_now_unavailable_tooltip": "Koble til nå er for øyeblikket utilgjengelig. Vennligst bestill fra tilgjengelige tider ovenfor eller prøv igjen senere.",
"managed_event_dialog_confirm_button_one": "Erstatt og varsle {{count}} medlem",
"managed_event_dialog_confirm_button_other": "Erstatt og varsle {{count}} medlemmer",
"managed_event_dialog_title_one": "URL-en /{{slug}} eksisterer allerede for {{count}} medlem. Vil du erstatte den?",
@@ -2688,7 +2692,7 @@
"enterprise": "Bedrift",
"enterprise_description": "Oppgrader til Enterprise for å opprette din organisasjon",
"create_your_org": "Opprett din Organisasjon",
"create_org": "Opprett Organisasjon",
"create_org": "Opprett organisasjon",
"create_your_org_description": "Oppgrader til Organisasjoner og få et underdomene, enhetlig fakturering, innsikt, omfattende whitelabeling og mer",
"create_your_enterprise_description": "Oppgrader til Enterprise og få tilgang til Active Directory Sync, SCIM Automatisk Brukerprovisjonering, Cal.ai Voice Agents, Admin APIer og mer!",
"other_payment_app_enabled": "Du kan kun aktivere én betalingsapp per hendelsestype",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Ved å fortsette godtar du våre",
"terms": "Vilkår",
"always_show_x_days": "Alltid {{x}} dager tilgjengelig",
"always_show_x_days_description": "Flytter automatisk {{x}}-dagers bestillingsvinduet fremover hver dag, slik at kalenderen din alltid viser de neste {{x}} dagene med tilgjengelige tider.",
"team_subscription_cancelled_successfully": "Teamabonnement kansellert",
"team_subscription_cancellation_error": "Feil ved kansellering av teamabonnement",
"unable_to_subscribe_to_the_platform": "En feil oppstod under forsøket på å abonnere på plattformplanen, vennligst prøv igjen senere",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Slutter med",
"filter_operator_is_empty": "Er tom",
"filter_operator_not_empty": "Er ikke tom",
"translate_description_button": "Oversett tittel/beskrivelse til besøkendes nettleserspråk ved hjelp av AI",
"translate_description_button": "Auto-oversett tittel og beskrivelse",
"translate_description_button_description": "Oversett automatisk titler og beskrivelser til besøkendes nettleserspråk ved hjelp av AI.",
"rr_distribution_method": "Distribusjon",
"rr_distribution_method_description": "Gjør det mulig å optimalisere distribusjonen for maksimal tilgjengelighet eller for å oppnå en mer balansert fordeling.",
"rr_distribution_method_availability_title": "Maksimer tilgjengelighet",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "Seterfunksjonen støtter ikke gjentakende arrangement. Deaktiver seterfunksjonen for å gjøre det gjentakende.",
"recurring_event_seats_error": "Gjentakende arrangement støtter ikke seterfunksjonen. Deaktiver seterfunksjonen eller gjør arrangementet ikke-gjentakende.",
"segment": "Segment",
"saved_filters": "Lagrede filtre",
"rename": "Gi nytt navn",
"rename_segment": "Gi nytt navn til segment",
"delete_segment": "Slett segment",
@@ -3338,6 +3345,7 @@
"picklist": "Valgliste",
"most_cancelled_bookings": "Flest kansellerte bookinger",
"interface_language": "Grensesnittspråk",
"interface_language_description": "Angi ditt foretrukne språk for bestillingsgrensesnittet",
"visitors_browser_language": "Besøkendes nettleserspråk",
"name_or_email": "Navn eller e-post",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Hvis ingen kontakt blir funnet, fall tilbake til leadeier hvis den eksisterer",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Redigere og endre arbeidsflytinnstillinger",
"pbac_desc_delete_workflows": "Fjerne arbeidsflyter fra systemet",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Tilgjengelighet",
"pbac_resource_out_of_office": "Ute av kontoret",
"pbac_desc_create_webhooks": "Opprett webhook'er",
"pbac_desc_view_webhooks": "Se webhook'er",
"pbac_desc_update_webhooks": "Oppdater webhooks",
"pbac_desc_delete_webhooks": "Slett webhooks",
"pbac_desc_create_availability": "Opprett tilgjengelighet",
"pbac_desc_view_availability": "Vis tilgjengelighet",
"pbac_desc_update_availability": "Oppdater tilgjengelighet",
"pbac_desc_delete_availability": "Slett tilgjengelighet",
"pbac_desc_create_out_of_office": "Opprett fravær",
"pbac_desc_view_out_of_office": "Vis fravær",
"pbac_desc_update_out_of_office": "Oppdater fravær",
"pbac_desc_delete_out_of_office": "Slett fravær",
"pbac_desc_manage_workflows": "Full administrasjonstilgang til alle arbeidsflyter",
"pbac_desc_create_event_types": "Opprett hendelsestyper",
"pbac_desc_view_event_types": "Vis hendelsestyper",
@@ -3560,8 +3578,8 @@
"rr_load_balancing_disabled_with_groups": "Lastbalansering er ikke tilgjengelig med round robin-grupper",
"round_robin_settings_description": "Tilpass standardinnstillingene for rundgang for dette teamet",
"round_robin_settings_updated_successfully": "Rundgangsinnstillinger oppdatert",
"booking_creation_time": "Tidspunkt for opprettelse av booking",
"meeting_start_time": "Møtestarttid",
"booking_creation_time": "Tidspunkt for opprettelse av bestilling",
"meeting_start_time": "Møtets starttidspunkt",
"members_affected_by_disabling_delegation_credential": "Berørte medlemmer",
"no_members_affected_by_disabling_delegation_credential": "Ingen medlemmer berørt av deaktivering av delegeringslegitimasjon",
"download_expense_log": "Last ned utgiftslogg",
+23 -5
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Sekretne spotkanie",
"login_instead": "Zamiast tego zaloguj się",
"create_account": "Utwórz Konto",
"get_started": "Rozpocznij",
"confirm_password": "Potwierdź hasło",
"reset_your_password": "Ustaw nowe hasło zgodnie z instrukcjami wysłanymi na Twój adres e-mail.",
"org_banner_instructions": "Proszę przesłać obraz o szerokości {{width}} i wysokości {{height}}.",
@@ -958,7 +959,7 @@
"light_event_type_color": "Kolor typu wydarzenia (jasny motyw)",
"dark_event_type_color": "Kolor typu wydarzenia (ciemny motyw)",
"file_not_named": "Plik nie jest nazwany [idOrSlug]/[user]",
"create_team": "Utwórz Zespół",
"create_team": "Utwórz zespół",
"name": "Nazwa",
"nameless_team": "Zespół bez nazwy",
"oauth_clients": "Klienci OAuth",
@@ -1019,6 +1020,7 @@
"attendees": "Uczestnicy",
"add_attendees": "Dodaj uczestników",
"show_advanced_settings": "Pokaż ustawienia zaawansowane",
"hide_advanced_settings": "Ukryj zaawansowane ustawienia",
"event_name": "Nazwa wydarzenia",
"event_name_in_calendar": "Nazwa wydarzenia w kalendarzu",
"event_name_tooltip": "Nazwa, która pojawi się w kalendarzach",
@@ -1231,7 +1233,7 @@
"contact_sales": "Skontaktuj się ze sprzedażą",
"error_404": "Błąd 404",
"default": "Domyślny",
"set_to_default": "Ustaw jako domyślny",
"set_to_default": "Ustaw jako domyślne",
"new_schedule_btn": "Nowy harmonogram",
"add_new_schedule": "Dodaj nowy harmonogram",
"add_new_calendar": "Dodaj nowy kalendarz",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Gdy to ustawienie jest włączone, członkowie zespołu nie będą mogli zobaczyć innych członków zespołu.",
"you_cannot_see_team_members": "Nie możesz zobaczyć wszystkich członków zespołu prywatnego.",
"you_cannot_see_teams_of_org": "Nie możesz zobaczyć zespołów prywatnej organizacji.",
"allow_booker_to_select_duration": "Zezwól rezerwującemu na wybranie czasu trwania",
"allow_multiple_durations": "Zezwól na wiele czasów trwania",
"impersonate_user_tip": "Wszystkie przypadki użycia tej funkcji są kontrolowane.",
"impersonating_user_warning": "Podszywanie się pod użytkownika „{{user}}”.",
"impersonating_stop_instructions": "Kliknij tutaj, aby zatrzymać",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Potwierdzenie rezerwacji nie powiodło się",
"not_enough_seats": "Za mało miejsc",
"form_builder_field_already_exists": "Pole o tej nazwie już istnieje",
"guests_field_must_be_multiemail": "Pole gości musi być typu 'Wiele adresów e-mail'",
"show_on_booking_page": "Pokaż na stronie rezerwacji",
"visit_cancelled_booking": "Możesz odwiedzić stronę anulowanej rezerwacji",
"get_started_zapier_templates": "Zacznij korzystać z szablonów Zapier",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Zablokowane przez administratora zespołu",
"app_not_connected": "Nie podłączono konta {{appName}}.",
"connect_now": "Podłącz teraz",
"connect_now_unavailable_tooltip": "Opcja 'Połącz teraz' jest obecnie niedostępna. Zarezerwuj z powyższych dostępnych terminów lub spróbuj ponownie później.",
"managed_event_dialog_confirm_button_one": "Zastąp i poinformuj {{count}} członka",
"managed_event_dialog_confirm_button_other": "Zastąp i powiadom {{count}} członków",
"managed_event_dialog_title_one": "Adres URL /{{slug}} już istnieje dla {{count}} członka. Czy chcesz go zastąpić?",
@@ -2688,7 +2692,7 @@
"enterprise": "Przedsiębiorstwo",
"enterprise_description": "Ulepsz do wersji Enterprise, aby utworzyć swoją Organizację",
"create_your_org": "Utwórz swoją Organizację",
"create_org": "Utwórz Organizację",
"create_org": "Utwórz organizację",
"create_your_org_description": "Ulepsz do wersji Organizations i otrzymaj subdomenę, zintegrowane rozliczenia, Insights, rozbudowane możliwości whitelabelingu i więcej",
"create_your_enterprise_description": "Ulepsz do wersji Enterprise i uzyskaj dostęp do synchronizacji Active Directory, automatycznego provisioningu użytkowników SCIM, agentów głosowych Cal.ai, API administracyjnych i więcej!",
"other_payment_app_enabled": "Możesz włączyć tylko jedną aplikację płatniczą na typ wydarzenia",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Kontynuując, zgadzasz się na nasze",
"terms": "Warunki",
"always_show_x_days": "Zawsze dostępne {{x}} dni",
"always_show_x_days_description": "Automatycznie przesuwa {{x}}-dniowe okno rezerwacji każdego dnia, zapewniając, że Twój kalendarz zawsze pokazuje kolejne {{x}} dni dostępnych terminów.",
"team_subscription_cancelled_successfully": "Subskrypcja zespołu została pomyślnie anulowana",
"team_subscription_cancellation_error": "Błąd podczas anulowania subskrypcji zespołu",
"unable_to_subscribe_to_the_platform": "Wystąpił błąd podczas próby subskrypcji planu platformy, spróbuj ponownie później",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Kończy się na",
"filter_operator_is_empty": "Jest pusty",
"filter_operator_not_empty": "Nie jest pusty",
"translate_description_button": "Przetłumacz tytuł/opis na język przeglądarki użytkownika za pomocą AI",
"translate_description_button": "Automatycznie przetłumacz tytuł i opis",
"translate_description_button_description": "Automatycznie tłumacz tytuły i opisy na język przeglądarki odwiedzającego za pomocą AI.",
"rr_distribution_method": "Dystrybucja",
"rr_distribution_method_description": "Pozwala zoptymalizować dystrybucję pod kątem maksymalnej dostępności lub bardziej zrównoważonego przydziału spotkań.",
"rr_distribution_method_availability_title": "Maksymalizuj dostępność",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "Funkcja miejsc nie obsługuje wydarzeń cyklicznych. Wyłącz funkcję miejsc, aby ustawić wydarzenie jako cykliczne.",
"recurring_event_seats_error": "Wydarzenie cykliczne nie obsługuje funkcji miejsc. Wyłącz funkcję miejsc lub ustaw wydarzenie jako jednorazowe.",
"segment": "Segment",
"saved_filters": "Zapisane filtry",
"rename": "Zmień nazwę",
"rename_segment": "Zmień nazwę segmentu",
"delete_segment": "Usuń segment",
@@ -3338,6 +3345,7 @@
"picklist": "Lista wyboru",
"most_cancelled_bookings": "Najczęściej anulowane rezerwacje",
"interface_language": "Język interfejsu",
"interface_language_description": "Ustaw preferowany język interfejsu rezerwacji",
"visitors_browser_language": "Język przeglądarki odwiedzającego",
"name_or_email": "Imię lub email",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Jeśli nie znaleziono kontaktu, powróć do właściciela leada, jeśli istnieje",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Edytuj i modyfikuj ustawienia przepływów pracy",
"pbac_desc_delete_workflows": "Usuń przepływy pracy z systemu",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Dostępność",
"pbac_resource_out_of_office": "Poza biurem",
"pbac_desc_create_webhooks": "Twórz webhooki",
"pbac_desc_view_webhooks": "Przeglądaj webhooki",
"pbac_desc_update_webhooks": "Aktualizuj webhooks",
"pbac_desc_delete_webhooks": "Usuń webhooks",
"pbac_desc_create_availability": "Utwórz dostępność",
"pbac_desc_view_availability": "Wyświetl dostępność",
"pbac_desc_update_availability": "Zaktualizuj dostępność",
"pbac_desc_delete_availability": "Usuń dostępność",
"pbac_desc_create_out_of_office": "Utwórz status poza biurem",
"pbac_desc_view_out_of_office": "Wyświetl status poza biurem",
"pbac_desc_update_out_of_office": "Zaktualizuj status poza biurem",
"pbac_desc_delete_out_of_office": "Usuń status poza biurem",
"pbac_desc_manage_workflows": "Pełny dostęp do zarządzania wszystkimi przepływami pracy",
"pbac_desc_create_event_types": "Tworzenie typów wydarzeń",
"pbac_desc_view_event_types": "Wyświetlanie typów wydarzeń",
@@ -321,6 +321,7 @@
"secret_meeting": "Reunião secreta",
"login_instead": "Em vez disso, faça login",
"create_account": "Criar Conta",
"get_started": "Começar",
"confirm_password": "Confirmar senha",
"reset_your_password": "Defina sua nova senha com as instruções enviadas para seu endereço de e-mail.",
"org_banner_instructions": "Faça upload de uma imagem com largura {{width}} e altura {{height}} .",
@@ -958,7 +959,7 @@
"light_event_type_color": "Cor do Tipo de Evento (Tema Claro)",
"dark_event_type_color": "Cor do Tipo de Evento (Tema Escuro)",
"file_not_named": "Arquivo não é [idOrSlug]/[user]",
"create_team": "Criar Time",
"create_team": "Criar equipe",
"name": "Nome",
"nameless_team": "Equipe sem nome",
"oauth_clients": "Clientes OAuth",
@@ -1019,6 +1020,7 @@
"attendees": "Participantes",
"add_attendees": "Adicionar Participantes",
"show_advanced_settings": "Mostrar Configurações Avançadas",
"hide_advanced_settings": "Ocultar configurações avançadas",
"event_name": "Nome do Evento",
"event_name_in_calendar": "Nome do evento no calendário",
"event_name_tooltip": "O nome que aparecerá nos calendários",
@@ -1231,7 +1233,7 @@
"contact_sales": "Fale com a equipe de vendas",
"error_404": "Erro 404",
"default": "Padrão",
"set_to_default": "Definir para padrão",
"set_to_default": "Definir como padrão",
"new_schedule_btn": "Nova agenda",
"add_new_schedule": "Adicionar nova agenda",
"add_new_calendar": "Adicionar um novo calendário",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Os membros da sua equipe não poderão ver outros membros da equipe após habilitar isso.",
"you_cannot_see_team_members": "Você não pode ver todos os membros da equipe de uma equipe privada.",
"you_cannot_see_teams_of_org": "Você não pode equipes de uma organização privada.",
"allow_booker_to_select_duration": "Permitir que o reservante selecione a duração",
"allow_multiple_durations": "Permitir múltiplas durações",
"impersonate_user_tip": "Todos os usos desse recurso são auditados.",
"impersonating_user_warning": "Representando o nome de usuário \"{{user}}\".",
"impersonating_stop_instructions": "Clique aqui para parar",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Falhar ao confirmar reserva",
"not_enough_seats": "Sem assentos suficientes",
"form_builder_field_already_exists": "Já existe um campo com este nome",
"guests_field_must_be_multiemail": "O campo de convidados deve ser do tipo 'Múltiplos e-mails'",
"show_on_booking_page": "Mostrar na página de reservas",
"visit_cancelled_booking": "Você pode visitar a página de reserva cancelada",
"get_started_zapier_templates": "Comece agora com modelos do Zapier",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Bloqueado pelo administrador da equipe",
"app_not_connected": "Você não conectou uma conta do {{appName}}.",
"connect_now": "Conectar agora",
"connect_now_unavailable_tooltip": "Conectar agora está indisponível no momento. Por favor, agende nos horários acima ou tente novamente mais tarde.",
"managed_event_dialog_confirm_button_one": "Substituir e notificar {{count}} membro",
"managed_event_dialog_confirm_button_other": "Substituir e notificar {{count}} membros",
"managed_event_dialog_title_one": "O url/{{slug}} já existe para {{count}} membro. Deseja substituir?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Ao prosseguir, você concorda com nossos",
"terms": "Termos",
"always_show_x_days": "Sempre {{x}} dias disponíveis",
"always_show_x_days_description": "Move automaticamente a janela de agendamento de {{x}} dias para frente a cada dia, garantindo que seu calendário sempre mostre os próximos {{x}} dias de horários disponíveis.",
"team_subscription_cancelled_successfully": "Assinatura da equipe cancelada com sucesso",
"team_subscription_cancellation_error": "Erro ao cancelar a assinatura da equipe",
"unable_to_subscribe_to_the_platform": "Um erro ocorreu ao tentar adicionar ao plano da plataforma, por favor tente novamente mais tarde",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Termina com",
"filter_operator_is_empty": "Está vazio",
"filter_operator_not_empty": "Não está vazio",
"translate_description_button": "Traduzir título/descrição para o idioma do navegador do visitante usando IA",
"translate_description_button": "Traduzir título e descrição automaticamente",
"translate_description_button_description": "Traduz automaticamente títulos e descrições para o idioma do navegador do visitante usando IA.",
"rr_distribution_method": "Distribuição",
"rr_distribution_method_description": "Permite otimizar a distribuição para máxima disponibilidade ou buscar uma atribuição mais equilibrada.",
"rr_distribution_method_availability_title": "Maximizar disponibilidade",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "O recurso de assentos não suporta eventos recorrentes. Desative o recurso de assentos para torná-lo recorrente.",
"recurring_event_seats_error": "Eventos recorrentes não suportam o recurso de assentos. Desative o recurso de assentos ou torne o evento não recorrente.",
"segment": "Segmento",
"saved_filters": "Filtros salvos",
"rename": "Renomear",
"rename_segment": "Renomear segmento",
"delete_segment": "Excluir segmento",
@@ -3338,6 +3345,7 @@
"picklist": "Lista de seleção",
"most_cancelled_bookings": "Mais cancelamentos",
"interface_language": "Idioma da interface",
"interface_language_description": "Defina seu idioma preferido para a interface de agendamento",
"visitors_browser_language": "Idioma do navegador do visitante",
"name_or_email": "Nome ou e-mail",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Se nenhum contato for encontrado, recorrer ao proprietário do lead, se existir",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Editar e modificar configurações de fluxo de trabalho",
"pbac_desc_delete_workflows": "Remover fluxos de trabalho do sistema",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Disponibilidade",
"pbac_resource_out_of_office": "Ausente",
"pbac_desc_create_webhooks": "Criar webhooks",
"pbac_desc_view_webhooks": "Visualizar webhooks",
"pbac_desc_update_webhooks": "Atualizar webhooks",
"pbac_desc_delete_webhooks": "Excluir webhooks",
"pbac_desc_create_availability": "Criar disponibilidade",
"pbac_desc_view_availability": "Visualizar disponibilidade",
"pbac_desc_update_availability": "Atualizar disponibilidade",
"pbac_desc_delete_availability": "Excluir disponibilidade",
"pbac_desc_create_out_of_office": "Criar ausência",
"pbac_desc_view_out_of_office": "Visualizar ausência",
"pbac_desc_update_out_of_office": "Atualizar ausência",
"pbac_desc_delete_out_of_office": "Excluir ausência",
"pbac_desc_manage_workflows": "Acesso completo de gerenciamento a todos os fluxos de trabalho",
"pbac_desc_create_event_types": "Criar tipos de eventos",
"pbac_desc_view_event_types": "Ver tipos de eventos",
@@ -3560,7 +3578,7 @@
"rr_load_balancing_disabled_with_groups": "O balanceamento de carga não está disponível com Grupos Round Robin",
"round_robin_settings_description": "Personalize as configurações padrão de rodízio para esta equipe",
"round_robin_settings_updated_successfully": "Configurações de rodízio atualizadas com sucesso",
"booking_creation_time": "Horário de criação da reserva",
"booking_creation_time": "Horário de criação do agendamento",
"meeting_start_time": "Horário de início da reunião",
"members_affected_by_disabling_delegation_credential": "Membros afetados",
"no_members_affected_by_disabling_delegation_credential": "Nenhum membro afetado pela desativação da credencial de delegação",
+23 -5
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Reunião Secreta",
"login_instead": "Inicie sessão em alternativa",
"create_account": "Criar Conta",
"get_started": "Começar",
"confirm_password": "Confirmar senha",
"reset_your_password": "Defina a sua nova palavra-passe seguindo as instruções enviadas para o seu endereço de e-mail.",
"org_banner_instructions": "Carregue uma imagem com {{width}} de largura e {{height}} de altura.",
@@ -958,7 +959,7 @@
"light_event_type_color": "Cor do Tipo de Evento (Tema Claro)",
"dark_event_type_color": "Cor do Tipo de Evento (Tema Escuro)",
"file_not_named": "Ficheiro não é [idOrSlug]/[user]",
"create_team": "Criar Equipa",
"create_team": "Criar equipa",
"name": "Nome",
"nameless_team": "Equipe sem nome",
"oauth_clients": "Clientes OAuth",
@@ -1019,6 +1020,7 @@
"attendees": "Participantes",
"add_attendees": "Adicionar Participantes",
"show_advanced_settings": "Mostrar Configurações Avançadas",
"hide_advanced_settings": "Ocultar configurações avançadas",
"event_name": "Nome do Evento",
"event_name_in_calendar": "Nome do evento no calendário",
"event_name_tooltip": "O nome que será mostrado nos calendários",
@@ -1231,7 +1233,7 @@
"contact_sales": "Contacte as vendas",
"error_404": "Erro 404",
"default": "Por omissão",
"set_to_default": "Definir por omissão",
"set_to_default": "Definir como padrão",
"new_schedule_btn": "Novo horário",
"add_new_schedule": "Adicionar novo horário",
"add_new_calendar": "Adicionar um novo calendário",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Os membros da sua equipa não poderão ver outros membros da equipa quando esta opção estiver ativada.",
"you_cannot_see_team_members": "Não pode ver todos os membros da equipa de uma equipa privada.",
"you_cannot_see_teams_of_org": "Não pode ver as equipas de uma organização privada.",
"allow_booker_to_select_duration": "Permitir que o responsável pela reserva selecione a duração",
"allow_multiple_durations": "Permitir múltiplas durações",
"impersonate_user_tip": "Todas as utilizações desta funcionalidade são auditadas.",
"impersonating_user_warning": "Representar o nome de utilizador \"{{user}}\".",
"impersonating_stop_instructions": "Clique aqui para parar",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "A confirmação da reserva falhou",
"not_enough_seats": "Não existem lugares suficientes",
"form_builder_field_already_exists": "Já existe um campo com este nome",
"guests_field_must_be_multiemail": "O campo de convidados deve ser do tipo 'Múltiplos emails'",
"show_on_booking_page": "Mostrar na página de reservas",
"visit_cancelled_booking": "Pode visitar a página da reserva cancelada",
"get_started_zapier_templates": "Comece a utilizar os modelos Zapier",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Bloqueado pelo administrador da equipa",
"app_not_connected": "Não associou uma conta {{appName}}.",
"connect_now": "Associar agora",
"connect_now_unavailable_tooltip": "Conectar agora está indisponível. Por favor, reserve nos horários acima ou tente novamente mais tarde.",
"managed_event_dialog_confirm_button_one": "Substituir e notificar {{count}} membro",
"managed_event_dialog_confirm_button_other": "Substituir e notificar {{count}} membros",
"managed_event_dialog_title_one": "A URL /{{slug}} já existe para {{count}} membro. Pretende substituir a mesma?",
@@ -2688,7 +2692,7 @@
"enterprise": "Empresa",
"enterprise_description": "Atualize para Enterprise para criar a sua Organização",
"create_your_org": "Crie a sua Organização",
"create_org": "Criar Organização",
"create_org": "Criar organização",
"create_your_org_description": "Atualize para Organizations e receba um subdomínio, faturação unificada, Insights, personalização abrangente da imagem corporativa e muito mais",
"create_your_enterprise_description": "Atualize para Enterprise e tenha acesso à sincronização com Active Directory, aprovisionamento automático de utilizadores com SCIM, agentes de voz Cal.ai, API de Administração e muito mais!",
"other_payment_app_enabled": "Só pode ativar uma aplicação de pagamento por tipo de evento",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Ao prosseguir, você concorda com nossos",
"terms": "Termos",
"always_show_x_days": "Sempre {{x}} dias disponíveis",
"always_show_x_days_description": "Move automaticamente a janela de reservas de {{x}} dias para frente a cada dia, garantindo que seu calendário sempre mostre os próximos {{x}} dias de horários disponíveis.",
"team_subscription_cancelled_successfully": "Assinatura da equipe cancelada com sucesso",
"team_subscription_cancellation_error": "Erro ao cancelar a assinatura da equipe",
"unable_to_subscribe_to_the_platform": "Ocorreu um erro ao tentar subscrever o plano Platform. Tente novamente mais tarde",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Termina com",
"filter_operator_is_empty": "Está vazio",
"filter_operator_not_empty": "Não está vazio",
"translate_description_button": "Traduzir título/descrição para o idioma do navegador do visitante usando IA",
"translate_description_button": "Traduzir automaticamente título e descrição",
"translate_description_button_description": "Traduzir automaticamente títulos e descrições para o idioma do navegador do visitante usando IA.",
"rr_distribution_method": "Distribuição",
"rr_distribution_method_description": "Permite otimizar a distribuição para máxima disponibilidade ou para buscar uma atribuição mais equilibrada.",
"rr_distribution_method_availability_title": "Maximizar disponibilidade",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "O recurso de assentos não suporta eventos recorrentes. Desative o recurso de assentos para torná-lo recorrente.",
"recurring_event_seats_error": "Eventos recorrentes não suportam o recurso de assentos. Desative o recurso de assentos ou torne o evento não recorrente.",
"segment": "Segmento",
"saved_filters": "Filtros salvos",
"rename": "Renomear",
"rename_segment": "Renomear segmento",
"delete_segment": "Excluir segmento",
@@ -3338,6 +3345,7 @@
"picklist": "Lista de seleção",
"most_cancelled_bookings": "Mais cancelamentos",
"interface_language": "Idioma da interface",
"interface_language_description": "Defina seu idioma preferido para a interface de reservas",
"visitors_browser_language": "Idioma do navegador do visitante",
"name_or_email": "Nome ou e-mail",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Se nenhum contato for encontrado, recorrer ao proprietário do lead, se existir",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Editar e modificar configurações de fluxo de trabalho",
"pbac_desc_delete_workflows": "Remover fluxos de trabalho do sistema",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Disponibilidade",
"pbac_resource_out_of_office": "Fora do escritório",
"pbac_desc_create_webhooks": "Criar webhooks",
"pbac_desc_view_webhooks": "Visualizar webhooks",
"pbac_desc_update_webhooks": "Atualizar webhooks",
"pbac_desc_delete_webhooks": "Eliminar webhooks",
"pbac_desc_create_availability": "Criar disponibilidade",
"pbac_desc_view_availability": "Ver disponibilidade",
"pbac_desc_update_availability": "Atualizar disponibilidade",
"pbac_desc_delete_availability": "Excluir disponibilidade",
"pbac_desc_create_out_of_office": "Criar fora do escritório",
"pbac_desc_view_out_of_office": "Ver fora do escritório",
"pbac_desc_update_out_of_office": "Atualizar fora do escritório",
"pbac_desc_delete_out_of_office": "Excluir fora do escritório",
"pbac_desc_manage_workflows": "Acesso completo de gerenciamento a todos os fluxos de trabalho",
"pbac_desc_create_event_types": "Criar tipos de evento",
"pbac_desc_view_event_types": "Ver tipos de evento",
+23 -5
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Întâlnire secretă",
"login_instead": "Autentifică-te în schimb",
"create_account": "Creează un cont",
"get_started": "Începeți",
"confirm_password": "Confirmă parola",
"reset_your_password": "Setați noua parolă cu instrucțiunile trimise la adresa dvs. de e-mail.",
"org_banner_instructions": "Te rugăm să încarci o imagine de {{width}} lățime și {{height}} înălțime.",
@@ -958,7 +959,7 @@
"light_event_type_color": "Culoare tip eveniment (Tema deschisă)",
"dark_event_type_color": "Culoare tip eveniment (Tema închisă)",
"file_not_named": "Fișierul nu este numit [idOrSlug]/[user]",
"create_team": "Creează o echipă",
"create_team": "Creați echipă",
"name": "Nume",
"nameless_team": "Echipă fără nume",
"oauth_clients": "Clienți OAuth",
@@ -1019,6 +1020,7 @@
"attendees": "Participanții",
"add_attendees": "Adăugați participanți",
"show_advanced_settings": "Afișează setările avansate",
"hide_advanced_settings": "Ascundeți setările avansate",
"event_name": "Denumirea evenimentului",
"event_name_in_calendar": "Denumirea evenimentului în calendar",
"event_name_tooltip": "Numele care va apărea în calendare",
@@ -1231,7 +1233,7 @@
"contact_sales": "Contactați departamentul de vânzări",
"error_404": "Eroare 404",
"default": "Implicit",
"set_to_default": "Setare ca implicit",
"set_to_default": "Setați ca implicit",
"new_schedule_btn": "Program nou",
"add_new_schedule": "Adăugați un program nou",
"add_new_calendar": "Adăugați un calendar nou",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Membrii echipei dvs. nu vor putea vedea ceilalți membrii ai echipei atunci când această opțiune este activată.",
"you_cannot_see_team_members": "Nu puteți vedea toți membrii unei echipe private.",
"you_cannot_see_teams_of_org": "Nu poți vedea echipele unei organizații private.",
"allow_booker_to_select_duration": "Permite persoanei care face rezervarea să selecteze durata",
"allow_multiple_durations": "Permiteți durate multiple",
"impersonate_user_tip": "Toate utilizările acestei caracteristici sunt verificate.",
"impersonating_user_warning": "Reprezentare nume de utilizator „{{user}}”.",
"impersonating_stop_instructions": "Fă clic aici pentru a înceta",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Confirmare rezervare nereușită",
"not_enough_seats": "Nu sunt suficiente locuri",
"form_builder_field_already_exists": "Există deja un câmp cu acest nume",
"guests_field_must_be_multiemail": "Câmpul pentru invitați trebuie să fie de tipul 'Emailuri multiple'",
"show_on_booking_page": "Afișare pe pagina de rezervare",
"visit_cancelled_booking": "Puteți vizita pagina rezervării anulate",
"get_started_zapier_templates": "Faceți primii pași cu șabloanele Zapier",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Blocat de administratorul echipei",
"app_not_connected": "Nu ați conectat un cont {{appName}}.",
"connect_now": "Conectați-l acum",
"connect_now_unavailable_tooltip": "Conectarea acum nu este disponibilă momentan. Vă rugăm să rezervați din sloturile de mai sus sau să încercați din nou mai târziu.",
"managed_event_dialog_confirm_button_one": "Înlocuiți și anunțați {{count}} membru",
"managed_event_dialog_confirm_button_other": "Înlocuiți și anunțați {{count}} (de) membri",
"managed_event_dialog_title_one": "URL-ul /{{slug}} există deja pentru {{count}} membru. Doriți să îl înlocuiți?",
@@ -2688,7 +2692,7 @@
"enterprise": "Întreprindere",
"enterprise_description": "Faceți upgrade la Enterprise pentru a crea organizația dvs.",
"create_your_org": "Creați-vă Organizația",
"create_org": "Creează Organizație",
"create_org": "Creați organizație",
"create_your_org_description": "Faceți upgrade la Organizații și primiți un subdomeniu, facturare unificată, Insights, whitelabeling extins și altele",
"create_your_enterprise_description": "Faceți upgrade la Enterprise și obțineți acces la Active Directory Sync, SCIM Automatic User provisioning, Cal.ai Voice Agents, Admin APIs și altele!",
"other_payment_app_enabled": "Puteți activa o singură aplicație de plată per tip de eveniment",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Continuând, sunteți de acord cu",
"terms": "Termenii",
"always_show_x_days": "Întotdeauna {{x}} zile disponibile",
"always_show_x_days_description": "Mută automat fereastra de rezervare de {{x}} zile înainte în fiecare zi, asigurându-se că calendarul dumneavoastră arată mereu următoarele {{x}} zile de sloturi disponibile.",
"team_subscription_cancelled_successfully": "Abonamentul echipei a fost anulat cu succes",
"team_subscription_cancellation_error": "Eroare la anularea abonamentului echipei",
"unable_to_subscribe_to_the_platform": "A apărut o eroare în timpul încercării de a te abona la planul platformei, te rugăm să încerci din nou mai târziu",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Se termină cu",
"filter_operator_is_empty": "Este gol",
"filter_operator_not_empty": "Nu este gol",
"translate_description_button": "Traduce titlul/descrierea în limba browserului vizitatorului folosind AI",
"translate_description_button": "Traduceți automat titlul și descrierea",
"translate_description_button_description": "Traduceți automat titlurile și descrierile în limba browserului vizitatorului folosind AI.",
"rr_distribution_method": "Distribuție",
"rr_distribution_method_description": "Permite optimizarea distribuției pentru disponibilitate maximă sau pentru o alocare mai echilibrată.",
"rr_distribution_method_availability_title": "Maximizează disponibilitatea",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "Funcția de locuri nu acceptă evenimentele recurente. Dezactivați funcția de locuri pentru a face evenimentul recurent.",
"recurring_event_seats_error": "Evenimentul recurent nu acceptă funcția de locuri. Dezactivați funcția de locuri sau faceți evenimentul nerecurent.",
"segment": "Segment",
"saved_filters": "Filtre salvate",
"rename": "Redenumește",
"rename_segment": "Redenumește segmentul",
"delete_segment": "Șterge segmentul",
@@ -3338,6 +3345,7 @@
"picklist": "Listă de selecție",
"most_cancelled_bookings": "Cele mai multe rezervări anulate",
"interface_language": "Limba interfeței",
"interface_language_description": "Setați limba preferată pentru interfața de rezervare",
"visitors_browser_language": "Limba browserului vizitatorului",
"name_or_email": "Nume sau email",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Dacă nu este găsit niciun contact, se revine la proprietarul lead-ului dacă acesta există",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Editați și modificați setările fluxurilor de lucru",
"pbac_desc_delete_workflows": "Eliminați fluxurile de lucru din sistem",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Disponibilitate",
"pbac_resource_out_of_office": "În afara biroului",
"pbac_desc_create_webhooks": "Creează webhooks",
"pbac_desc_view_webhooks": "Vizualizează webhooks",
"pbac_desc_update_webhooks": "Actualizează webhooks",
"pbac_desc_delete_webhooks": "Șterge webhooks",
"pbac_desc_create_availability": "Creați disponibilitate",
"pbac_desc_view_availability": "Vizualizați disponibilitatea",
"pbac_desc_update_availability": "Actualizați disponibilitatea",
"pbac_desc_delete_availability": "Ștergeți disponibilitatea",
"pbac_desc_create_out_of_office": "Creați absență de la birou",
"pbac_desc_view_out_of_office": "Vizualizați absența de la birou",
"pbac_desc_update_out_of_office": "Actualizați absența de la birou",
"pbac_desc_delete_out_of_office": "Ștergeți absența de la birou",
"pbac_desc_manage_workflows": "Acces complet de gestionare pentru toate fluxurile de lucru",
"pbac_desc_create_event_types": "Creați tipuri de evenimente",
"pbac_desc_view_event_types": "Vizualizați tipuri de evenimente",
+21 -3
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Секретная встреча",
"login_instead": "Вход",
"create_account": "Создать аккаунт",
"get_started": "Начать",
"confirm_password": "Подтвердите пароль",
"reset_your_password": "Задайте новый пароль. Инструкции о том, как это сделать, мы отправили на ваш адрес электронной почты.",
"org_banner_instructions": "Пожалуйста, загрузите изображение шириной {{width}} и высотой {{height}}.",
@@ -1019,6 +1020,7 @@
"attendees": "Участники",
"add_attendees": "Добавить участников",
"show_advanced_settings": "Показать дополнительные параметры",
"hide_advanced_settings": "Скрыть расширенные настройки",
"event_name": "Название события",
"event_name_in_calendar": "Название события в календаре",
"event_name_tooltip": "Название, которое будет отображаться в календарях",
@@ -1231,7 +1233,7 @@
"contact_sales": "Связаться с отделом продаж",
"error_404": "Ошибка 404",
"default": "По умолчанию",
"set_to_default": "Использовать по умолчанию",
"set_to_default": "Установить по умолчанию",
"new_schedule_btn": "Новое расписание",
"add_new_schedule": "Добавить новое расписание",
"add_new_calendar": "Добавить новый календарь",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Если эта опция включена, участники вашей команды не смогут видеть других участников команды.",
"you_cannot_see_team_members": "Вы не можете видеть список участников закрытой команды.",
"you_cannot_see_teams_of_org": "Вы не можете видеть команды приватной организации.",
"allow_booker_to_select_duration": "Разрешить пользователю, оформляющему бронирование, выбирать длительность",
"allow_multiple_durations": "Разрешить несколько длительностей",
"impersonate_user_tip": "Все варианты использования этой функции проверяются.",
"impersonating_user_warning": "Олицетворение пользователя \"{{user}}\".",
"impersonating_stop_instructions": "Нажмите здесь, чтобы закончить",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Не удалось подтвердить бронирование",
"not_enough_seats": "Недостаточно мест",
"form_builder_field_already_exists": "Поле с таким именем уже существует",
"guests_field_must_be_multiemail": "Поле гостей должно быть типа 'Несколько адресов электронной почты'",
"show_on_booking_page": "Показывать на странице бронирования",
"visit_cancelled_booking": "Вы можете перейти на страницу отмененной встречи",
"get_started_zapier_templates": "Начать работу с шаблонами Zapier",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Заблокировано администратором команды",
"app_not_connected": "Вы не подключили аккаунт {{appName}}.",
"connect_now": "Подключить",
"connect_now_unavailable_tooltip": "Функция \"Подключиться сейчас\" в данный момент недоступна. Пожалуйста, выберите время из доступных выше слотов или попробуйте позже.",
"managed_event_dialog_confirm_button_one": "Заменить и уведомить {{count}} участника",
"managed_event_dialog_confirm_button_other": "Заменить и уведомить участников ({{count}})",
"managed_event_dialog_title_one": "URL-адрес /{{slug}} уже используется {{count}} участником. Заменить?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Продолжая, вы соглашаетесь с нашими",
"terms": "Условиями",
"always_show_x_days": "Всегда доступно {{x}} дней",
"always_show_x_days_description": "Автоматически сдвигает {{x}}-дневное окно бронирования вперед каждый день, чтобы ваш календарь всегда показывал следующие {{x}} дней доступных слотов.",
"team_subscription_cancelled_successfully": "Командная подписка успешно отменена",
"team_subscription_cancellation_error": "Ошибка при отмене командной подписки",
"unable_to_subscribe_to_the_platform": "Произошла ошибка при попытке подписаться на план платформы, пожалуйста, попробуйте позже",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Заканчивается на",
"filter_operator_is_empty": "Пусто",
"filter_operator_not_empty": "Не пусто",
"translate_description_button": "Перевести заголовок/описание на язык браузера посетителя с помощью ИИ",
"translate_description_button": "Автоматически перевести заголовок и описание",
"translate_description_button_description": "Автоматически переводить заголовки и описания на язык браузера посетителя с помощью ИИ.",
"rr_distribution_method": "Распределение",
"rr_distribution_method_description": "Позволяет оптимизировать распределение для максимальной доступности или для достижения более сбалансированного назначения",
"rr_distribution_method_availability_title": "Максимизировать доступность",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "Функция мест не поддерживает повторяющиеся события. Отключите функцию мест, чтобы сделать событие повторяющимся.",
"recurring_event_seats_error": "Повторяющееся событие не поддерживает функцию мест. Отключите функцию мест или сделайте событие неповторяющимся.",
"segment": "Сегмент",
"saved_filters": "Сохраненные фильтры",
"rename": "Переименовать",
"rename_segment": "Переименовать сегмент",
"delete_segment": "Удалить сегмент",
@@ -3338,6 +3345,7 @@
"picklist": "Список выбора",
"most_cancelled_bookings": "Наибольшее количество отмен",
"interface_language": "Язык интерфейса",
"interface_language_description": "Выберите предпочитаемый язык интерфейса бронирования",
"visitors_browser_language": "Язык браузера посетителя",
"name_or_email": "Имя или электронная почта",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Если контакт не найден, использовать владельца лида, если он существует",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Редактирование и изменение настроек рабочих процессов",
"pbac_desc_delete_workflows": "Удаление рабочих процессов из системы",
"pbac_resource_webhook": "Вебхук",
"pbac_resource_availability": "Доступность",
"pbac_resource_out_of_office": "Вне офиса",
"pbac_desc_create_webhooks": "Создание вебхуков",
"pbac_desc_view_webhooks": "Просмотр вебхуков",
"pbac_desc_update_webhooks": "Обновить веб-хуки",
"pbac_desc_delete_webhooks": "Удалить веб-хуки",
"pbac_desc_create_availability": "Создать доступность",
"pbac_desc_view_availability": "Просмотреть доступность",
"pbac_desc_update_availability": "Обновить доступность",
"pbac_desc_delete_availability": "Удалить доступность",
"pbac_desc_create_out_of_office": "Создать статус \"Вне офиса\"",
"pbac_desc_view_out_of_office": "Просмотреть статус \"Вне офиса\"",
"pbac_desc_update_out_of_office": "Обновить статус \"Вне офиса\"",
"pbac_desc_delete_out_of_office": "Удалить статус \"Вне офиса\"",
"pbac_desc_manage_workflows": "Полный доступ к управлению всеми рабочими процессами",
"pbac_desc_create_event_types": "Создание типов событий",
"pbac_desc_view_event_types": "Просмотр типов событий",
@@ -321,6 +321,7 @@
"secret_meeting": "Tajné stretnutie",
"login_instead": "Prihláste sa namiesto toho",
"create_account": "Vytvoriť účet",
"get_started": "Začať",
"confirm_password": "Potvrďte heslo",
"reset_your_password": "Nastavte si nové heslo podľa pokynov zaslaných na vašu e-mailovú adresu.",
"org_banner_instructions": "Nahrajte prosím obrázok so šírkou {{width}} a výškou {{height}}.",
@@ -1019,6 +1020,7 @@
"attendees": "Účastníci",
"add_attendees": "Pridať účastníkov",
"show_advanced_settings": "Zobraziť pokročilé nastavenia",
"hide_advanced_settings": "Skryť pokročilé nastavenia",
"event_name": "Názov udalosti",
"event_name_in_calendar": "S názvom udalosti",
"event_name_tooltip": "Názov, ktorý sa zobrazí v kalendároch",
@@ -1231,7 +1233,7 @@
"contact_sales": "Kontaktovať obchodný tím",
"error_404": "Chyba 404",
"default": "Predvolené",
"set_to_default": "Nastaviť na predvolené",
"set_to_default": "Nastaviť ako predvolené",
"new_schedule_btn": "Nový rozvrh",
"add_new_schedule": "Pridať nový rozvrh",
"add_new_calendar": "Pridať nový kalendár",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Členovia vášho tímu nebudú môcť vidieť ostatných členov tímu, keď je táto možnosť zapnutá.",
"you_cannot_see_team_members": "Nemôžete vidieť všetkých členov súkromného tímu.",
"you_cannot_see_teams_of_org": "Nemôžete vidieť tímy súkromnej organizácie.",
"allow_booker_to_select_duration": "Povoliť rezervujúcemu vybrať si trvanie",
"allow_multiple_durations": "Povoliť viacero trvaní",
"impersonate_user_tip": "Všetky použitia tejto funkcie sú auditované.",
"impersonating_user_warning": "Napodobňovanie používateľa \"{{user}}\".",
"impersonating_stop_instructions": "Kliknite sem pre zastavenie",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Potvrdenie rezervácie zlyhalo",
"not_enough_seats": "Nedostatok voľných miest",
"form_builder_field_already_exists": "Pole s týmto názvom už existuje",
"guests_field_must_be_multiemail": "Pole pre hostí musí byť typu 'Viacero e-mailov'",
"show_on_booking_page": "Zobraziť na stránke rezervácie",
"visit_cancelled_booking": "Môžete navštíviť stránku zrušenej rezervácie",
"get_started_zapier_templates": "Začnite so šablónami Zapier",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Zamknuté tímovým administrátorom",
"app_not_connected": "Nemáte pripojený účet {{appName}}.",
"connect_now": "Pripojiť teraz",
"connect_now_unavailable_tooltip": "Pripojenie teraz je momentálne nedostupné. Prosím, rezervujte si termín z vyššie uvedených časov alebo to skúste neskôr.",
"managed_event_dialog_confirm_button_one": "Nahradiť a upozorniť {{count}} člena",
"managed_event_dialog_confirm_button_other": "Nahradiť a upozorniť {{count}} členov",
"managed_event_dialog_title_one": "URL /{{slug}} už existuje pre {{count}} člena. Chcete ho nahradiť?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Pokračovaním súhlasíte s našimi",
"terms": "Podmienkami",
"always_show_x_days": "Vždy {{x}} dní dostupných",
"always_show_x_days_description": "Automaticky posúva {{x}}-dňové rezervačné okno každý deň dopredu, čím zabezpečuje, že váš kalendár vždy zobrazuje dostupné termíny na nasledujúcich {{x}} dní.",
"team_subscription_cancelled_successfully": "Predplatné tímu bolo úspešne zrušené",
"team_subscription_cancellation_error": "Chyba pri rušení predplatného tímu",
"unable_to_subscribe_to_the_platform": "Pri pokuse o prihlásenie na odber plánu platformy došlo k chybe, skúste to prosím neskôr",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Končí na",
"filter_operator_is_empty": "Je prázdne",
"filter_operator_not_empty": "Nie je prázdne",
"translate_description_button": "Preložiť názov/popis do jazyka prehliadača návštevníka pomocou AI",
"translate_description_button": "Automaticky preložiť názov a popis",
"translate_description_button_description": "Automaticky prekladať názvy a popisy do jazyka prehliadača návštevníka pomocou umelej inteligencie.",
"rr_distribution_method": "Distribúcia",
"rr_distribution_method_description": "Umožňuje optimalizovať distribúciu pre maximálnu dostupnosť alebo pre vyváženejšie prideľovanie.",
"rr_distribution_method_availability_title": "Maximalizovať dostupnosť",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "Funkcia miest nepodporuje opakujúce sa udalosti. Vypnite funkciu miest, aby ste mohli nastaviť opakovanie.",
"recurring_event_seats_error": "Opakujúca sa udalosť nepodporuje funkciu miest. Vypnite funkciu miest alebo zmeňte udalosť na neopakujúcu sa.",
"segment": "Segment",
"saved_filters": "Uložené filtre",
"rename": "Premenovať",
"rename_segment": "Premenovať segment",
"delete_segment": "Odstrániť segment",
@@ -3338,6 +3345,7 @@
"picklist": "Zoznam na výber",
"most_cancelled_bookings": "Najviac zrušených",
"interface_language": "Jazyk rozhrania",
"interface_language_description": "Nastavte svoj preferovaný jazyk pre rezervačné rozhranie",
"visitors_browser_language": "Jazyk prehliadača návštevníka",
"name_or_email": "Meno alebo email",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Ak sa nenájde žiadny kontakt, použije sa vlastník leadu, ak existuje",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Upravovať a modifikovať nastavenia pracovných postupov",
"pbac_desc_delete_workflows": "Odstraňovať pracovné postupy zo systému",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Dostupnosť",
"pbac_resource_out_of_office": "Neprítomnosť",
"pbac_desc_create_webhooks": "Vytvárať webhooky",
"pbac_desc_view_webhooks": "Zobraziť webhooky",
"pbac_desc_update_webhooks": "Aktualizovať webhoky",
"pbac_desc_delete_webhooks": "Odstrániť webhoky",
"pbac_desc_create_availability": "Vytvoriť dostupnosť",
"pbac_desc_view_availability": "Zobraziť dostupnosť",
"pbac_desc_update_availability": "Aktualizovať dostupnosť",
"pbac_desc_delete_availability": "Odstrániť dostupnosť",
"pbac_desc_create_out_of_office": "Vytvoriť neprítomnosť",
"pbac_desc_view_out_of_office": "Zobraziť neprítomnosť",
"pbac_desc_update_out_of_office": "Aktualizovať neprítomnosť",
"pbac_desc_delete_out_of_office": "Odstrániť neprítomnosť",
"pbac_desc_manage_workflows": "Úplný prístup k správe všetkých pracovných postupov",
"pbac_desc_create_event_types": "Vytvárať typy udalostí",
"pbac_desc_view_event_types": "Zobraziť typy udalostí",
+22 -4
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Tajni Sastanak",
"login_instead": "Umesto toga se ulogujte",
"create_account": "Napravite Nalog",
"get_started": "Započnite",
"confirm_password": "Potvrdite lozinku",
"reset_your_password": "Podesite novu lozinku pomoću uputstva poslatim na vašu imejl adresu.",
"org_banner_instructions": "Molimo vas da otpremite sliku širine {{width}} i visine {{height}}.",
@@ -958,7 +959,7 @@
"light_event_type_color": "Boja tipa događaja (svetla tema)",
"dark_event_type_color": "Boja tipa događaja (tamna tema)",
"file_not_named": "Datoteka nije imenovana [idOrSlug]/[user]",
"create_team": "Napravi Tim",
"create_team": "Kreiraj tim",
"name": "Ime",
"nameless_team": "Tim bez imena",
"oauth_clients": "OAuth klijenti",
@@ -1019,6 +1020,7 @@
"attendees": "Polaznici",
"add_attendees": "Dodaj polaznike",
"show_advanced_settings": "Pokaži napredna podešavanja",
"hide_advanced_settings": "Sakrij napredna podešavanja",
"event_name": "Ime Događaja",
"event_name_in_calendar": "Naziv događaja u kalendaru",
"event_name_tooltip": "Ime koje će se pojaviti u kalendarima",
@@ -1231,7 +1233,7 @@
"contact_sales": "Kontaktirajte tim prodaje",
"error_404": "Greška 404",
"default": "Podrazumevano",
"set_to_default": "Postavi na Podrazumevano",
"set_to_default": "Postavi kao podrazumevano",
"new_schedule_btn": "Novi raspored",
"add_new_schedule": "Dodaj novi raspored",
"add_new_calendar": "Dodajte novi kalendar",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Članovi vašeg tima neće moći da vide druge članove tima kada je ovo aktivirano.",
"you_cannot_see_team_members": "Nećete moći da vidite sve članove privatnog tima.",
"you_cannot_see_teams_of_org": "Ne možete videti timove privatne organizacije.",
"allow_booker_to_select_duration": "Dozvoli polazniku da izabere trajanje",
"allow_multiple_durations": "Dozvoli višestruka trajanja",
"impersonate_user_tip": "Sva korišćenja ove funkcije se nadgledaju.",
"impersonating_user_warning": "Predstavljanje kao korisnik „{{user}}“.",
"impersonating_stop_instructions": "Kliknite ovde da zaustavite",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Potvrda rezervacije nije uspela",
"not_enough_seats": "Nema dovoljno mesta",
"form_builder_field_already_exists": "Polje sa ovim imenom već postoji",
"guests_field_must_be_multiemail": "Polje za goste mora biti tipa 'Više e-mail adresa'",
"show_on_booking_page": "Prikaži na stranici zakazivanja",
"visit_cancelled_booking": "Možete posetiti stranicu otkazane rezervacije",
"get_started_zapier_templates": "Započnite sa Zapier predlošcima",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Zaključao administrator tima",
"app_not_connected": "Niste povezali {{appName}} nalog.",
"connect_now": "Povežite odmah",
"connect_now_unavailable_tooltip": "Povezivanje odmah trenutno nije dostupno. Molimo rezervišite iz gornjih termina ili pokušajte kasnije.",
"managed_event_dialog_confirm_button_one": "Zameni i obavesti {{count}} člana",
"managed_event_dialog_confirm_button_other": "Zameni i obavesti {{count}} članova",
"managed_event_dialog_title_one": "URL /{{slug}} već postoji za {{count}} člana. Da li želite da ga zamenite?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Nastavljanjem prihvatate naše",
"terms": "Uslove korišćenja",
"always_show_x_days": "Uvek dostupno {{x}} dana",
"always_show_x_days_description": "Automatski pomera prozor za rezervacije od {{x}} dana unapred svakog dana, osiguravajući da vaš kalendar uvek prikazuje sledećih {{x}} dana dostupnih termina.",
"team_subscription_cancelled_successfully": "Pretplata tima je uspešno otkazana",
"team_subscription_cancellation_error": "Greška prilikom otkazivanja pretplate tima",
"unable_to_subscribe_to_the_platform": "Došlo je do greške prilikom pokušaja pretplate na platformu, molimo pokušajte ponovo kasnije",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Završava sa",
"filter_operator_is_empty": "Prazno je",
"filter_operator_not_empty": "Nije prazno",
"translate_description_button": "Prevedi naslov/opis na jezik posetiočevog pretraživača koristeći AI",
"translate_description_button": "Automatski prevedi naslov i opis",
"translate_description_button_description": "Automatski prevedi naslove i opise na jezik posetiočevog pregledača koristeći veštačku inteligenciju.",
"rr_distribution_method": "Distribucija",
"rr_distribution_method_description": "Omogućava optimizaciju distribucije za maksimalnu dostupnost ili za balansiranu raspodelu.",
"rr_distribution_method_availability_title": "Maksimalna dostupnost",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "Funkcija mesta ne podržava događaj koji se ponavlja. Onemogućite funkciju mesta da bi se događaj mogao ponavljati.",
"recurring_event_seats_error": "Događaj koji se ponavlja ne podržava funkciju mesta. Onemogućite funkciju mesta ili učinite događaj jednokratnim.",
"segment": "Segment",
"saved_filters": "Sačuvani filteri",
"rename": "Preimenuj",
"rename_segment": "Preimenuj segment",
"delete_segment": "Obriši segment",
@@ -3338,6 +3345,7 @@
"picklist": "Lista za izbor",
"most_cancelled_bookings": "Najviše otkazanih",
"interface_language": "Jezik interfejsa",
"interface_language_description": "Postavite željeni jezik za interfejs za rezervacije",
"visitors_browser_language": "Jezik pregledača posetioca",
"name_or_email": "Ime ili email",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Ako kontakt nije pronađen, vrati se na vlasnika potencijalnog klijenta ako postoji",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Uređivanje i modifikovanje podešavanja radnih tokova",
"pbac_desc_delete_workflows": "Uklanjanje radnih tokova iz sistema",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Dostupnost",
"pbac_resource_out_of_office": "Van kancelarije",
"pbac_desc_create_webhooks": "Kreirajte webhooks",
"pbac_desc_view_webhooks": "Pregledajte webhooks",
"pbac_desc_update_webhooks": "Ažuriraj webhook-ove",
"pbac_desc_delete_webhooks": "Obriši webhook-ove",
"pbac_desc_create_availability": "Kreiraj dostupnost",
"pbac_desc_view_availability": "Pregledaj dostupnost",
"pbac_desc_update_availability": "Ažuriraj dostupnost",
"pbac_desc_delete_availability": "Obriši dostupnost",
"pbac_desc_create_out_of_office": "Kreiraj odsustvo iz kancelarije",
"pbac_desc_view_out_of_office": "Pregledaj odsustvo iz kancelarije",
"pbac_desc_update_out_of_office": "Ažuriraj odsustvo iz kancelarije",
"pbac_desc_delete_out_of_office": "Obriši odsustvo iz kancelarije",
"pbac_desc_manage_workflows": "Puni pristup upravljanju svim radnim tokovima",
"pbac_desc_create_event_types": "Kreiranje tipova događaja",
"pbac_desc_view_event_types": "Pregled tipova događaja",
+22 -4
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Hemligt möte",
"login_instead": "Logga in istället",
"create_account": "Skapa konto",
"get_started": "Kom igång",
"confirm_password": "Bekräfta lösenord",
"reset_your_password": "Konfigurera ditt nya lösenord med instruktionerna som skickas till din e-postadress.",
"org_banner_instructions": "Vänligen ladda upp en bild med {{width}} bredd och {{height}} höjd.",
@@ -1019,6 +1020,7 @@
"attendees": "Deltagare",
"add_attendees": "Lägg till deltagare",
"show_advanced_settings": "Visa avancerade inställningar",
"hide_advanced_settings": "Dölj avancerade inställningar",
"event_name": "Händelsenamn",
"event_name_in_calendar": "Händelsenamn i kalender",
"event_name_tooltip": "Namnet som kommer visas i kalendrar",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Dina teammedlemmar kommer inte att kunna se andra teammedlemmar när detta är aktiverat.",
"you_cannot_see_team_members": "Du kan inte se alla teammedlemmar i ett privat team.",
"you_cannot_see_teams_of_org": "Du kan inte se teamen i en privat organisation.",
"allow_booker_to_select_duration": "Låt bokaren välja varaktighet",
"allow_multiple_durations": "Tillåt flera varaktigheter",
"impersonate_user_tip": "All användning av denna funktion granskas.",
"impersonating_user_warning": "Imiterar användarnamn \"{{user}}\".",
"impersonating_stop_instructions": "Klicka här för att sluta",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Bokningsbekräftelsen misslyckades",
"not_enough_seats": "Inte tillräckligt med platser",
"form_builder_field_already_exists": "Det finns redan ett fält med det här namnet",
"guests_field_must_be_multiemail": "Fältet för gäster måste vara av typen 'Flera e-postadresser'",
"show_on_booking_page": "Visa på bokningssidan",
"visit_cancelled_booking": "Du kan besöka sidan för avbokade bokningar",
"get_started_zapier_templates": "Kom igång med Zapier-mallar",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Låst av teamadministratör",
"app_not_connected": "Du har inte anslutit ett {{appName}}-konto.",
"connect_now": "Anslut nu",
"connect_now_unavailable_tooltip": "Anslut nu är för närvarande inte tillgängligt. Boka från ovanstående tider eller försök igen senare.",
"managed_event_dialog_confirm_button_one": "Ersätt och meddela {{count}} medlem",
"managed_event_dialog_confirm_button_other": "Ersätt och meddela {{count}} medlemmar",
"managed_event_dialog_title_one": "Webbadressen/{{slug}} finns redan för {{count}} medlem. Vill du ersätta den?",
@@ -2688,7 +2692,7 @@
"enterprise": "Företag",
"enterprise_description": "Uppgradera till Enterprise för att skapa din organisation",
"create_your_org": "Skapa din Organisation",
"create_org": "Skapa Organisation",
"create_org": "Skapa organisation",
"create_your_org_description": "Uppgradera till Organisationer och få en subdomän, enhetlig fakturering, Insights, omfattande whitelabeling och mer",
"create_your_enterprise_description": "Uppgradera till Enterprise och få tillgång till Active Directory Sync, SCIM Automatisk Användarprovisionering, Cal.ai Röstagenter, Admin API:er och mer!",
"other_payment_app_enabled": "Du kan bara aktivera en betalningsapp per eventtyp",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Genom att fortsätta godkänner du våra",
"terms": "Villkor",
"always_show_x_days": "Visa alltid {{x}} dagar tillgängliga",
"always_show_x_days_description": "Flyttar automatiskt fram {{x}}-dagars bokningsfönstret varje dag, vilket säkerställer att din kalender alltid visar de kommande {{x}} dagarna med tillgängliga tider.",
"team_subscription_cancelled_successfully": "Teamprenumerationen avbröts framgångsrikt",
"team_subscription_cancellation_error": "Fel vid avbokning av teamprenumerationen",
"unable_to_subscribe_to_the_platform": "Ett fel uppstod vid försök att prenumerera på plattformen, försök igen senare",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Slutar med",
"filter_operator_is_empty": "Är tom",
"filter_operator_not_empty": "Är inte tom",
"translate_description_button": "Översätt titel/beskrivning till besökarens webbläsarspråk med hjälp av AI",
"translate_description_button": "Översätt titel och beskrivning automatiskt",
"translate_description_button_description": "Översätt automatiskt titlar och beskrivningar till besökarens webbläsarspråk med hjälp av AI.",
"rr_distribution_method": "Distribution",
"rr_distribution_method_description": "Möjliggör optimering av distributionen för maximal tillgänglighet eller för att uppnå en mer balanserad fördelning.",
"rr_distribution_method_availability_title": "Maximera tillgänglighet",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "Platser-funktionen stöder inte återkommande evenemang. Inaktivera platser-funktionen för att göra det återkommande.",
"recurring_event_seats_error": "Återkommande evenemang stöder inte platser-funktionen. Inaktivera platser-funktionen eller gör evenemanget icke-återkommande.",
"segment": "Segment",
"saved_filters": "Sparade filter",
"rename": "Byt namn",
"rename_segment": "Byt namn på segment",
"delete_segment": "Ta bort segment",
@@ -3338,6 +3345,7 @@
"picklist": "Urvalslista",
"most_cancelled_bookings": "Mest avbokade",
"interface_language": "Gränssnittsspråk",
"interface_language_description": "Ange ditt föredragna språk för bokningsgränssnittet",
"visitors_browser_language": "Besökarens webbläsarspråk",
"name_or_email": "Namn eller e-post",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Om ingen kontakt hittas, återgå till lead-ägaren om sådan finns",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Redigera och ändra inställningar för arbetsflöden",
"pbac_desc_delete_workflows": "Ta bort arbetsflöden från systemet",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Tillgänglighet",
"pbac_resource_out_of_office": "Inte på kontoret",
"pbac_desc_create_webhooks": "Skapa webhooks",
"pbac_desc_view_webhooks": "Visa webhooks",
"pbac_desc_update_webhooks": "Uppdatera webhooks",
"pbac_desc_delete_webhooks": "Ta bort webhooks",
"pbac_desc_create_availability": "Skapa tillgänglighet",
"pbac_desc_view_availability": "Visa tillgänglighet",
"pbac_desc_update_availability": "Uppdatera tillgänglighet",
"pbac_desc_delete_availability": "Ta bort tillgänglighet",
"pbac_desc_create_out_of_office": "Skapa frånvaro",
"pbac_desc_view_out_of_office": "Visa frånvaro",
"pbac_desc_update_out_of_office": "Uppdatera frånvaro",
"pbac_desc_delete_out_of_office": "Ta bort frånvaro",
"pbac_desc_manage_workflows": "Fullständig hantering av alla arbetsflöden",
"pbac_desc_create_event_types": "Skapa händelsetyper",
"pbac_desc_view_event_types": "Visa händelsetyper",
@@ -3560,7 +3578,7 @@
"rr_load_balancing_disabled_with_groups": "Lastbalansering är inte tillgänglig med Round Robin-grupper",
"round_robin_settings_description": "Anpassa standardinställningarna för round robin för det här teamet",
"round_robin_settings_updated_successfully": "Round robin-inställningarna har uppdaterats",
"booking_creation_time": "Tid för bokningens skapande",
"booking_creation_time": "Tid för bokningsskapande",
"meeting_start_time": "Mötesstarttid",
"members_affected_by_disabling_delegation_credential": "Påverkade medlemmar",
"no_members_affected_by_disabling_delegation_credential": "Inga medlemmar påverkas av att inaktivera delegeringsuppgifter",
+24 -6
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Gizli Toplantı",
"login_instead": "Ya da giriş yapın",
"create_account": "Hesap Oluştur",
"get_started": "Başlayın",
"confirm_password": "Şifreyi onayla",
"reset_your_password": "E-posta adresinize gönderilen talimatlarla yeni şifrenizi belirleyin.",
"org_banner_instructions": "Lütfen {{width}} genişlik ve {{height}} yükseklikte bir resim yükleyin.",
@@ -958,7 +959,7 @@
"light_event_type_color": "Etkinlik Türü Rengi (Açık Tema)",
"dark_event_type_color": "Etkinlik Türü Rengi (Koyu Tema)",
"file_not_named": "Dosya adlandırılmamış [idOrSlug]/[user]",
"create_team": "Ekip Oluştur",
"create_team": "Ekip oluştur",
"name": "Ad",
"nameless_team": "İsimsiz Takım",
"oauth_clients": "OAuth İstemcileri",
@@ -1019,6 +1020,7 @@
"attendees": "Katılımcılar",
"add_attendees": "Katılımcı ekle",
"show_advanced_settings": "Gelişmiş ayarları göster",
"hide_advanced_settings": "Gelişmiş ayarları gizle",
"event_name": "Etkinlik Adı",
"event_name_in_calendar": "Takvimdeki etkinlik adı",
"event_name_tooltip": "Takvimlerde görünecek isim",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Bu özellik açıldığında ekip üyeleriniz diğer ekip üyelerini göremez.",
"you_cannot_see_team_members": "Özel bir ekibin tüm ekip üyelerini göremezsiniz.",
"you_cannot_see_teams_of_org": "Özel bir kuruluşun takımlarını göremezsiniz.",
"allow_booker_to_select_duration": "Rezervasyon yapan kişinin süreyi seçmesine izin ver",
"allow_multiple_durations": "Çoklu sürelere izin ver",
"impersonate_user_tip": "Bu özelliğin tüm kullanımları denetlenir.",
"impersonating_user_warning": "\"{{user}}\" kullanıcı adı kimliğine bürünme.",
"impersonating_stop_instructions": "Durdurmak için buraya tıklayın",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Rezervasyon onayı yapılamadı",
"not_enough_seats": "Yeterli yer yok",
"form_builder_field_already_exists": "Bu ada sahip bir alan zaten var",
"guests_field_must_be_multiemail": "Misafirler alanı 'Çoklu e-postalar' türünde olmalıdır",
"show_on_booking_page": "Rezervasyon sayfasında göster",
"visit_cancelled_booking": "İptal edilen rezervasyon sayfasını ziyaret edebilirsiniz",
"get_started_zapier_templates": "Zapier şablonlarını kullanmaya başlayın",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Ekip yöneticisi tarafından kilitlendi",
"app_not_connected": "{{appName}} hesabı bağlamadınız.",
"connect_now": "Hemen bağlan",
"connect_now_unavailable_tooltip": "Şu anda hemen bağlan kullanılamıyor. Lütfen yukarıdaki zaman dilimlerinden rezervasyon yapın veya daha sonra tekrar deneyin.",
"managed_event_dialog_confirm_button_one": "{{count}} üyeyi değiştirin ve bilgilendirin",
"managed_event_dialog_confirm_button_other": "{{count}} üyeyi değiştirin ve bilgilendirin",
"managed_event_dialog_title_one": "URL /{{slug}}, {{count}} üye için zaten mevcut. Bunu değiştirmek istiyor musunuz?",
@@ -2688,7 +2692,7 @@
"enterprise": "Kurumsal",
"enterprise_description": "Organizasyonunuzu oluşturmak için Enterprise'a yükseltin",
"create_your_org": "Organizasyonunuzu Oluşturun",
"create_org": "Kuruluş Oluştur",
"create_org": "Organizasyon oluştur",
"create_your_org_description": "Organizasyonlara yükseltin ve bir alt alan adı, birleşik faturalama, Insights, kapsamlı beyaz etiketleme ve daha fazlasını alın",
"create_your_enterprise_description": "Enterprise'a yükseltin ve Active Directory Sync, SCIM Otomatik Kullanıcı sağlama, Cal.ai Ses Ajanları, Yönetici API'leri ve daha fazlasına erişim kazanın!",
"other_payment_app_enabled": "Her etkinlik türü için yalnızca bir ödeme uygulamasını etkinleştirebilirsiniz",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Devam ederek",
"terms": "Koşullarımızı",
"always_show_x_days": "Her zaman {{x}} gün mevcut",
"always_show_x_days_description": "{{x}} günlük rezervasyon penceresini her gün otomatik olarak ilerletir, böylece takviminiz her zaman müsait zaman dilimlerinin sonraki {{x}} gününü gösterir.",
"team_subscription_cancelled_successfully": "Takım aboneliği başarıyla iptal edildi",
"team_subscription_cancellation_error": "Takım aboneliğini iptal ederken hata oluştu",
"unable_to_subscribe_to_the_platform": "Platform planına abone olmaya çalışırken bir hata oluştu, lütfen daha sonra tekrar deneyin",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "İle biter",
"filter_operator_is_empty": "Boş",
"filter_operator_not_empty": "Boş değil",
"translate_description_button": "Başlığı/açıklamayı ziyaretçinin tarayıcı diline yapay zeka kullanarak çevir",
"translate_description_button": "Başlık ve açıklamayı otomatik çevir",
"translate_description_button_description": "Başlıkları ve açıklamaları ziyaretçinin tarayıcı diline yapay zeka kullanarak otomatik olarak çevirin.",
"rr_distribution_method": "Dağıtım",
"rr_distribution_method_description": "Maksimum kullanılabilirlik için dağıtımı optimize etmeye veya daha dengeli bir atama sağlamaya olanak tanır.",
"rr_distribution_method_availability_title": "Kullanılabilirliği maksimize et",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "Koltuk özelliği tekrarlanan etkinliği desteklemiyor. Tekrarlanan etkinlik yapmak için koltuk özelliğini devre dışı bırakın.",
"recurring_event_seats_error": "Tekrarlanan etkinlik, koltuk özelliğini desteklemiyor. Koltuk özelliğini devre dışı bırakın veya etkinliği tekrarlanmayan hale getirin.",
"segment": "Segment",
"saved_filters": "Kaydedilmiş filtreler",
"rename": "Yeniden adlandır",
"rename_segment": "Segmenti yeniden adlandır",
"delete_segment": "Segmenti sil",
@@ -3338,6 +3345,7 @@
"picklist": "Seçim listesi",
"most_cancelled_bookings": "En Çok İptal Edilen",
"interface_language": "Arayüz dili",
"interface_language_description": "Rezervasyon arayüzü için tercih ettiğiniz dili ayarlayın",
"visitors_browser_language": "Ziyaretçinin tarayıcı dili",
"name_or_email": "İsim veya E-posta",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Hiçbir kişi bulunamazsa, eğer varsa müşteri adayı sahibine geri dön",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "İş akışı ayarlarını düzenle ve değiştir",
"pbac_desc_delete_workflows": "İş akışlarını sistemden kaldır",
"pbac_resource_webhook": "Web kancası",
"pbac_resource_availability": "Müsaitlik",
"pbac_resource_out_of_office": "Ofis Dışında",
"pbac_desc_create_webhooks": "Web kancaları oluştur",
"pbac_desc_view_webhooks": "Web kancalarını görüntüle",
"pbac_desc_update_webhooks": "Web kancalarını güncelle",
"pbac_desc_delete_webhooks": "Web kancalarını sil",
"pbac_desc_create_availability": "Müsaitlik oluştur",
"pbac_desc_view_availability": "Müsaitliği görüntüle",
"pbac_desc_update_availability": "Müsaitliği güncelle",
"pbac_desc_delete_availability": "Müsaitliği sil",
"pbac_desc_create_out_of_office": "Ofis dışı oluştur",
"pbac_desc_view_out_of_office": "Ofis dışını görüntüle",
"pbac_desc_update_out_of_office": "Ofis dışını güncelle",
"pbac_desc_delete_out_of_office": "Ofis dışını sil",
"pbac_desc_manage_workflows": "Tüm iş akışlarına tam yönetim erişimi",
"pbac_desc_create_event_types": "Etkinlik türleri oluştur",
"pbac_desc_view_event_types": "Etkinlik türlerini görüntüle",
@@ -3560,8 +3578,8 @@
"rr_load_balancing_disabled_with_groups": "Round Robin Grupları ile Yük Dengeleme kullanılamaz",
"round_robin_settings_description": "Bu takım için varsayılan round robin ayarlarını özelleştirin",
"round_robin_settings_updated_successfully": "Round Robin ayarları başarıyla güncellendi",
"booking_creation_time": "Rezervasyon Oluşturma Zamanı",
"meeting_start_time": "Toplantı Başlangıç Zamanı",
"booking_creation_time": "Rezervasyon oluşturma zamanı",
"meeting_start_time": "Toplantı başlangıç zamanı",
"members_affected_by_disabling_delegation_credential": "Etkilenen üyeler",
"no_members_affected_by_disabling_delegation_credential": "Yetkilendirme kimlik bilgisinin devre dışı bırakılmasından etkilenen üye yok",
"download_expense_log": "Gider Kaydını İndir",
+21 -3
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Таємна нарада",
"login_instead": "Натомість увійти",
"create_account": "Створити обліковий запис",
"get_started": "Почати",
"confirm_password": "Підтвердити пароль",
"reset_your_password": "Установіть новий пароль, дотримуючись інструкцій, надісланих на вашу електронну адресу.",
"org_banner_instructions": "Будь ласка, завантажте зображення шириною {{width}} і висотою {{height}}.",
@@ -1019,6 +1020,7 @@
"attendees": "Учасники",
"add_attendees": "Додати учасників",
"show_advanced_settings": "Показати додаткові параметри",
"hide_advanced_settings": "Приховати розширені налаштування",
"event_name": "Тип заходу",
"event_name_in_calendar": "Назва події в календарі",
"event_name_tooltip": "Ім’я, яке показуватиметься в календарях",
@@ -1231,7 +1233,7 @@
"contact_sales": "Зв’язатися з відділом продажів",
"error_404": "Помилка 404",
"default": "За замовчуванням",
"set_to_default": "Установити за замовчуванням",
"set_to_default": "Встановити за замовчуванням",
"new_schedule_btn": "Створити розклад",
"add_new_schedule": "Додати новий розклад",
"add_new_calendar": "Додати новий календар",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Учасники вашої команди не зможуть бачити інших учасників, якщо цей параметр ввімкнено.",
"you_cannot_see_team_members": "Ви не можете бачити всіх учасників приватної команди.",
"you_cannot_see_teams_of_org": "Ви не можете бачити команди приватної організації.",
"allow_booker_to_select_duration": "Дозволити автору бронювання вибирати тривалість",
"allow_multiple_durations": "Дозволити кілька тривалостей",
"impersonate_user_tip": "Усі випадки використання цієї функції підпадають під аудит.",
"impersonating_user_warning": "Виконується роль користувача {{user}}.",
"impersonating_stop_instructions": "Натисніть тут, щоб зупинити",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Не вдалося підтвердити бронювання",
"not_enough_seats": "Недостатньо місць",
"form_builder_field_already_exists": "Поле з такою назвою вже існує",
"guests_field_must_be_multiemail": "Поле гостей повинно бути типу 'Кілька електронних адрес'",
"show_on_booking_page": "Показувати на сторінці бронювання",
"visit_cancelled_booking": "Ви можете відвідати сторінку скасованого бронювання",
"get_started_zapier_templates": "Розпочніть роботу із шаблонами Zapier",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Заблоковано адміністратором команди",
"app_not_connected": "Ви не під’єднали обліковий запис {{appName}}.",
"connect_now": "Під’єднати",
"connect_now_unavailable_tooltip": "Підключення зараз недоступне. Будь ласка, забронюйте з доступних слотів вище або спробуйте пізніше.",
"managed_event_dialog_confirm_button_one": "Замінити й повідомити {{count}} учасника",
"managed_event_dialog_confirm_button_other": "Замінити й повідомити учасників ({{count}})",
"managed_event_dialog_title_one": "URL-адреса /{{slug}} вже існує для {{count}} учасника. Замінити її?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Продовжуючи, ви погоджуєтеся з нашими",
"terms": "Умовами",
"always_show_x_days": "Завжди доступно {{x}} днів",
"always_show_x_days_description": "Автоматично переміщує вікно бронювання на {{x}} днів вперед щодня, забезпечуючи, щоб ваш календар завжди показував наступні {{x}} днів доступних слотів.",
"team_subscription_cancelled_successfully": "Підписку команди успішно скасовано",
"team_subscription_cancellation_error": "Помилка під час скасування підписки команди",
"unable_to_subscribe_to_the_platform": "Сталася помилка під час спроби підписатися на план платформи, будь ласка, спробуйте пізніше",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Закінчується на",
"filter_operator_is_empty": "Порожньо",
"filter_operator_not_empty": "Не порожньо",
"translate_description_button": "Перекласти заголовок/опис мовою браузера відвідувача за допомогою ШІ",
"translate_description_button": "Автоматичний переклад назви та опису",
"translate_description_button_description": "Автоматично перекладати назви та описи мовою браузера відвідувача за допомогою ШІ.",
"rr_distribution_method": "Розподіл",
"rr_distribution_method_description": "Дозволяє оптимізувати розподіл для максимальної доступності або для більш збалансованого розподілу завдань.",
"rr_distribution_method_availability_title": "Максимізувати доступність",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "Функція місць не підтримує повторювані події. Вимкніть функцію місць, щоб зробити подію повторюваною.",
"recurring_event_seats_error": "Повторювана подія не підтримує функцію місць. Вимкніть функцію місць або зробіть подію неповторюваною.",
"segment": "Сегмент",
"saved_filters": "Збережені фільтри",
"rename": "Перейменувати",
"rename_segment": "Перейменувати сегмент",
"delete_segment": "Видалити сегмент",
@@ -3338,6 +3345,7 @@
"picklist": "Список вибору",
"most_cancelled_bookings": "Найбільше скасованих бронювань",
"interface_language": "Мова інтерфейсу",
"interface_language_description": "Встановіть бажану мову для інтерфейсу бронювання",
"visitors_browser_language": "Мова браузера відвідувача",
"name_or_email": "Ім'я або електронна пошта",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Якщо контакт не знайдено, використовувати власника ліда, якщо він існує",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Редагування та зміна налаштувань робочих процесів",
"pbac_desc_delete_workflows": "Видалення робочих процесів із системи",
"pbac_resource_webhook": "Вебгук",
"pbac_resource_availability": "Доступність",
"pbac_resource_out_of_office": "Поза офісом",
"pbac_desc_create_webhooks": "Створення вебгуків",
"pbac_desc_view_webhooks": "Перегляд вебгуків",
"pbac_desc_update_webhooks": "Оновлення вебгуків",
"pbac_desc_delete_webhooks": "Видалення вебгуків",
"pbac_desc_create_availability": "Створення доступності",
"pbac_desc_view_availability": "Перегляд доступності",
"pbac_desc_update_availability": "Оновлення доступності",
"pbac_desc_delete_availability": "Видалення доступності",
"pbac_desc_create_out_of_office": "Створення режиму відсутності в офісі",
"pbac_desc_view_out_of_office": "Перегляд режиму відсутності в офісі",
"pbac_desc_update_out_of_office": "Оновлення режиму відсутності в офісі",
"pbac_desc_delete_out_of_office": "Видалення режиму відсутності в офісі",
"pbac_desc_manage_workflows": "Повний доступ до керування всіма робочими процесами",
"pbac_desc_create_event_types": "Створення типів заходів",
"pbac_desc_view_event_types": "Перегляд типів заходів",
+22 -4
View File
@@ -321,6 +321,7 @@
"secret_meeting": "Cuộc họp bí mật",
"login_instead": "Đăng nhập",
"create_account": "Tạo tài khoản",
"get_started": "Bắt đầu",
"confirm_password": "Xác nhận mật khẩu",
"reset_your_password": "Thiết lập mật khẩu mới bằng những chỉ dẫn được gửi đến địa chỉ email của bạn.",
"org_banner_instructions": "Vui lòng tải lên hình ảnh có chiều rộng {{width}} và chiều cao {{height}}.",
@@ -1019,6 +1020,7 @@
"attendees": "Người tham dự",
"add_attendees": "Thêm người tham dự",
"show_advanced_settings": "Hiển thị cài đặt nâng cao",
"hide_advanced_settings": "Ẩn cài đặt nâng cao",
"event_name": "Tên sự kiện",
"event_name_in_calendar": "Tên sự kiện trong lịch",
"event_name_tooltip": "Tên sẽ xuất hiện trong lịch",
@@ -1231,7 +1233,7 @@
"contact_sales": "Liên hệ với bộ phận bán hàng",
"error_404": "Lỗi 404",
"default": "Mặc định",
"set_to_default": "Đặt thành Mặc định",
"set_to_default": "Đặt làm mặc định",
"new_schedule_btn": "Lịch mới",
"add_new_schedule": "Thêm một lịch mới",
"add_new_calendar": "Thêm một lịch mới",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "Thành viên nhóm của bạn sẽ không thể thấy thành viên nhóm khác khi cái này được bật.",
"you_cannot_see_team_members": "Bạn không thể thấy tất cả thành viên nhóm của một nhóm riêng tư.",
"you_cannot_see_teams_of_org": "Bạn không thể thấy các nhóm của một tổ chức riêng tư.",
"allow_booker_to_select_duration": "Cho phép người đặt hẹn chọn khoảng thời gian",
"allow_multiple_durations": "Cho phép nhiều khoảng thời gian",
"impersonate_user_tip": "Tất cả những lần dùng tính năng này đều bị kiểm toán.",
"impersonating_user_warning": "Đang mạo danh tên người dùng \"{{user}}\".",
"impersonating_stop_instructions": "Nhấp vào đây để ngừng lại",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "Xác nhận lịch hẹn không thành công",
"not_enough_seats": "Không đủ chỗ",
"form_builder_field_already_exists": "Đã tồn tại một trường có tên này",
"guests_field_must_be_multiemail": "Trường khách mời phải có kiểu 'Nhiều email'",
"show_on_booking_page": "Hiện trên trang lịch hẹn",
"visit_cancelled_booking": "Bạn có thể truy cập trang đặt chỗ đã hủy",
"get_started_zapier_templates": "Bắt đầu với các mẫu Zapier",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "Bị khoá bởi quản trị viên của nhóm",
"app_not_connected": "Bạn không có tài khoản {{appName}} đã được kết nối.",
"connect_now": "Kết nối ngay",
"connect_now_unavailable_tooltip": "Kết nối ngay hiện không khả dụng. Vui lòng đặt lịch từ các khung giờ phía trên hoặc thử lại sau.",
"managed_event_dialog_confirm_button_one": "Thay thế & thông báo cho {{count}} thành viên",
"managed_event_dialog_confirm_button_other": "Thay thế & thông báo cho {{count}} thành viên",
"managed_event_dialog_title_one": "URL / {{slug}} đã có sẵn cho {{count}} thành viên. Bạn có muốn thay thế nó?",
@@ -2688,7 +2692,7 @@
"enterprise": "Doanh nghiệp",
"enterprise_description": "Nâng cấp lên Enterprise để tạo Tổ chức của bạn",
"create_your_org": "Tạo Tổ chức của bạn",
"create_org": "Tạo Tổ chức",
"create_org": "Tạo tổ chức",
"create_your_org_description": "Nâng cấp lên Tổ chức và nhận một tên miền phụ, thanh toán hợp nhất, Insights, nhãn trắng mở rộng và nhiều hơn nữa",
"create_your_enterprise_description": "Nâng cấp lên Enterprise và truy cập vào Đồng bộ Active Directory, SCIM Tự động cung cấp người dùng, Cal.ai Voice Agents, Admin APIs và nhiều hơn nữa!",
"other_payment_app_enabled": "Bạn chỉ có thể kích hoạt một ứng dụng thanh toán cho mỗi loại sự kiện",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "Bằng việc tiếp tục, bạn đồng ý với",
"terms": "Điều khoản",
"always_show_x_days": "Luôn hiển thị {{x}} ngày có sẵn",
"always_show_x_days_description": "Tự động di chuyển cửa sổ đặt lịch {{x}} ngày mỗi ngày, đảm bảo lịch của bạn luôn hiển thị {{x}} ngày tiếp theo có các khung giờ trống.",
"team_subscription_cancelled_successfully": "Đã hủy đăng ký nhóm thành công",
"team_subscription_cancellation_error": "Lỗi khi hủy đăng ký nhóm",
"unable_to_subscribe_to_the_platform": "Đã xảy ra lỗi khi cố gắng đăng ký gói nền tảng, vui lòng thử lại sau",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "Kết thúc bằng",
"filter_operator_is_empty": "Trống",
"filter_operator_not_empty": "Không trống",
"translate_description_button": "Dùng AI để dịch tiêu đề/mô tả sang ngôn ngữ trình duyệt của khách",
"translate_description_button": "Tự động dịch tiêu đề và mô tả",
"translate_description_button_description": "Tự động dịch tiêu đề và mô tả sang ngôn ngữ trình duyệt của khách truy cập bằng AI.",
"rr_distribution_method": "Phân phối",
"rr_distribution_method_description": "Cho phép tối ưu hóa việc phân phối để đạt khả năng sẵn sàng tối đa hoặc hướng đến việc phân công cân bằng hơn.",
"rr_distribution_method_availability_title": "Tối đa hóa khả năng sẵn sàng",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "Tính năng chỗ ngồi không hỗ trợ sự kiện định kỳ. Hãy tắt tính năng chỗ ngồi để đặt thành định kỳ.",
"recurring_event_seats_error": "Sự kiện định kỳ không hỗ trợ tính năng chỗ ngồi. Hãy tắt tính năng chỗ ngồi hoặc đặt sự kiện thành không định kỳ.",
"segment": "Phân đoạn",
"saved_filters": "Bộ lọc đã lưu",
"rename": "Đổi tên",
"rename_segment": "Đổi tên phân đoạn",
"delete_segment": "Xóa phân đoạn",
@@ -3338,6 +3345,7 @@
"picklist": "Danh sách chọn",
"most_cancelled_bookings": "Bị hủy nhiều nhất",
"interface_language": "Ngôn ngữ giao diện",
"interface_language_description": "Đặt ngôn ngữ ưa thích cho giao diện đặt lịch",
"visitors_browser_language": "Ngôn ngữ trình duyệt của khách",
"name_or_email": "Tên hoặc email",
"salesforce_round_robin_skip_fallback_to_lead_owner": "Nếu không tìm thấy liên hệ, chuyển sang chủ sở hữu khách hàng tiềm năng nếu tồn tại",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "Chỉnh sửa và điều chỉnh cài đặt tiến độ công việc",
"pbac_desc_delete_workflows": "Xóa tiến độ công việc khỏi hệ thống",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "Lịch khả dụng",
"pbac_resource_out_of_office": "Vắng mặt",
"pbac_desc_create_webhooks": "Tạo webhooks",
"pbac_desc_view_webhooks": "Xem webhooks",
"pbac_desc_update_webhooks": "Cập nhật webhooks",
"pbac_desc_delete_webhooks": "Xóa webhooks",
"pbac_desc_create_availability": "Tạo lịch khả dụng",
"pbac_desc_view_availability": "Xem lịch khả dụng",
"pbac_desc_update_availability": "Cập nhật lịch khả dụng",
"pbac_desc_delete_availability": "Xóa lịch khả dụng",
"pbac_desc_create_out_of_office": "Tạo trạng thái vắng mặt",
"pbac_desc_view_out_of_office": "Xem trạng thái vắng mặt",
"pbac_desc_update_out_of_office": "Cập nhật trạng thái vắng mặt",
"pbac_desc_delete_out_of_office": "Xóa trạng thái vắng mặt",
"pbac_desc_manage_workflows": "Quyền truy cập quản lý đầy đủ đối với tất cả tiến độ công việc",
"pbac_desc_create_event_types": "Tạo loại sự kiện",
"pbac_desc_view_event_types": "Xem loại sự kiện",
@@ -321,6 +321,7 @@
"secret_meeting": "私密会议",
"login_instead": "改为登录",
"create_account": "创建账户",
"get_started": "开始使用",
"confirm_password": "确认密码",
"reset_your_password": "按照发送到您的电子邮件地址的说明设置新密码。",
"org_banner_instructions": "请上传一张宽为 {{width}},高为 {{height}} 的照片。",
@@ -958,7 +959,7 @@
"light_event_type_color": "事件类型颜色(浅色主题)",
"dark_event_type_color": "事件类型颜色(深色主题)",
"file_not_named": "文件名不是 [idOrSlug]/[user]",
"create_team": "新建团队",
"create_team": "创建团队",
"name": "名称",
"nameless_team": "未命名团队",
"oauth_clients": "OAuth 客户端",
@@ -1019,6 +1020,7 @@
"attendees": "参与者",
"add_attendees": "添加参与者",
"show_advanced_settings": "显示高级设置",
"hide_advanced_settings": "隐藏高级设置",
"event_name": "活动名称",
"event_name_in_calendar": "日历中的活动名称",
"event_name_tooltip": "在日历中显示的名称",
@@ -1231,7 +1233,7 @@
"contact_sales": "联系销售",
"error_404": "错误 404",
"default": "默认值",
"set_to_default": "设置为默认值",
"set_to_default": "设置为默认",
"new_schedule_btn": "新建时间表",
"add_new_schedule": "添加新的时间表",
"add_new_calendar": "添加新日历",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "开启此选项后,您的团队成员将无法看到其他团队成员。",
"you_cannot_see_team_members": "您无法看到私密团队的所有团队成员。",
"you_cannot_see_teams_of_org": "您无法看到私有组织的团队。",
"allow_booker_to_select_duration": "允许预约者选择时长",
"allow_multiple_durations": "允许多种时长",
"impersonate_user_tip": "此功能的所有使用都已审核。",
"impersonating_user_warning": "正在模拟用户名“{{user}}”。",
"impersonating_stop_instructions": "点击此处停止",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "预约确认失败",
"not_enough_seats": "位置不足",
"form_builder_field_already_exists": "具有此名称的字段已存在",
"guests_field_must_be_multiemail": "访客字段必须为“多个邮箱”类型",
"show_on_booking_page": "在预约页面上显示",
"visit_cancelled_booking": "您可以访问已取消的预订页面",
"get_started_zapier_templates": "开始使用 Zapier 模板",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "被团队管理员锁定",
"app_not_connected": "您尚未连接 {{appName}} 帐户。",
"connect_now": "立即连接",
"connect_now_unavailable_tooltip": "“立即连接”当前不可用。请从上方的时间段中预订或稍后再试。",
"managed_event_dialog_confirm_button_one": "替换并通知 {{count}} 个成员",
"managed_event_dialog_confirm_button_other": "替换并通知 {{count}} 个成员",
"managed_event_dialog_title_one": "{{count}} 个成员已具有链接/{{slug}}。是否要将其替换?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "继续即表示您同意我们的",
"terms": "条款",
"always_show_x_days": "始终显示{{x}}天的可用时间",
"always_show_x_days_description": "自动将{{x}}天的预订窗口每天向前移动,确保您的日历始终显示接下来{{x}}天的可用时间段。",
"team_subscription_cancelled_successfully": "团队订阅已成功取消",
"team_subscription_cancellation_error": "取消团队订阅时出错",
"unable_to_subscribe_to_the_platform": "尝试订阅平台计划时发生错误,请稍后再试",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "结尾为",
"filter_operator_is_empty": "为空",
"filter_operator_not_empty": "不为空",
"translate_description_button": "使用AI将标题/描述自动翻译为访问者浏览器的语言",
"translate_description_button": "自动翻译标题和描述",
"translate_description_button_description": "使用AI自动将标题和描述翻译为访客浏览器的语言。",
"rr_distribution_method": "分配方式",
"rr_distribution_method_description": "可以优化分配方式以实现最大可用性或更均衡的任务分配。",
"rr_distribution_method_availability_title": "最大化可用性",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "座位功能不支持重复活动。禁用座位功能以使其成为重复活动。",
"recurring_event_seats_error": "重复活动不支持座位功能。请禁用座位功能或将活动设置为非重复。",
"segment": "细分群组",
"saved_filters": "已保存的筛选器",
"rename": "重命名",
"rename_segment": "重命名细分群组",
"delete_segment": "删除细分群组",
@@ -3338,6 +3345,7 @@
"picklist": "选择列表",
"most_cancelled_bookings": "取消最多的预约",
"interface_language": "界面语言",
"interface_language_description": "设置预订界面的首选语言",
"visitors_browser_language": "访客的浏览器语言",
"name_or_email": "姓名或电子邮箱",
"salesforce_round_robin_skip_fallback_to_lead_owner": "如果未找到联系人,则在存在潜在客户所有者的情况下回退到潜在客户所有者",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "编辑和修改工作流程设置",
"pbac_desc_delete_workflows": "从系统中移除工作流程",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "可用性",
"pbac_resource_out_of_office": "外出",
"pbac_desc_create_webhooks": "创建 Webhook",
"pbac_desc_view_webhooks": "查看 Webhook",
"pbac_desc_update_webhooks": "更新 Webhooks",
"pbac_desc_delete_webhooks": "删除 Webhooks",
"pbac_desc_create_availability": "创建可用性",
"pbac_desc_view_availability": "查看可用性",
"pbac_desc_update_availability": "更新可用性",
"pbac_desc_delete_availability": "删除可用性",
"pbac_desc_create_out_of_office": "创建外出",
"pbac_desc_view_out_of_office": "查看外出",
"pbac_desc_update_out_of_office": "更新外出",
"pbac_desc_delete_out_of_office": "删除外出",
"pbac_desc_manage_workflows": "全面管理所有工作流程的权限",
"pbac_desc_create_event_types": "创建活动类型",
"pbac_desc_view_event_types": "查看活动类型",
@@ -321,6 +321,7 @@
"secret_meeting": "秘密會議",
"login_instead": "或登入",
"create_account": "新增帳號",
"get_started": "開始使用",
"confirm_password": "確認密碼",
"reset_your_password": "按照傳送至您電子郵件地址的指示來重設密碼。",
"org_banner_instructions": "請上傳寬度為 {{width}} 高度為 {{height}} 的圖片。",
@@ -958,7 +959,7 @@
"light_event_type_color": "事件類型顏色(淺色主題)",
"dark_event_type_color": "事件類型顏色(深色主題)",
"file_not_named": "檔案名稱不叫做 [idOrSlug]/[user]",
"create_team": "新增團隊",
"create_team": "建立團隊",
"name": "名稱",
"nameless_team": "未命名團隊",
"oauth_clients": "OAuth 客戶端",
@@ -1019,6 +1020,7 @@
"attendees": "與會者",
"add_attendees": "加入與會者",
"show_advanced_settings": "顯示進階設定",
"hide_advanced_settings": "隱藏進階設定",
"event_name": "活動名稱",
"event_name_in_calendar": "行事曆中的活動名稱",
"event_name_tooltip": "會顯示在行事曆上的名字",
@@ -1329,7 +1331,7 @@
"make_team_private_description": "啟用此設定時,團隊成員將無法查看其他團隊成員。",
"you_cannot_see_team_members": "您無法查看私人團隊的所有團隊成員。",
"you_cannot_see_teams_of_org": "您無法看到私人組織的團隊。",
"allow_booker_to_select_duration": "允許預約者選擇時間長度",
"allow_multiple_durations": "允許多種時長",
"impersonate_user_tip": "此功能的所有使用已經過稽核。",
"impersonating_user_warning": "正在模擬使用者名稱「{{user}}」。",
"impersonating_stop_instructions": "按一下此處以停止。",
@@ -2202,6 +2204,7 @@
"booking_confirmation_failed": "預約確認失敗",
"not_enough_seats": "座位不足",
"form_builder_field_already_exists": "已存在使用此名稱的欄位",
"guests_field_must_be_multiemail": "來賓欄位必須是「多個電子郵件」類型",
"show_on_booking_page": "在預約頁面上顯示",
"visit_cancelled_booking": "您可以訪問已取消的預訂頁面",
"get_started_zapier_templates": "開始使用 Zapier 範本",
@@ -2298,6 +2301,7 @@
"locked_by_team_admin": "已由團隊管理員鎖定",
"app_not_connected": "您尚未連結 {{appName}} 帳號。",
"connect_now": "立即連結",
"connect_now_unavailable_tooltip": "立即連接目前無法使用。請從上方時段預訂或稍後再試。",
"managed_event_dialog_confirm_button_one": "取代並通知 {{count}} 位成員",
"managed_event_dialog_confirm_button_other": "取代並通知 {{count}} 位成員",
"managed_event_dialog_title_one": "{{count}} 位成員已有網址 /{{slug}}。是否要替換?",
@@ -2922,6 +2926,7 @@
"proceeding_agreement": "繼續即表示您同意我們的",
"terms": "條款",
"always_show_x_days": "總是顯示 {{x}} 天的可用時間",
"always_show_x_days_description": "自動將{{x}}天的預訂視窗每天向前移動,確保您的日曆始終顯示接下來{{x}}天的可用時段。",
"team_subscription_cancelled_successfully": "團隊訂閱已成功取消",
"team_subscription_cancellation_error": "取消團隊訂閱時出錯",
"unable_to_subscribe_to_the_platform": "嘗試訂閱平台計劃時發生錯誤,請稍後再試",
@@ -3143,7 +3148,8 @@
"filter_operator_ends_with": "結尾為",
"filter_operator_is_empty": "為空",
"filter_operator_not_empty": "不為空",
"translate_description_button": "使用 AI 將標題/描述翻譯成訪客瀏覽器的語言",
"translate_description_button": "自動翻譯標題和描述",
"translate_description_button_description": "使用 AI 自動將標題和描述翻譯為訪客瀏覽器的語言。",
"rr_distribution_method": "分配方式",
"rr_distribution_method_description": "可以優化分配方式以達到最大可用性或更平衡的任務分配。",
"rr_distribution_method_availability_title": "最大化可用性",
@@ -3309,6 +3315,7 @@
"seats_doesnt_support_recurring": "座位功能不支援重複活動。請停用座位功能以設為重複活動。",
"recurring_event_seats_error": "重複活動不支援座位功能。請停用座位功能或將活動設為非重複活動。",
"segment": "區隔",
"saved_filters": "已儲存的篩選條件",
"rename": "重新命名",
"rename_segment": "重新命名區隔",
"delete_segment": "刪除區隔",
@@ -3338,6 +3345,7 @@
"picklist": "選項清單",
"most_cancelled_bookings": "最多取消的預約",
"interface_language": "介面語言",
"interface_language_description": "設定預訂介面的偏好語言",
"visitors_browser_language": "訪客的瀏覽器語言",
"name_or_email": "姓名或電子郵件",
"salesforce_round_robin_skip_fallback_to_lead_owner": "如果找不到聯絡人,則回退到潛在客戶擁有者(如果存在)",
@@ -3509,10 +3517,20 @@
"pbac_desc_update_workflows": "編輯和修改工作流程設定",
"pbac_desc_delete_workflows": "從系統中移除工作流程",
"pbac_resource_webhook": "Webhook",
"pbac_resource_availability": "可用性",
"pbac_resource_out_of_office": "不在辦公室",
"pbac_desc_create_webhooks": "建立 Webhook",
"pbac_desc_view_webhooks": "查看 Webhook",
"pbac_desc_update_webhooks": "更新 Webhooks",
"pbac_desc_delete_webhooks": "刪除 Webhooks",
"pbac_desc_create_availability": "建立可用性",
"pbac_desc_view_availability": "檢視可用性",
"pbac_desc_update_availability": "更新可用性",
"pbac_desc_delete_availability": "刪除可用性",
"pbac_desc_create_out_of_office": "建立外出辦公",
"pbac_desc_view_out_of_office": "檢視外出辦公",
"pbac_desc_update_out_of_office": "更新外出辦公",
"pbac_desc_delete_out_of_office": "刪除外出辦公",
"pbac_desc_manage_workflows": "完全管理所有工作流程的存取權限",
"pbac_desc_create_event_types": "建立活動類型",
"pbac_desc_view_event_types": "檢視活動類型",
@@ -3560,7 +3578,7 @@
"rr_load_balancing_disabled_with_groups": "負載平衡無法與 Round Robin 群組一起使用",
"round_robin_settings_description": "自訂此團隊的預設輪詢設定",
"round_robin_settings_updated_successfully": "輪詢設定已成功更新",
"booking_creation_time": "預約建立時間",
"booking_creation_time": "預訂建立時間",
"meeting_start_time": "會議開始時間",
"members_affected_by_disabling_delegation_credential": "受影響的成員",
"no_members_affected_by_disabling_delegation_credential": "停用委派憑證未影響任何成員",
-4
View File
@@ -640,10 +640,6 @@ select:focus {
@apply !border-r-default left-0.5 !border-y-0 !border-l-0;
}
.intercom-lightweight-app {
@apply z-40 !important;
}
[data-radix-popper-content-wrapper] {
border: none;
}
+14 -10
View File
@@ -1,12 +1,17 @@
import prismaMock from "../../../../tests/libs/__mocks__/prismaMock";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import dayjs from "@calcom/dayjs";
import { getCheckBookingLimitsService } from "@calcom/lib/di/containers/BookingLimits";
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
import { validateIntervalLimitOrder } from "@calcom/lib/intervalLimits/validateIntervalLimitOrder";
const mockCountBookingsByEventTypeAndDateRange = vi.fn();
vi.mock("@calcom/lib/server/repository/booking", () => ({
BookingRepository: vi.fn().mockImplementation(() => ({
countBookingsByEventTypeAndDateRange: mockCountBookingsByEventTypeAndDateRange,
})),
}));
type Mockdata = {
id: number;
startDate: Date;
@@ -25,21 +30,20 @@ const checkBookingLimitsService = getCheckBookingLimitsService();
describe("Check Booking Limits Tests", () => {
it("Should return no errors", async () => {
prismaMock.booking.count.mockResolvedValue(0);
mockCountBookingsByEventTypeAndDateRange.mockResolvedValue(0);
expect(
checkBookingLimitsService.checkBookingLimits(MOCK_DATA.bookingLimits, MOCK_DATA.startDate, MOCK_DATA.id)
).resolves.toBeTruthy();
});
it("Should throw an error", async () => {
// Mock there being two a day
prismaMock.booking.count.mockResolvedValue(2);
mockCountBookingsByEventTypeAndDateRange.mockResolvedValue(2);
expect(
checkBookingLimitsService.checkBookingLimits(MOCK_DATA.bookingLimits, MOCK_DATA.startDate, MOCK_DATA.id)
).rejects.toThrowError();
});
it("Should pass with multiple booking limits", async () => {
prismaMock.booking.count.mockResolvedValue(0);
mockCountBookingsByEventTypeAndDateRange.mockResolvedValue(0);
expect(
checkBookingLimitsService.checkBookingLimits(
{
@@ -52,7 +56,7 @@ describe("Check Booking Limits Tests", () => {
).resolves.toBeTruthy();
});
it("Should pass with multiple booking limits with one undefined", async () => {
prismaMock.booking.count.mockResolvedValue(0);
mockCountBookingsByEventTypeAndDateRange.mockResolvedValue(0);
expect(
checkBookingLimitsService.checkBookingLimits(
@@ -66,7 +70,7 @@ describe("Check Booking Limits Tests", () => {
).resolves.toBeTruthy();
});
it("Should handle multiple limits correctly", async () => {
prismaMock.booking.count.mockResolvedValue(1);
mockCountBookingsByEventTypeAndDateRange.mockResolvedValue(1);
expect(
checkBookingLimitsService.checkBookingLimit({
key: "PER_DAY",
@@ -75,7 +79,7 @@ describe("Check Booking Limits Tests", () => {
eventId: MOCK_DATA.id,
})
).resolves.not.toThrow();
prismaMock.booking.count.mockResolvedValue(3);
mockCountBookingsByEventTypeAndDateRange.mockResolvedValue(3);
expect(
checkBookingLimitsService.checkBookingLimit({
key: "PER_WEEK",
+8
View File
@@ -4,6 +4,14 @@
"path": "/api/cron/calendar-cache-cleanup",
"schedule": "0 5 * * *"
},
{
"path": "/api/cron/calendar-subscriptions",
"schedule": "*/5 * * * *"
},
{
"path": "/api/cron/calendar-subscriptions-cleanup",
"schedule": "0 3 * * *"
},
{
"path": "/api/cron/queuedFormResponseCleanup",
"schedule": "0 */12 * * *"
+25 -7
View File
@@ -323,6 +323,7 @@ checksums:
secret_meeting: b22ebd9b53662182d95749b3a6b5d3de
login_instead: 4e7a13b6c39065d8e093c554345ccd7c
create_account: 3c3c9666b2acc1404eaa89e34e17f946
get_started: 5c783951b0100a168bdd2161ff294833
confirm_password: b5cdb861d8deb598b0613d48098b82b6
reset_your_password: 092dcca439cd79d85489ef004cdfdcc1
org_banner_instructions: 3d1e6ab6ae5844ff11bfc012cf62de1b
@@ -960,7 +961,7 @@ checksums:
light_event_type_color: 6144591373599b4d1fc43f354c81680c
dark_event_type_color: 2a620447632a5238af304040cb605f41
file_not_named: 42e93dfbf3037148e169df7f873bb536
create_team: d3cba7d5fb6932d919f9dfad7a1e9d9f
create_team: c1d3e864b66be9cb2e15d66509b17a23
name: 9368b5a047572b6051f334af5aa76819
nameless_team: 44bfe8692c3839f1fd10459239409a30
oauth_clients: 2227d1efbcc881c6950c9f89defc3ef6
@@ -1021,6 +1022,7 @@ checksums:
attendees: 44796e0b664cc408404b1bc3bb32752b
add_attendees: 6346e21f1a597f17b1d8274b01009320
show_advanced_settings: e62afb3ad1a9c06c7b4645644c85c8e0
hide_advanced_settings: ffa251d7762030b72c12e92f3c69a9b4
event_name: e9ee5be0bc00804cc48c982d2ad1f849
event_name_in_calendar: a0ba9c8a87a892b060579bd2dc697a43
event_name_tooltip: a00ef459ffb3ee9c2660b4125a9afdea
@@ -1233,7 +1235,7 @@ checksums:
contact_sales: 94537da970ffef6210bba8fcecb74a8c
error_404: 978c8521afd0be2ce95291e6920c7cfd
default: d9c6dc5c412fe94143dfd1d332ec81d4
set_to_default: 538a1fe3974e84a1e2726f00c7306598
set_to_default: 5b7c2d3d837cfdfc3df1d435fdf2796f
new_schedule_btn: 3ed90ee5292a8df7e1f5b13f0a132df9
add_new_schedule: d0b75dad5cd4a7da4284262729b5cc49
add_new_calendar: cf55065ffc4259b906d6af6ce45b85cf
@@ -1330,7 +1332,7 @@ checksums:
make_team_private_description: c48915c8357c3687506183e92e6aa155
you_cannot_see_team_members: dc7a5d20692b36f17081d7a10e66821c
you_cannot_see_teams_of_org: c6dafa34320944aa57a5f4053541c092
allow_booker_to_select_duration: b93ab67d353eb91e9220b6b92fd1533c
allow_multiple_durations: 7fac476934a8e73494fb0a2267015082
impersonate_user_tip: b185a4d43f5d2bd47d90c3118a9e6a4a
impersonating_user_warning: 2df022b91992a4c819abee8ca62cdf1c
impersonating_stop_instructions: fc01010bfe9c16de03a6311a3a146f7c
@@ -2199,6 +2201,7 @@ checksums:
booking_confirmation_failed: 59daa6c4b4bcfff03dca5fb3776d5c29
not_enough_seats: f2c120d55643caa712a0ef44350e56af
form_builder_field_already_exists: 623a2903bef6bc840794893083969567
guests_field_must_be_multiemail: 68c9b9f5a64081e10388f75efafce2b3
show_on_booking_page: 515d42a965a615916eb9299f9a8ba698
visit_cancelled_booking: 42c1a6ee7e33ca49661c4e4763feb9e3
get_started_zapier_templates: 0adfac5a026b181a55ff131b9572b5ab
@@ -2295,6 +2298,7 @@ checksums:
locked_by_team_admin: 539128c9579f340b95d58abbfa0880ef
app_not_connected: 25c979da79e9f983e6f5e62dcd1c47bb
connect_now: ca98ba0375aad91861c150bbc0fff9ec
connect_now_unavailable_tooltip: f259b61c574fd191048ddce748b87829
managed_event_dialog_confirm_button_one: 57569c5943d7cf44f3373f763559a0d8
managed_event_dialog_confirm_button_other: d665ecde0909425ff7433b417c45ada3
managed_event_dialog_title_one: 11c192644024dffc351d11d9681a81f9
@@ -2685,7 +2689,7 @@ checksums:
enterprise: c70474271708e5b27392fde87462fa26
enterprise_description: cd3740d29ced9d8f2cd985ffa9677072
create_your_org: 0ac13e92f5227b4f08522707f911fd61
create_org: a1164cc2ec95880ee05e9c06bce99bd8
create_org: e79a6de15033cc09a320a6f9c294d696
create_your_org_description: d3c2a2cba0ce932123704baca89bb7d4
create_your_enterprise_description: 42426099bf97d49dea548ab03f22e7b9
other_payment_app_enabled: 2630450e1fbac6809df1cda21434ef5c
@@ -2919,6 +2923,7 @@ checksums:
proceeding_agreement: ee92983ae1f9fe1ecd3fe0f5e28761db
terms: c531b88ecfe7dd2337a34f4a9ed18a5f
always_show_x_days: a37d937d9387b7df0a32bcd89b9c346a
always_show_x_days_description: 5b9145d7f9e87a2bb3b5277461ce432e
team_subscription_cancelled_successfully: 7ef5e0dbc3db34b1705086116d38c05c
team_subscription_cancellation_error: 9a17ccdcd12aadb0c846f5b5bd207973
unable_to_subscribe_to_the_platform: 13bcc85e03aff904d15b41ee85f0043f
@@ -3140,7 +3145,8 @@ checksums:
filter_operator_ends_with: c8a5f60f1bd1d8fa018dbbf49806fb5b
filter_operator_is_empty: dca87bc415341b1cdf9523f3b795a313
filter_operator_not_empty: 303caa93dcb79e54d47d4cd7d748a1df
translate_description_button: 659cad3e743ba1b6a554fc8007a9dbfb
translate_description_button: 9fdee8d44e1949e75a5dab7dfa1afcab
translate_description_button_description: 129577a213f0c9cb5596bf05b230c9a5
rr_distribution_method: c3d4633236c01f8314a38a9e9c2361f1
rr_distribution_method_description: e9c1dda0299fbfc979a52a79b57e40c7
rr_distribution_method_availability_title: 3a7c8afaf3ddb8d262e2a03d7fc8779d
@@ -3306,6 +3312,7 @@ checksums:
seats_doesnt_support_recurring: efd91eb0c11cfedd08d794ff14b96bdd
recurring_event_seats_error: 39819a065f63e077ac7373274ce416c3
segment: e8908115453de180bbda7478ba4c2d50
saved_filters: f77e3389ad77a947e8be0cd18f8a6328
rename: 6f9413e4606e8d83a010685008fb2931
rename_segment: 7b8f51ad24c411fe48045039d357d793
delete_segment: 7dd5b27c80d5748c50d17d7ad8a96113
@@ -3335,6 +3342,7 @@ checksums:
picklist: 5a3d44808605be31190c9956ecc5e412
most_cancelled_bookings: 5470c614919f530c8e308d664c5170fa
interface_language: a5c8eaac0640a27d6805c86b3faecd16
interface_language_description: a0189e533e016c6f1ed0118f321df6c7
visitors_browser_language: bda926411281203feec4334bc43a15f6
name_or_email: d6e1cc7612211a2f7080b5096510f282
salesforce_round_robin_skip_fallback_to_lead_owner: 95ba66cdc5f1422c48d907e9d4d442b9
@@ -3506,10 +3514,20 @@ checksums:
pbac_desc_update_workflows: aecf636063ef3b8a5ece8d6381e053f0
pbac_desc_delete_workflows: fabfe2406f079e08cf4b017afca797bd
pbac_resource_webhook: 70f95b2c27f2c3840b500fcaf79ee83c
pbac_resource_availability: f141aa3e9adb2179806cc1c909b5516a
pbac_resource_out_of_office: 9def361368ed730df37c884fe16d72e5
pbac_desc_create_webhooks: 3df3e99c62fd661d12ae4d1817deec6c
pbac_desc_view_webhooks: c3c1a5113700fc39aff9596d29371ce5
pbac_desc_update_webhooks: efe117c23053d397589dd9bc7e18d6dd
pbac_desc_delete_webhooks: 7877bc06e4a68ba21abf5ff702416b79
pbac_desc_create_availability: d62c426d46c5c004e1dbf8213ab19434
pbac_desc_view_availability: 3f1f2fe1200dd419d6a40cce45ad1d0d
pbac_desc_update_availability: 9a50a74361d3e4743932913e2d469f7b
pbac_desc_delete_availability: abf16f0da6e59c7322dc0d2348fa9b2c
pbac_desc_create_out_of_office: 2c59dbea23e478b2589bc78e8d5c8182
pbac_desc_view_out_of_office: 8e3ded73b455934c2f911296593985f3
pbac_desc_update_out_of_office: 2c080504ede7215d690888f900c75610
pbac_desc_delete_out_of_office: c28cb6f5884ab1db9fa890cea0266b16
pbac_desc_manage_workflows: 9ff3998b178daad7fa575d1277345d31
pbac_desc_create_event_types: 0a58f83b33aa6ba7784de540b3107bbe
pbac_desc_view_event_types: 390e116e8ff1ec3db6dc5079aaa80b7b
@@ -3557,8 +3575,8 @@ checksums:
rr_load_balancing_disabled_with_groups: 21faa6951621aae82853766c44de7ede
round_robin_settings_description: e3700766347dce87bf8bf5829dd7a6b3
round_robin_settings_updated_successfully: d5af777266614256a0f23338515323dc
booking_creation_time: cb184643434b260638743678fe6e5948
meeting_start_time: 99d3983338813d02c553f8f7392aed1f
booking_creation_time: 8a9f5ebc9b9abc341da5d8205a14425f
meeting_start_time: 1ee5753b97e6f589e4a0b9538bc6c73c
members_affected_by_disabling_delegation_credential: 693f7323afd369947660403297a3f1dc
no_members_affected_by_disabling_delegation_credential: 0ec66caddbcbe0c935a75e4114ff6629
download_expense_log: c00ccbe3e58afe31fa0cb6cca10e2b69
+8
View File
@@ -0,0 +1,8 @@
export default {
"(apps|packages)/**/*.{js,ts,jsx,tsx}": [
"prettier --write",
process.env.SKIP_WARNINGS === "1" ? "eslint --fix" : "eslint --fix --max-warnings=0",
],
"*.json": ["prettier --write"],
"packages/prisma/schema.prisma": ["prisma format"],
};
+2 -13
View File
@@ -82,7 +82,8 @@
"docker-stop-api": "docker ps --filter 'ancestor=cal-api' -q | xargs docker stop",
"changesets-add": "yarn changeset add",
"changesets-version": "yarn changeset version",
"changesets-release": "NODE_OPTIONS='--max_old_space_size=12288' turbo run build --filter=@calcom/atoms && yarn changeset publish"
"changesets-release": "NODE_OPTIONS='--max_old_space_size=12288' turbo run build --filter=@calcom/atoms && yarn changeset publish",
"lint-staged": "lint-staged"
},
"devDependencies": {
"@calcom/eslint-config": "workspace:*",
@@ -140,18 +141,6 @@
"dayjs@^1.8.29": "patch:dayjs@npm%3A1.11.4#./.yarn/patches/dayjs-npm-1.11.4-97921cd375.patch",
"import-in-the-middle": "1.13.1"
},
"lint-staged": {
"(apps|packages)/**/*.{js,ts,jsx,tsx}": [
"prettier --write",
"eslint --fix --max-warnings=0"
],
"*.json": [
"prettier --write"
],
"packages/prisma/schema.prisma": [
"prisma format"
]
},
"engines": {
"npm": ">=7.0.0",
"yarn": "3.4.1"
+41 -18
View File
@@ -1,28 +1,17 @@
import { CalendarSubscriptionService } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionService";
import { CalendarCacheEventRepository } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventRepository";
import { CalendarCacheEventService } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheEventService";
import { CalendarCacheWrapper } from "@calcom/features/calendar-subscription/lib/cache/CalendarCacheWrapper";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import logger from "@calcom/lib/logger";
import type { Calendar, CalendarClass } from "@calcom/types/Calendar";
import { prisma } from "@calcom/prisma";
import type { Calendar } from "@calcom/types/Calendar";
import type { CredentialForCalendarService } from "@calcom/types/Credential";
import { CalendarServiceMap } from "../calendar.services.generated";
interface CalendarApp {
lib: {
CalendarService: CalendarClass;
};
}
const log = logger.getSubLogger({ prefix: ["CalendarManager"] });
/**
* @see [Using type predicates](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates)
*/
const isCalendarService = (x: unknown): x is CalendarApp =>
!!x &&
typeof x === "object" &&
"lib" in x &&
typeof x.lib === "object" &&
!!x.lib &&
"CalendarService" in x.lib;
export const getCalendar = async (
credential: CredentialForCalendarService | null
): Promise<Calendar | null> => {
@@ -53,5 +42,39 @@ export const getCalendar = async (
return null;
}
// check if Calendar Cache is supported and enabled
if (CalendarCacheEventService.isCalendarTypeSupported(calendarType)) {
log.debug(
`Using regular CalendarService for credential ${credential.id} (not Google or Office365 Calendar)`
);
const featuresRepository = new FeaturesRepository(prisma);
const [isCalendarSubscriptionCacheEnabled, isCalendarSubscriptionCacheEnabledForUser] = await Promise.all(
[
featuresRepository.checkIfFeatureIsEnabledGlobally(
CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_CACHE_FEATURE
),
featuresRepository.checkIfUserHasFeature(
credential.userId as number,
CalendarSubscriptionService.CALENDAR_SUBSCRIPTION_CACHE_FEATURE
),
]
);
if (isCalendarSubscriptionCacheEnabled && isCalendarSubscriptionCacheEnabledForUser) {
log.debug(`Calendar Cache is enabled, using CalendarCacheService for credential ${credential.id}`);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const originalCalendar = new CalendarService(credential as any);
if (originalCalendar) {
// return cacheable calendar
const calendarCacheEventRepository = new CalendarCacheEventRepository(prisma);
return new CalendarCacheWrapper({
originalCalendar,
calendarCacheEventRepository,
});
}
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return new CalendarService(credential as any);
};
@@ -3,6 +3,7 @@ import { vi } from "vitest";
const featuresRepositoryModuleMock = {
FeaturesRepository: vi.fn().mockImplementation(() => ({
checkIfFeatureIsEnabledGlobally: vi.fn().mockResolvedValue(true),
checkIfUserHasFeature: vi.fn().mockResolvedValue(true),
})),
};
@@ -46,8 +46,14 @@ export class PaymentService implements IAbstractPaymentService {
private async getPayment(where: Prisma.PaymentWhereInput) {
const payment = await prisma.payment.findFirst({ where });
if (!payment) throw new Error("Payment not found");
if (!payment.externalId) throw new Error("Payment externalId not found");
// if payment isn't found, return null.
if (!payment) {
return null;
}
// if it is found, but there's no externalId - it indicates invalid state and an error should be thrown.
if (!payment.externalId) {
throw new Error("Payment externalId not found");
}
return { ...payment, externalId: payment.externalId };
}
@@ -326,13 +332,21 @@ export class PaymentService implements IAbstractPaymentService {
throw new Error("Method not implemented.");
}
async refund(paymentId: Payment["id"]): Promise<Payment> {
async refund(paymentId: Payment["id"]): Promise<Payment | null> {
const payment = await this.getPayment({
id: paymentId,
});
if (!payment) {
return null;
}
if (!payment.success) {
throw new Error("Unable to refund failed payment");
}
if (payment.refunded) {
// refunded already, bail early as success without throwing an error.
return payment;
}
try {
const payment = await this.getPayment({
id: paymentId,
success: true,
refunded: false,
});
const refund = await this.stripe.refunds.create(
{
payment_intent: payment.externalId,
@@ -399,8 +413,12 @@ export class PaymentService implements IAbstractPaymentService {
const payment = await this.getPayment({
id: paymentId,
});
const stripeAccount = (payment.data as unknown as StripePaymentData).stripeAccount;
// no payment found, return false.
if (!payment) {
return false;
}
const stripeAccount = (payment.data as unknown as StripePaymentData).stripeAccount;
if (!stripeAccount) {
throw new Error("Stripe account not found");
}
@@ -61,7 +61,7 @@ export const TeamInviteEmail = (
<div style={{ display: "flex", justifyContent: "center" }}>
<CallToAction
label={props.language(
props.isCalcomMember ? (props.isAutoJoin ? "login" : "email_user_cta") : "create_your_account"
props.isCalcomMember ? (props.isAutoJoin ? "login" : "accept_invite") : "join_now"
)}
href={props.joinLink}
endIconName="linkIcon"
+1
View File
@@ -22,6 +22,7 @@ export const config = [
},
rules: {
"turbo/no-undeclared-env-vars": "warn",
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
},
languageOptions: {
ecmaVersion: 2021,
@@ -1,5 +1,5 @@
import type { MembershipRole } from "@calcom/prisma/enums";
import { MembershipRole } from "@calcom/prisma/enums";
export function checkAdminOrOwner(role: MembershipRole | null | undefined): role is "OWNER" | "ADMIN" {
return role === "OWNER" || role === "ADMIN";
return role === MembershipRole.OWNER || role === MembershipRole.ADMIN;
}
@@ -39,9 +39,9 @@ import { ProfileRepository } from "@calcom/lib/server/repository/profile";
import { UserRepository } from "@calcom/lib/server/repository/user";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import type { Membership, Team, UserPermissionRole } from "@calcom/prisma/client";
import type { Membership, Team } from "@calcom/prisma/client";
import { CreationSource } from "@calcom/prisma/enums";
import { IdentityProvider, MembershipRole } from "@calcom/prisma/enums";
import { IdentityProvider, MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
import { getOrgUsernameFromEmail } from "../signup/utils/getOrgUsernameFromEmail";
@@ -249,7 +249,7 @@ const providers: Provider[] = [
// authentication success- but does it meet the minimum password requirements?
const validateRole = (role: UserPermissionRole) => {
// User's role is not "ADMIN"
if (role !== "ADMIN") return role;
if (role !== UserPermissionRole.ADMIN) return role;
// User's identity provider is not "CAL"
if (user.identityProvider !== IdentityProvider.CAL) return role;
+11 -4
View File
@@ -92,7 +92,6 @@ const BookerComponent = ({
);
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
const setSelectedDate = useBookerStoreContext((state) => state.setSelectedDate);
const {
shouldShowFormInDialog,
@@ -145,7 +144,14 @@ const BookerComponent = ({
const { bookerFormErrorRef, key, formEmail, bookingForm, errors: formErrors } = bookerForm;
const { handleBookEvent, errors, loadingStates, expiryTime, instantVideoMeetingUrl } = bookings;
const {
handleBookEvent,
errors,
loadingStates,
expiryTime,
instantVideoMeetingUrl,
instantConnectCooldownMs,
} = bookings;
const watchedCfToken = bookingForm.watch("cfToken");
@@ -230,9 +236,9 @@ const BookerComponent = ({
setSelectedTimeslot(slot || null);
}, [slot, setSelectedTimeslot]);
const onSubmit = (timeSlot?: string) => {
const onSubmit = (timeSlot?: string) =>
renderConfirmNotVerifyEmailButtonCond ? handleBookEvent(timeSlot) : handleVerifyEmail();
};
const EventBooker = useMemo(() => {
return bookerState === "booking" ? (
@@ -543,6 +549,7 @@ const BookerComponent = ({
style={{ animationDelay: "1s" }}>
<InstantBooking
event={event.data}
cooldownMs={instantConnectCooldownMs}
onConnectNow={() => {
onConnectNowInstantMeeting();
}}
@@ -3,16 +3,19 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { User } from "@calcom/prisma/client";
import { UserAvatarGroupWithOrg } from "@calcom/ui/components/avatar";
import { Button } from "@calcom/ui/components/button";
import { Tooltip } from "@calcom/ui/components/tooltip";
interface IInstantBookingProps {
onConnectNow: () => void;
event: Pick<BookerEvent, "entity" | "schedulingType"> & {
subsetOfUsers: (Pick<User, "name" | "username" | "avatarUrl"> & { bookerUrl: string })[];
};
cooldownMs?: number;
}
export const InstantBooking = ({ onConnectNow, event }: IInstantBookingProps) => {
export const InstantBooking = ({ onConnectNow, event, cooldownMs = 0 }: IInstantBookingProps) => {
const { t } = useLocale();
const disabled = cooldownMs > 0;
return (
<div className=" bg-default border-subtle mx-2 block items-center gap-3 rounded-xl border p-[6px] text-sm shadow-sm delay-1000 sm:flex">
@@ -33,16 +36,33 @@ export const InstantBooking = ({ onConnectNow, event }: IInstantBookingProps) =>
</div>
<div>{t("dont_want_to_wait")}</div>
</div>
<div className="mt-2 sm:mt-0">
<Button
color="primary"
onClick={() => {
onConnectNow();
}}
size="sm"
className="w-full justify-center rounded-lg sm:w-auto">
{t("connect_now")}
</Button>
<div className="mt-2 sm:mt-0 flex items-center gap-3">
{disabled ? (
<Tooltip content={t("connect_now_unavailable_tooltip") || "Connect now is not available right now."}>
<span className="inline-flex">
<Button
disabled={disabled}
color="primary"
onClick={() => {
onConnectNow();
}}
size="sm"
className="w-full justify-center rounded-lg sm:w-auto">
{t("connect_now")}
</Button>
</span>
</Tooltip>
) : (
<Button
color="primary"
onClick={() => {
onConnectNow();
}}
size="sm"
className="w-full justify-center rounded-lg sm:w-auto">
{t("connect_now")}
</Button>
)}
</div>
</div>
);
@@ -114,6 +114,43 @@ export interface IUseBookingErrors {
export type UseBookingsReturnType = ReturnType<typeof useBookings>;
const STORAGE_KEY = "instantBookingData";
const COOLDOWN_STORAGE_KEY = "instantBookingCooldownByEvent";
const COOLDOWN_WINDOW_MS = 10 * 60 * 1000; // 10 minutes
type InstantBookingCooldownMap = Record<string, number>;
const readInstantCooldownMap = (): InstantBookingCooldownMap => {
try {
const raw = localStorage.getItem(COOLDOWN_STORAGE_KEY);
return raw ? (JSON.parse(raw) as InstantBookingCooldownMap) : {};
} catch {
return {};
}
};
const writeInstantCooldownMap = (map: InstantBookingCooldownMap) => {
try {
localStorage.setItem(COOLDOWN_STORAGE_KEY, JSON.stringify(map));
} catch {
// don't do anything
}
};
const getInstantCooldownRemainingMs = (eventTypeId?: number | null): number => {
if (!eventTypeId) return 0;
const map = readInstantCooldownMap();
const lastTs = map[String(eventTypeId)];
if (!lastTs) return 0;
const remaining = lastTs + COOLDOWN_WINDOW_MS - Date.now();
return remaining > 0 ? remaining : 0;
};
const setInstantCooldownNow = (eventTypeId?: number | null) => {
if (!eventTypeId) return;
const map = readInstantCooldownMap();
map[String(eventTypeId)] = Date.now();
writeInstantCooldownMap(map);
};
const storeInLocalStorage = ({
eventTypeId,
@@ -133,7 +170,6 @@ export const useBookings = ({
hashedLink,
bookingForm,
metadata,
teamMemberEmail,
isBookingDryRun,
}: IUseBookings) => {
const router = useRouter();
@@ -179,6 +215,8 @@ export const useBookings = ({
}
}, [eventTypeId, isInstantMeeting]);
const instantConnectCooldownMs = getInstantCooldownRemainingMs(eventTypeId);
const _instantBooking = trpc.viewer.bookings.getInstantBookingLocation.useQuery(
{
bookingId: bookingId,
@@ -245,7 +283,7 @@ export const useBookings = ({
const { uid, paymentUid } = booking;
const fullName = getFullName(bookingForm.getValues("responses.name"));
const users = !!event.data?.subsetOfHosts?.length
const users = event.data?.subsetOfHosts?.length
? event.data?.subsetOfHosts.map((host) => host.user)
: event.data?.subsetOfUsers;
@@ -368,6 +406,7 @@ export const useBookings = ({
expiryTime: responseData.expires,
bookingId: responseData.bookingId,
});
setInstantCooldownNow(eventTypeId);
}
updateQueryParam("bookingId", responseData.bookingId);
@@ -472,7 +511,19 @@ export const useBookings = ({
bookingForm,
hashedLink,
metadata,
handleInstantBooking: createInstantBookingMutation.mutate,
handleInstantBooking: (
variables: Parameters<typeof createInstantBookingMutation.mutate>[0]
) => {
const remaining = getInstantCooldownRemainingMs(eventTypeId);
if (remaining > 0) {
showToast(
t("please_try_again_later_or_book_another_slot"),
"error"
);
return;
}
createInstantBookingMutation.mutate(variables);
},
handleRecBooking: createRecurringBookingMutation.mutate,
handleBooking: createBookingMutation.mutate,
isBookingDryRun,
@@ -506,5 +557,6 @@ export const useBookings = ({
errors,
loadingStates,
instantVideoMeetingUrl,
instantConnectCooldownMs,
};
};
@@ -1,4 +1,4 @@
// eslint-disable-next-line no-restricted-imports
import short, { uuid } from "short-uuid";
import { v5 as uuidv5 } from "uuid";
@@ -403,6 +403,9 @@ export type BookingHandlerInput = {
// These used to come from headers but now we're passing them as params
hostname?: string;
forcedSlug?: string;
skipAvailabilityCheck?: boolean;
skipEventLimitsCheck?: boolean;
skipCalendarSyncTaskCreation?: boolean;
} & PlatformParams;
function formatAvailabilitySnapshot(data: {
@@ -437,6 +440,9 @@ async function handler(
hostname,
forcedSlug,
areCalendarEventsEnabled = true,
skipAvailabilityCheck = false,
skipEventLimitsCheck = false,
skipCalendarSyncTaskCreation = false,
} = input;
const isPlatformBooking = !!platformClientId;
@@ -511,7 +517,7 @@ async function handler(
try {
await verifyCodeUnAuthenticated(bookerEmail, verificationCode);
} catch (error) {
} catch {
throw new HttpError({
statusCode: 400,
message: "invalid_verification_code",
@@ -719,12 +725,14 @@ async function handler(
location,
});
const checkBookingAndDurationLimitsService = getCheckBookingAndDurationLimitsService();
await checkBookingAndDurationLimitsService.checkBookingAndDurationLimits({
eventType,
reqBodyStart: reqBody.start,
reqBodyRescheduleUid: reqBody.rescheduleUid,
});
if (!skipEventLimitsCheck) {
const checkBookingAndDurationLimitsService = getCheckBookingAndDurationLimitsService();
await checkBookingAndDurationLimitsService.checkBookingAndDurationLimits({
eventType,
reqBodyStart: reqBody.start,
reqBodyRescheduleUid: reqBody.rescheduleUid,
});
}
let luckyUserResponse;
let isFirstSeat = true;
@@ -798,48 +806,55 @@ async function handler(
if (isTeamEvent) {
// each fixed user must be available
for (const key in fixedUsers) {
if (!skipAvailabilityCheck) {
await ensureAvailableUsers(
{ ...eventTypeWithUsers, users: [fixedUsers[key]] },
{
dateFrom: dayjs(start).tz(reqBody.timeZone).format(),
dateTo: dayjs(end).tz(reqBody.timeZone).format(),
timeZone: reqBody.timeZone,
originalRescheduledBooking: originalRescheduledBooking ?? null,
},
loggerWithEventDetails,
shouldServeCache
);
}
}
} else {
if (!skipAvailabilityCheck) {
await ensureAvailableUsers(
{ ...eventTypeWithUsers, users: [fixedUsers[key]] },
eventTypeWithUsers,
{
dateFrom: dayjs(start).tz(reqBody.timeZone).format(),
dateTo: dayjs(end).tz(reqBody.timeZone).format(),
timeZone: reqBody.timeZone,
originalRescheduledBooking: originalRescheduledBooking ?? null,
originalRescheduledBooking,
},
loggerWithEventDetails,
shouldServeCache
);
}
} else {
eventTypeWithUsers.users[0].credentials;
await ensureAvailableUsers(
eventTypeWithUsers,
{
dateFrom: dayjs(start).tz(reqBody.timeZone).format(),
dateTo: dayjs(end).tz(reqBody.timeZone).format(),
timeZone: reqBody.timeZone,
originalRescheduledBooking,
},
loggerWithEventDetails,
shouldServeCache
);
}
}
}
if (!input.bookingData.allRecurringDates || input.bookingData.isFirstRecurringSlot) {
try {
availableUsers = await ensureAvailableUsers(
{ ...eventTypeWithUsers, users: [...qualifiedRRUsers, ...fixedUsers] as IsFixedAwareUser[] },
{
dateFrom: dayjs(reqBody.start).tz(reqBody.timeZone).format(),
dateTo: dayjs(reqBody.end).tz(reqBody.timeZone).format(),
timeZone: reqBody.timeZone,
originalRescheduledBooking,
},
loggerWithEventDetails,
shouldServeCache
);
if (!skipAvailabilityCheck) {
availableUsers = await ensureAvailableUsers(
{ ...eventTypeWithUsers, users: [...qualifiedRRUsers, ...fixedUsers] as IsFixedAwareUser[] },
{
dateFrom: dayjs(reqBody.start).tz(reqBody.timeZone).format(),
dateTo: dayjs(reqBody.end).tz(reqBody.timeZone).format(),
timeZone: reqBody.timeZone,
originalRescheduledBooking,
},
loggerWithEventDetails,
shouldServeCache
);
} else {
availableUsers = [...qualifiedRRUsers, ...fixedUsers] as IsFixedAwareUser[];
}
} catch {
if (additionalFallbackRRUsers.length) {
loggerWithEventDetails.debug(
@@ -851,20 +866,24 @@ async function handler(
);
// can happen when contact owner not available for 2 weeks or fairness would block at least 2 weeks
// use fallback instead
availableUsers = await ensureAvailableUsers(
{
...eventTypeWithUsers,
users: [...additionalFallbackRRUsers, ...fixedUsers] as IsFixedAwareUser[],
},
{
dateFrom: dayjs(reqBody.start).tz(reqBody.timeZone).format(),
dateTo: dayjs(reqBody.end).tz(reqBody.timeZone).format(),
timeZone: reqBody.timeZone,
originalRescheduledBooking,
},
loggerWithEventDetails,
shouldServeCache
);
if (!skipAvailabilityCheck) {
availableUsers = await ensureAvailableUsers(
{
...eventTypeWithUsers,
users: [...additionalFallbackRRUsers, ...fixedUsers] as IsFixedAwareUser[],
},
{
dateFrom: dayjs(reqBody.start).tz(reqBody.timeZone).format(),
dateTo: dayjs(reqBody.end).tz(reqBody.timeZone).format(),
timeZone: reqBody.timeZone,
originalRescheduledBooking,
},
loggerWithEventDetails,
shouldServeCache
);
} else {
availableUsers = [...additionalFallbackRRUsers, ...fixedUsers] as IsFixedAwareUser[];
}
} else {
loggerWithEventDetails.debug(
"Qualified users not available, no fallback users",
@@ -880,7 +899,11 @@ async function handler(
const nonFixedUsers: IsFixedAwareUser[] = [];
availableUsers.forEach((user) => {
user.isFixed ? fixedUserPool.push(user) : nonFixedUsers.push(user);
if (user.isFixed) {
fixedUserPool.push(user);
} else {
nonFixedUsers.push(user);
}
});
// Group non-fixed users by their group IDs
@@ -958,17 +981,19 @@ async function handler(
const start = input.bookingData.allRecurringDates[i].start;
const end = input.bookingData.allRecurringDates[i].end;
await ensureAvailableUsers(
{ ...eventTypeWithUsers, users: [newLuckyUser] },
{
dateFrom: dayjs(start).tz(reqBody.timeZone).format(),
dateTo: dayjs(end).tz(reqBody.timeZone).format(),
timeZone: reqBody.timeZone,
originalRescheduledBooking,
},
loggerWithEventDetails,
shouldServeCache
);
if (!skipAvailabilityCheck) {
await ensureAvailableUsers(
{ ...eventTypeWithUsers, users: [newLuckyUser] },
{
dateFrom: dayjs(start).tz(reqBody.timeZone).format(),
dateTo: dayjs(end).tz(reqBody.timeZone).format(),
timeZone: reqBody.timeZone,
originalRescheduledBooking,
},
loggerWithEventDetails,
shouldServeCache
);
}
}
// if no error, then lucky user is available for the next slots
luckyUsers.push(newLuckyUser);
@@ -1000,7 +1025,7 @@ async function handler(
// Filter out host groups that have no hosts in them
const nonEmptyHostGroups = Object.fromEntries(
Object.entries(hostGroups).filter(([_groupId, hosts]) => hosts.length > 0)
Object.entries(hostGroups).filter(([, hosts]) => hosts.length > 0)
);
// If there are RR hosts, we need to find a lucky user
if (
@@ -1658,9 +1683,10 @@ async function handler(
// After polling videoBusyTimes, credentials might have been changed due to refreshment, so query them again.
const credentials = await refreshCredentials(allCredentials);
const apps = eventTypeAppMetadataOptionalSchema.parse(eventType?.metadata?.apps);
const eventManager = !isDryRun
? new EventManager({ ...organizerUser, credentials }, apps)
: buildDryRunEventManager();
const eventManager =
!isDryRun && !skipCalendarSyncTaskCreation
? new EventManager({ ...organizerUser, credentials }, apps)
: buildDryRunEventManager();
let videoCallUrl;
@@ -1721,20 +1747,24 @@ async function handler(
: evt.responses,
};
await originalHostEventManager.deleteEventsAndMeetings({
event: deletionEvent,
bookingReferences: originalRescheduledBooking.references,
});
if (!skipCalendarSyncTaskCreation) {
await originalHostEventManager.deleteEventsAndMeetings({
event: deletionEvent,
bookingReferences: originalRescheduledBooking.references,
});
}
}
const updateManager = await eventManager.reschedule(
evt,
originalRescheduledBooking.uid,
undefined,
changedOrganizer,
previousHostDestinationCalendar,
isBookingRequestedReschedule,
skipDeleteEventsAndMeetings
);
const updateManager = !skipCalendarSyncTaskCreation
? await eventManager.reschedule(
evt,
originalRescheduledBooking.uid,
undefined,
changedOrganizer,
previousHostDestinationCalendar,
isBookingRequestedReschedule,
skipDeleteEventsAndMeetings
)
: placeholderCreatedEvent;
// This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back
// to the default description when we are sending the emails.
evt.description = eventType.description;
@@ -1870,7 +1900,10 @@ async function handler(
// Create a booking
} else if (isConfirmedByDefault) {
// Use EventManager to conditionally use all needed integrations.
const createManager = areCalendarEventsEnabled ? await eventManager.create(evt) : placeholderCreatedEvent;
const createManager =
areCalendarEventsEnabled && !skipCalendarSyncTaskCreation
? await eventManager.create(evt)
: placeholderCreatedEvent;
if (evt.location) {
booking.location = evt.location;
}
@@ -0,0 +1,396 @@
import prismaMock from "../../../../../../tests/libs/__mocks__/prisma";
import {
createBookingScenario,
getDate,
getGoogleCalendarCredential,
TestData,
getOrganizer,
getBooker,
getScenarioData,
mockCalendarToHaveNoBusySlots,
mockCalendar,
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import { expectBookingToBeInDatabase } from "@calcom/web/test/utils/bookingScenario/expects";
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
import { describe, expect, test } from "vitest";
import { BookingStatus } from "@calcom/prisma/enums";
import { getNewBookingHandler } from "./getNewBookingHandler";
setupAndTeardown();
describe("handleNewBooking - Booking Flags", () => {
describe("skipAvailabilityCheck flag", () => {
test("should skip availability checks when skipAvailabilityCheck is true", async () => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CREATED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 1,
appId: null,
},
],
eventTypes: [
{
id: 1,
slotInterval: 45,
length: 45,
users: [
{
id: 101,
},
],
},
],
organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
})
);
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
uid: "MOCK_ID",
},
});
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
start: `${plus1DateString}T05:00:00.000Z`,
end: `${plus1DateString}T05:45:00.000Z`,
},
});
const createdBooking = await handleNewBooking({
bookingData: mockBookingData,
skipAvailabilityCheck: true,
});
expect(createdBooking.responses).toEqual(
expect.objectContaining({
email: booker.email,
name: booker.name,
})
);
expect(createdBooking.status).toBe(BookingStatus.ACCEPTED);
await expectBookingToBeInDatabase({
description: "",
location: "integrations:daily",
responses: expect.objectContaining({
email: booker.email,
name: booker.name,
}),
uid: createdBooking.uid,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
});
});
});
describe("skipEventLimitsCheck flag", () => {
test("should skip event limits checks when skipEventLimitsCheck is true", async () => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 45,
length: 45,
bookingLimits: {
PER_DAY: 1,
},
users: [
{
id: 101,
},
],
},
],
organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
})
);
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
uid: "MOCK_ID",
},
});
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
start: `${plus1DateString}T05:00:00.000Z`,
end: `${plus1DateString}T05:45:00.000Z`,
},
});
const createdBooking = await handleNewBooking({
bookingData: mockBookingData,
skipEventLimitsCheck: true,
});
expect(createdBooking.responses).toEqual(
expect.objectContaining({
email: booker.email,
name: booker.name,
})
);
expect(createdBooking.status).toBe(BookingStatus.ACCEPTED);
await expectBookingToBeInDatabase({
description: "",
location: "integrations:daily",
responses: expect.objectContaining({
email: booker.email,
name: booker.name,
}),
uid: createdBooking.uid,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
});
});
});
describe("skipCalendarSyncTaskCreation flag", () => {
test("should skip calendar sync when skipCalendarSyncTaskCreation is true", async () => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 45,
length: 45,
users: [
{
id: 101,
},
],
},
],
organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
})
);
mockCalendar("googlecalendar", {
create: {
uid: "MOCK_ID",
},
});
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
start: `${plus1DateString}T05:00:00.000Z`,
end: `${plus1DateString}T05:45:00.000Z`,
},
});
const createdBooking = await handleNewBooking({
bookingData: mockBookingData,
skipCalendarSyncTaskCreation: true,
});
expect(createdBooking.responses).toEqual(
expect.objectContaining({
email: booker.email,
name: booker.name,
})
);
expect(createdBooking.status).toBe(BookingStatus.ACCEPTED);
await expectBookingToBeInDatabase({
description: "",
location: "integrations:daily",
responses: expect.objectContaining({
email: booker.email,
name: booker.name,
}),
uid: createdBooking.uid,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
});
const actualBooking = await prismaMock.booking.findFirst({
where: { uid: createdBooking.uid },
include: { references: true },
});
expect(actualBooking?.references).toEqual([]);
});
});
describe("combined flags", () => {
test("should work with multiple flags enabled", async () => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
await createBookingScenario(
getScenarioData({
eventTypes: [
{
id: 1,
slotInterval: 45,
length: 45,
bookingLimits: {
PER_DAY: 1,
},
users: [
{
id: 101,
},
],
},
],
organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
})
);
mockCalendar("googlecalendar", {
create: {
uid: "MOCK_ID",
},
});
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "integrations:daily" },
},
start: `${plus1DateString}T05:00:00.000Z`,
end: `${plus1DateString}T05:45:00.000Z`,
},
});
const createdBooking = await handleNewBooking({
bookingData: mockBookingData,
skipAvailabilityCheck: true,
skipEventLimitsCheck: true,
skipCalendarSyncTaskCreation: true,
});
expect(createdBooking.responses).toEqual(
expect.objectContaining({
email: booker.email,
name: booker.name,
})
);
expect(createdBooking.status).toBe(BookingStatus.ACCEPTED);
await expectBookingToBeInDatabase({
description: "",
location: "integrations:daily",
responses: expect.objectContaining({
email: booker.email,
name: booker.name,
}),
uid: createdBooking.uid,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
});
const actualBooking = await prismaMock.booking.findFirst({
where: { uid: createdBooking.uid },
include: { references: true },
});
expect(actualBooking?.references).toEqual([]);
});
});
});
@@ -2,6 +2,7 @@ import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-util
import logger from "@calcom/lib/logger";
import { MembershipRepository } from "@calcom/lib/server/repository/membership";
import type { Payment } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import { handleNoShowFee } from "./handleNoShowFee";
import { shouldChargeNoShowCancellationFee } from "./shouldChargeNoShowCancellationFee";
@@ -32,7 +33,10 @@ export const processNoShowFeeOnCancellation = async ({
teamId: booking.eventType.teamId,
});
if (membership && (membership.role === "ADMIN" || membership.role === "OWNER")) {
if (
membership &&
(membership.role === MembershipRole.OWNER || membership.role === MembershipRole.ADMIN)
) {
log.info(
`Booking ${booking.uid} was cancelled by team admin/owner (${cancelledByUserId}), skipping no-show fee`
);
@@ -0,0 +1,115 @@
# Calendar Cache and Sync
The **Calendar Cache and Sync** feature provides efficient calendar synchronization with intelligent caching to reduce API calls and ensure real-time updates across your Cal.com instance.
## Feature Overview
This feature introduces two complementary capabilities:
- **Calendar Cache**: Stores calendar availability data locally to reduce external API calls and improve performance
- **Calendar Sync**: Uses webhooks to automatically listen for calendar updates and apply changes in real-time
**Key Benefits:**
- **Efficiency**: Reduces API calls with optimized caching strategies
- **Reliability**: Guarantees updates through webhook event delivery
- **Real-Time Sync**: Ensures calendars are always up-to-date with minimal latency
- **Scalability**: Supports multiple calendars and handles high-volume updates seamlessly
**Motivation:**
By subscribing to calendars via webhooks and implementing intelligent caching, you gain a smarter, faster, and more resource-friendly way to keep your data in sync. This eliminates the need for constant polling and reduces the load on external calendar APIs while ensuring data consistency.
## Environment Variables
- **GOOGLE_WEBHOOK_URL**: Optional, only used for local tests. Default points to application url.
- **GOOGLE_WEBHOOK_TOKEN**: Required, token to validate Google Webhook incoming.
- **MICROSOFT_WEBHOOK_URL**: Optional, only used for local tests. Default points to application url.
- **MICROSOFT_WEBHOOK_TOKEN**: Required, token to validate Microsoft Webhook incoming.
## Feature Flags
This feature is controlled by three feature flags that can be enabled independently:
### 1. calendar-subscription-cache
Enables calendar cache recording and usage through calendars. This flag should be managed individually by teams.
```sql
INSERT INTO "Feature" ("slug", "enabled", "description", "type", "stale", "lastUsedAt", "createdAt", "updatedAt", "updatedBy")
VALUES ('calendar-subscription-cache', false, 'Allow calendar cache to be recorded and used through calendars.', 'OPERATIONAL', false, NULL, NOW(), NOW(), NULL)
ON CONFLICT (slug) DO NOTHING;
```
### 2. calendar-subscription-sync
Enables calendar sync globally for all users regardless of team or organization.
```sql
INSERT INTO "Feature" ("slug", "enabled", "description", "type", "stale", "lastUsedAt", "createdAt", "updatedAt", "updatedBy")
VALUES ('calendar-subscription-sync', false, 'Enable calendar sync for all calendars globally.', 'OPERATIONAL', false, NULL, NOW(), NOW(), NULL)
ON CONFLICT (slug) DO NOTHING;
```
## Enabling Features for Specific Users
To enable calendar cache features for specific users, add entries to the `UserFeatures` table:
```sql
-- Enable calendar-subscription-cache for user ID 123
INSERT INTO "UserFeatures" ("userId", "featureId", "assignedAt", "assignedBy", "updatedAt")
VALUES (123, 'calendar-subscription-cache', NOW(), 'admin', NOW())
ON CONFLICT ("userId", "featureId") DO NOTHING;
-- Enable calendar-subscription-sync for user ID 123
INSERT INTO "UserFeatures" ("userId", "featureId", "assignedAt", "assignedBy", "updatedAt")
VALUES (123, 'calendar-subscription-sync', NOW(), 'admin', NOW())
ON CONFLICT ("userId", "featureId") DO NOTHING;
```
## Enabling Features for Specific Teams
To enable calendar cache features for specific teams, add entries to the `TeamFeatures` table:
```sql
-- Enable calendar-subscription-cache for team ID 456
INSERT INTO "TeamFeatures" ("teamId", "featureId", "assignedAt", "assignedBy", "updatedAt")
VALUES (456, 'calendar-subscription-cache', NOW(), 'admin', NOW())
ON CONFLICT ("teamId", "featureId") DO NOTHING;
-- Enable calendar-subscription-sync for team ID 456
INSERT INTO "TeamFeatures" ("teamId", "featureId", "assignedAt", "assignedBy", "updatedAt")
VALUES (456, 'calendar-subscription-sync', NOW(), 'admin', NOW())
ON CONFLICT ("teamId", "featureId") DO NOTHING;
```
## Architecture
The calendar cache and sync system consists of several key components:
### Database Schema
- **CalendarCacheEvent Table**: Stores cached calendar events with status tracking
- **SelectedCalendar Extensions**: Additional fields for sync state and webhook management including:
- `channelId`: Webhook channel identifier
- `channelResourceId`: Resource ID for webhook subscriptions
- `channelResourceUri`: URI for webhook notifications
- `channelKind`: Type of webhook channel
- `channelExpiration`: Webhook subscription expiration time
- `syncToken`: Token for incremental sync
- `syncedAt`: Last successful sync timestamp
- `syncErrorAt`: Last sync error timestamp
- `syncErrorCount`: Number of consecutive sync errors
- `syncSubscribedAt`: Webhook subscription timestamp
### Core Services
- **CalendarCacheEventRepository**: Manages cached event storage and retrieval
- **CalendarSubscriptionService**: Orchestrates webhook subscriptions and event processing
- **Provider-specific Adapters**: Handle calendar-specific sync logic (Google, Office365)
### Background Processes
- **Cron Jobs**: Automated processes for cache cleanup and calendar watching
- **Webhook Handlers**: Real-time event processing for calendar updates
### Integration Points
- **Calendar Providers**: Google Calendar, Office365, and other supported integrations
- **Webhook Endpoints**: Receive real-time notifications from calendar providers
- **Cache Layer**: Optimized storage for frequently accessed calendar data
For detailed technical implementation, see:
- Database migrations in `packages/prisma/migrations/`
@@ -0,0 +1,44 @@
import { GoogleCalendarSubscriptionAdapter } from "@calcom/features/calendar-subscription/adapters/GoogleCalendarSubscription.adapter";
import { Office365CalendarSubscriptionAdapter } from "@calcom/features/calendar-subscription/adapters/Office365CalendarSubscription.adapter";
import type { ICalendarSubscriptionPort } from "@calcom/features/calendar-subscription/lib/CalendarSubscriptionPort.interface";
export type CalendarSubscriptionProvider = "google_calendar" | "office365_calendar";
export interface AdapterFactory {
get(provider: CalendarSubscriptionProvider): ICalendarSubscriptionPort;
getProviders(): CalendarSubscriptionProvider[];
}
/**
* Default adapter factory
*/
export class DefaultAdapterFactory implements AdapterFactory {
private singletons = {
google_calendar: new GoogleCalendarSubscriptionAdapter(),
office365_calendar: new Office365CalendarSubscriptionAdapter(),
} as const;
/**
* Returns the adapter for the given provider
*
* @param provider
* @returns
*/
get(provider: CalendarSubscriptionProvider): ICalendarSubscriptionPort {
const adapter = this.singletons[provider];
if (!adapter) {
throw new Error(`No adapter found for provider ${provider}`);
}
return adapter;
}
/**
* Returns all available providers
*
* @returns
*/
getProviders(): CalendarSubscriptionProvider[] {
const providers: CalendarSubscriptionProvider[] = ["google_calendar"];
return providers;
}
}
@@ -0,0 +1,199 @@
import type { calendar_v3 } from "@googleapis/calendar";
import { v4 as uuid } from "uuid";
import { CalendarAuth } from "@calcom/app-store/googlecalendar/lib/CalendarAuth";
import dayjs from "@calcom/dayjs";
import logger from "@calcom/lib/logger";
import type { SelectedCalendar } from "@calcom/prisma/client";
import type {
ICalendarSubscriptionPort,
CalendarSubscriptionResult,
CalendarSubscriptionEvent,
CalendarSubscriptionEventItem,
CalendarCredential,
} from "../lib/CalendarSubscriptionPort.interface";
const log = logger.getSubLogger({ prefix: ["GoogleCalendarSubscriptionAdapter"] });
/**
* Google Calendar Subscription Adapter
*
* This adapter uses the Google Calendar API to create and manage calendar subscriptions
* @see https://developers.google.com/google-apps/calendar/quickstart/nodejs
*/
export class GoogleCalendarSubscriptionAdapter implements ICalendarSubscriptionPort {
private GOOGLE_WEBHOOK_TOKEN = process.env.GOOGLE_WEBHOOK_TOKEN;
private GOOGLE_WEBHOOK_URL = `${
process.env.GOOGLE_WEBHOOK_URL || process.env.NEXT_PUBLIC_WEBAPP_URL
}/api/webhooks/calendar-subscription/google_calendar`;
async validate(request: Request): Promise<boolean> {
const token = request?.headers?.get("X-Goog-Channel-Token");
if (!this.GOOGLE_WEBHOOK_TOKEN) {
log.warn("GOOGLE_WEBHOOK_TOKEN not configured");
return false;
}
if (token !== this.GOOGLE_WEBHOOK_TOKEN) {
log.warn("Invalid webhook token");
return false;
}
return true;
}
async extractChannelId(request: Request): Promise<string | null> {
const channelId = request?.headers?.get("X-Goog-Channel-ID");
if (!channelId) {
log.warn("Missing channel ID in webhook");
return null;
}
return channelId;
}
async subscribe(
selectedCalendar: SelectedCalendar,
credential: CalendarCredential
): Promise<CalendarSubscriptionResult> {
log.debug("Attempt to subscribe to Google Calendar", { externalId: selectedCalendar.externalId });
const MONTH_IN_SECONDS = 60 * 60 * 24 * 30;
const client = await this.getClient(credential);
const result = await client.events.watch({
calendarId: selectedCalendar.externalId,
requestBody: {
id: uuid(),
type: "web_hook",
address: this.GOOGLE_WEBHOOK_URL,
token: this.GOOGLE_WEBHOOK_TOKEN,
params: {
ttl: MONTH_IN_SECONDS.toFixed(0),
},
},
});
const e = result.data?.expiration;
const expiration = e ? new Date(/^\d+$/.test(e) ? +e : e) : null;
return {
provider: "google_calendar",
id: result.data.id,
resourceId: result.data.resourceId,
resourceUri: result.data.resourceUri,
expiration,
};
}
async unsubscribe(selectedCalendar: SelectedCalendar, credential: CalendarCredential): Promise<void> {
log.debug("Attempt to unsubscribe from Google Calendar", { externalId: selectedCalendar.externalId });
const client = await this.getClient(credential);
await client.channels
.stop({
requestBody: {
id: selectedCalendar.channelId as string,
resourceId: selectedCalendar.channelResourceId as string,
},
})
.catch((err) => {
log.error("Error unsubscribing from Google Calendar", err);
throw err;
});
}
async fetchEvents(
selectedCalendar: SelectedCalendar,
credential: CalendarCredential
): Promise<CalendarSubscriptionEvent> {
log.info("Attempt to fetch events from Google Calendar", { externalId: selectedCalendar.externalId });
const client = await this.getClient(credential);
let syncToken = selectedCalendar.syncToken || undefined;
let pageToken;
const params: calendar_v3.Params$Resource$Events$List = {
calendarId: selectedCalendar.externalId,
pageToken,
singleEvents: true,
};
if (!syncToken) {
const now = dayjs();
// first sync or unsync (3 months)
const threeMonths = now.add(3, "month");
const timeMinISO = now.toISOString();
const timeMaxISO = threeMonths.toISOString();
params.timeMin = timeMinISO;
params.timeMax = timeMaxISO;
} else {
// incremental sync
params.syncToken = syncToken;
}
const events: calendar_v3.Schema$Event[] = [];
do {
const { data }: { data: calendar_v3.Schema$Events } = await client.events.list(params);
syncToken = data.nextSyncToken || syncToken;
pageToken = data.nextPageToken ?? null;
events.push(...(data.items || []));
} while (pageToken);
return {
provider: "google_calendar",
syncToken: syncToken || null,
items: this.parseEvents(events),
};
}
private parseEvents(events: calendar_v3.Schema$Event[]): CalendarSubscriptionEventItem[] {
return events
.filter((event) => typeof event.id === "string" && !!event.id)
.map((event) => {
// empty or opaque is busy
const busy = !event.transparency || event.transparency === "opaque";
const start = event.start?.dateTime
? new Date(event.start.dateTime)
: event.start?.date
? new Date(event.start.date)
: new Date();
const end = event.end?.dateTime
? new Date(event.end.dateTime)
: event.end?.date
? new Date(event.end.date)
: new Date();
return {
id: event.id as string,
iCalUID: event.iCalUID ?? null,
start,
end,
busy,
summary: event.summary ?? null,
description: event.description ?? null,
location: event.location ?? null,
kind: event.kind ?? null,
etag: event.etag ?? null,
status: event.status ?? null,
isAllDay: typeof event.start?.date === "string" && !event.start?.dateTime ? true : false,
timeZone: event.start?.timeZone ?? null,
recurringEventId: event.recurringEventId ?? null,
originalStartDate: event.originalStartTime?.dateTime
? new Date(event.originalStartTime.dateTime)
: event.originalStartTime?.date
? new Date(event.originalStartTime.date)
: null,
createdAt: event.created ? new Date(event.created) : null,
updatedAt: event.updated ? new Date(event.updated) : null,
};
});
}
private async getClient(credential: CalendarCredential) {
const auth = new CalendarAuth(credential);
return await auth.getClient();
}
}
@@ -0,0 +1,262 @@
import logger from "@calcom/lib/logger";
import type { SelectedCalendar } from "@calcom/prisma/client";
import type {
CalendarSubscriptionEvent,
ICalendarSubscriptionPort,
CalendarSubscriptionResult,
CalendarCredential,
CalendarSubscriptionEventItem,
} from "../lib/CalendarSubscriptionPort.interface";
const log = logger.getSubLogger({ prefix: ["MicrosoftCalendarSubscriptionAdapter"] });
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
type GraphClient = { accessToken: string };
interface MicrosoftGraphEvent {
id: string;
iCalUId?: string;
subject?: string;
bodyPreview?: string;
location?: { displayName?: string };
start?: { dateTime: string; timeZone: string };
end?: { dateTime: string; timeZone: string };
showAs?: "free" | "tentative" | "busy" | "oof" | "workingElsewhere" | "unknown";
isAllDay?: boolean;
isCancelled?: boolean;
type?: string;
}
interface MicrosoftGraphEventsResponse {
"@odata.nextLink"?: string;
"@odata.deltaLink"?: string;
value: MicrosoftGraphEvent[];
}
interface MicrosoftGraphSubscriptionReq {
resource: string;
changeType: string;
notificationUrl: string;
expirationDateTime: string;
clientState?: string;
}
interface MicrosoftGraphSubscriptionRes {
id: string;
resource: string;
expirationDateTime: string;
}
type AdapterConfig = {
baseUrl?: string;
webhookToken?: string | null;
webhookUrl?: string | null;
subscriptionTtlMs?: number;
};
/**
* Office365 Calendar Subscription Adapter
*
* This adapter uses the Microsoft Graph API to create and manage calendar subscriptions
* @see https://docs.microsoft.com/en-us/graph/api/resources/subscription
*/
export class Office365CalendarSubscriptionAdapter implements ICalendarSubscriptionPort {
private readonly baseUrl: string;
private readonly webhookToken?: string | null;
private readonly webhookUrl?: string | null;
private readonly subscriptionTtlMs: number;
constructor(cfg: AdapterConfig = {}) {
this.baseUrl = cfg.baseUrl ?? "https://graph.microsoft.com/v1.0";
this.webhookToken = cfg.webhookToken ?? process.env.MICROSOFT_WEBHOOK_TOKEN ?? null;
this.webhookUrl = cfg.webhookUrl ?? process.env.MICROSOFT_WEBHOOK_URL ?? null;
this.subscriptionTtlMs = cfg.subscriptionTtlMs ?? 3 * 24 * 60 * 60 * 1000;
}
async validate(request: Request): Promise<boolean> {
// validate handshake
let validationToken: string | null = null;
if (request?.url) {
try {
const urlObj = new URL(request.url);
validationToken = urlObj.searchParams.get("validationToken");
} catch (e) {
log.warn("Invalid request URL", { url: request.url });
}
}
if (validationToken) return true;
// validate notifications
const clientState =
request?.headers?.get("clientState") ??
(typeof request?.body === "object" && request.body !== null && "clientState" in request.body
? (request.body as { clientState?: string }).clientState
: undefined);
if (!this.webhookToken) {
log.warn("MICROSOFT_WEBHOOK_TOKEN missing");
return false;
}
if (clientState !== this.webhookToken) {
log.warn("Invalid clientState");
return false;
}
return true;
}
async extractChannelId(request: Request): Promise<string | null> {
let id: string | null = null;
if (request?.body && typeof request.body === "object" && "subscriptionId" in request.body) {
id = (request.body as { subscriptionId?: string }).subscriptionId ?? null;
} else if (request?.headers?.get("subscriptionId")) {
id = request.headers.get("subscriptionId");
}
if (!id) {
log.warn("subscriptionId missing in webhook");
}
return id;
}
async subscribe(
selectedCalendar: SelectedCalendar,
credential: CalendarCredential
): Promise<CalendarSubscriptionResult> {
if (!this.webhookUrl || !this.webhookToken) {
throw new Error("Webhook config missing (MICROSOFT_WEBHOOK_URL/TOKEN)");
}
const expirationDateTime = new Date(Date.now() + this.subscriptionTtlMs).toISOString();
const body: MicrosoftGraphSubscriptionReq = {
resource: `me/calendars/${selectedCalendar.externalId}/events`,
changeType: "created,updated,deleted",
notificationUrl: this.webhookUrl,
expirationDateTime,
clientState: this.webhookToken,
};
const client = await this.getGraphClient(credential);
const res = await this.request<MicrosoftGraphSubscriptionRes>(client, "POST", "/subscriptions", body);
return {
provider: "office365_calendar",
id: res.id,
resourceId: res.resource,
resourceUri: `${this.baseUrl}/${res.resource}`,
expiration: new Date(res.expirationDateTime),
};
}
async unsubscribe(selectedCalendar: SelectedCalendar, credential: CalendarCredential): Promise<void> {
const subId = selectedCalendar.channelResourceId;
if (!subId) return;
const client = await this.getGraphClient(credential);
await this.request(client, "DELETE", `/subscriptions/${subId}`);
}
async fetchEvents(
selectedCalendar: SelectedCalendar,
credential: CalendarCredential
): Promise<CalendarSubscriptionEvent> {
const client = await this.getGraphClient(credential);
let deltaLink = selectedCalendar.syncToken ?? null;
const items: MicrosoftGraphEvent[] = [];
if (deltaLink) {
const path = this.stripBase(deltaLink);
const r = await this.request<MicrosoftGraphEventsResponse>(client, "GET", path);
items.push(...r.value);
deltaLink = r["@odata.deltaLink"] ?? deltaLink;
} else {
let next: string | null = `/me/calendars/${selectedCalendar.externalId}/events/delta`;
while (next) {
const r: MicrosoftGraphEventsResponse = await this.request<MicrosoftGraphEventsResponse>(
client,
"GET",
next
);
items.push(...r.value);
deltaLink = r["@odata.deltaLink"] ?? deltaLink;
next = r["@odata.nextLink"] ? this.stripBase(r["@odata.nextLink"]) : null;
}
}
return {
provider: "office365_calendar",
syncToken: deltaLink,
items: this.parseEvents(items),
};
}
private parseEvents(events: MicrosoftGraphEvent[]): CalendarSubscriptionEventItem[] {
return events
.map((e) => {
const busy = e.showAs === "busy" || e.showAs === "tentative" || e.showAs === "oof";
const start = e.start?.dateTime ? new Date(e.start.dateTime) : new Date();
const end = e.end?.dateTime ? new Date(e.end.dateTime) : new Date();
return {
id: e.id,
iCalUID: e.iCalUId ?? null,
start,
end,
busy,
etag: null,
summary: e.subject ?? null,
description: e.bodyPreview ?? null,
location: e.location?.displayName ?? null,
kind: e.type ?? "microsoftgraph#event",
status: e.isCancelled ? "cancelled" : "confirmed",
isAllDay: e.isAllDay ?? false,
timeZone: e.start?.timeZone ?? null,
recurringEventId: null,
originalStartDate: null,
createdAt: null,
updatedAt: null,
};
})
.filter(({ id }) => !!id);
}
private async getGraphClient(credential: CalendarCredential): Promise<GraphClient> {
const accessToken = credential.delegatedTo?.serviceAccountKey?.private_key ?? (credential.key as string);
if (!accessToken) throw new Error("Missing Microsoft access token");
return { accessToken };
}
private stripBase(urlOrPath: string): string {
return urlOrPath.startsWith("http") ? urlOrPath.replace(this.baseUrl, "") : urlOrPath;
}
private async request<T = unknown>(
client: GraphClient,
method: HttpMethod,
endpoint: string,
data?: unknown
): Promise<T> {
const url = endpoint.startsWith("http") ? endpoint : `${this.baseUrl}${endpoint}`;
const headers: Record<string, string> = {
Authorization: `Bearer ${client.accessToken}`,
"Content-Type": "application/json",
};
const init: RequestInit = { method, headers };
if (data && (method === "POST" || method === "PUT" || method === "PATCH")) {
init.body = JSON.stringify(data);
}
const res = await fetch(url, init);
if (!res.ok) {
const text = await res.text().catch(() => "");
log.error("Graph API error", { method, endpoint: this.stripBase(url), status: res.status, text });
throw new Error(`Graph ${res.status} ${res.statusText}`);
}
if (method === "DELETE" || res.status === 204) return {} as T;
return (await res.json()) as T;
}
}
@@ -0,0 +1,17 @@
import { vi } from "vitest";
export const CalendarAuth = vi.fn().mockImplementation(() => ({
getClient: vi.fn().mockResolvedValue({
events: {
watch: vi.fn(),
list: vi.fn(),
},
channels: {
stop: vi.fn(),
},
}),
}));
vi.doMock("@calcom/app-store/googlecalendar/lib/CalendarAuth", () => ({
CalendarAuth,
}));
@@ -0,0 +1,54 @@
import { describe, test, expect, beforeEach } from "vitest";
import { DefaultAdapterFactory } from "../AdaptersFactory";
import { GoogleCalendarSubscriptionAdapter } from "../GoogleCalendarSubscription.adapter";
import { Office365CalendarSubscriptionAdapter } from "../Office365CalendarSubscription.adapter";
describe("DefaultAdapterFactory", () => {
let factory: DefaultAdapterFactory;
beforeEach(() => {
factory = new DefaultAdapterFactory();
});
describe("get", () => {
test("should return GoogleCalendarSubscriptionAdapter for google_calendar", () => {
const adapter = factory.get("google_calendar");
expect(adapter).toBeInstanceOf(GoogleCalendarSubscriptionAdapter);
});
test("should return Office365CalendarSubscriptionAdapter for office365_calendar", () => {
const adapter = factory.get("office365_calendar");
expect(adapter).toBeInstanceOf(Office365CalendarSubscriptionAdapter);
});
test("should return the same instance for multiple calls (singleton)", () => {
const adapter1 = factory.get("google_calendar");
const adapter2 = factory.get("google_calendar");
expect(adapter1).toBe(adapter2);
});
test("should throw error for unsupported provider", () => {
expect(() => {
factory.get("unsupported_calendar" as never);
}).toThrow("No adapter found for provider unsupported_calendar");
});
});
describe("getProviders", () => {
test("should return all available providers", () => {
const providers = factory.getProviders();
expect(providers).toEqual(["google_calendar"]);
});
test("should return array with correct length", () => {
const providers = factory.getProviders();
expect(providers).toHaveLength(1);
});
});
});
@@ -0,0 +1,497 @@
import "../__mocks__/CalendarAuth";
import { beforeEach, describe, expect, test, vi } from "vitest";
import type { SelectedCalendar } from "@calcom/prisma/client";
import type { CredentialForCalendarServiceWithEmail } from "@calcom/types/Credential";
import { GoogleCalendarSubscriptionAdapter } from "../GoogleCalendarSubscription.adapter";
vi.mock("uuid", () => ({
v4: vi.fn().mockReturnValue("test-uuid"),
}));
const mockSelectedCalendar: SelectedCalendar = {
id: "test-calendar-id",
userId: 1,
credentialId: 1,
integration: "google_calendar",
externalId: "test@example.com",
eventTypeId: null,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
googleChannelId: null,
googleChannelKind: null,
googleChannelResourceId: null,
googleChannelResourceUri: null,
googleChannelExpiration: null,
error: null,
lastErrorAt: null,
watchAttempts: 0,
maxAttempts: 3,
unwatchAttempts: 0,
createdAt: new Date(),
updatedAt: new Date(),
channelId: "test-channel-id",
channelKind: "web_hook",
channelResourceId: "test-resource-id",
channelResourceUri: "test-resource-uri",
channelExpiration: new Date(Date.now() + 86400000),
syncSubscribedAt: new Date(),
syncToken: "test-sync-token",
syncedAt: new Date(),
syncErrorAt: null,
syncErrorCount: 0,
};
const mockCredential = {
id: 1,
key: { access_token: "test-token" },
user: { email: "test@example.com" },
delegatedTo: null,
type: null,
teamId: null,
} as unknown as CredentialForCalendarServiceWithEmail;
describe("GoogleCalendarSubscriptionAdapter", () => {
let adapter: GoogleCalendarSubscriptionAdapter;
let mockClient: {
events: {
watch: ReturnType<typeof vi.fn>;
list: ReturnType<typeof vi.fn>;
};
channels: {
stop: ReturnType<typeof vi.fn>;
};
};
beforeEach(async () => {
process.env.GOOGLE_WEBHOOK_TOKEN = "test-webhook-token";
process.env.GOOGLE_WEBHOOK_URL = "https://example.com";
mockClient = {
events: {
watch: vi.fn(),
list: vi.fn(),
},
channels: {
stop: vi.fn(),
},
};
const { CalendarAuth } = await import("../__mocks__/CalendarAuth");
vi.mocked(CalendarAuth).mockImplementation(() => ({
getClient: vi.fn().mockResolvedValue(mockClient),
}));
adapter = new GoogleCalendarSubscriptionAdapter();
vi.clearAllMocks();
});
describe("validate", () => {
test("should validate webhook with correct token", async () => {
const mockRequest = {
headers: {
get: vi.fn().mockReturnValue("test-webhook-token"),
},
} as unknown as Request;
const result = await adapter.validate(mockRequest);
expect(result).toBe(true);
expect(mockRequest.headers.get).toHaveBeenCalledWith("X-Goog-Channel-Token");
});
test("should reject webhook with incorrect token", async () => {
const mockRequest = {
headers: {
get: vi.fn().mockReturnValue("wrong-token"),
},
} as unknown as Request;
const result = await adapter.validate(mockRequest);
expect(result).toBe(false);
});
test("should reject webhook when token is not configured", async () => {
delete process.env.GOOGLE_WEBHOOK_TOKEN;
adapter = new GoogleCalendarSubscriptionAdapter();
const mockRequest = {
headers: {
get: vi.fn().mockReturnValue("test-webhook-token"),
},
} as unknown as Request;
const result = await adapter.validate(mockRequest);
expect(result).toBe(false);
});
test("should reject webhook with missing token header", async () => {
const mockRequest = {
headers: {
get: vi.fn().mockReturnValue(null),
},
} as unknown as Request;
const result = await adapter.validate(mockRequest);
expect(result).toBe(false);
});
});
describe("extractChannelId", () => {
test("should extract channel ID from webhook headers", async () => {
const mockRequest = {
headers: {
get: vi.fn().mockReturnValue("test-channel-id"),
},
} as unknown as Request;
const result = await adapter.extractChannelId(mockRequest);
expect(result).toBe("test-channel-id");
expect(mockRequest.headers.get).toHaveBeenCalledWith("X-Goog-Channel-ID");
});
test("should return null when channel ID is missing", async () => {
const mockRequest = {
headers: {
get: vi.fn().mockReturnValue(null),
},
} as unknown as Request;
const result = await adapter.extractChannelId(mockRequest);
expect(result).toBeNull();
});
});
describe("subscribe", () => {
test("should successfully subscribe to calendar", async () => {
const mockWatchResponse = {
data: {
id: "test-channel-id",
resourceId: "test-resource-id",
resourceUri: "test-resource-uri",
expiration: "1640995200000",
},
};
mockClient.events.watch.mockResolvedValue(mockWatchResponse);
const result = await adapter.subscribe(mockSelectedCalendar, mockCredential);
expect(mockClient.events.watch).toHaveBeenCalledWith({
calendarId: "test@example.com",
requestBody: {
id: "test-uuid",
type: "web_hook",
address: "https://example.com/api/webhooks/calendar-subscription/google_calendar",
token: "test-webhook-token",
params: {
ttl: "2592000",
},
},
});
expect(result).toEqual({
provider: "google_calendar",
id: "test-channel-id",
resourceId: "test-resource-id",
resourceUri: "test-resource-uri",
expiration: new Date(1640995200000),
});
});
test("should handle expiration as ISO string", async () => {
const mockWatchResponse = {
data: {
id: "test-channel-id",
resourceId: "test-resource-id",
resourceUri: "test-resource-uri",
expiration: "2023-12-01T10:00:00Z",
},
};
mockClient.events.watch.mockResolvedValue(mockWatchResponse);
const result = await adapter.subscribe(mockSelectedCalendar, mockCredential);
expect(result.expiration).toEqual(new Date("2023-12-01T10:00:00Z"));
});
test("should handle missing expiration", async () => {
const mockWatchResponse = {
data: {
id: "test-channel-id",
resourceId: "test-resource-id",
resourceUri: "test-resource-uri",
},
};
mockClient.events.watch.mockResolvedValue(mockWatchResponse);
const result = await adapter.subscribe(mockSelectedCalendar, mockCredential);
expect(result.expiration).toBeNull();
});
});
describe("unsubscribe", () => {
test("should successfully unsubscribe from calendar", async () => {
mockClient.channels.stop.mockResolvedValue({});
await adapter.unsubscribe(mockSelectedCalendar, mockCredential);
expect(mockClient.channels.stop).toHaveBeenCalledWith({
requestBody: {
id: "test-channel-id",
resourceId: "test-resource-id",
},
});
});
test("should handle unsubscribe errors", async () => {
const error = new Error("Unsubscribe failed");
mockClient.channels.stop.mockRejectedValue(error);
await expect(adapter.unsubscribe(mockSelectedCalendar, mockCredential)).rejects.toThrow(
"Unsubscribe failed"
);
});
});
describe("fetchEvents", () => {
test("should fetch events with sync token", async () => {
const mockEventsResponse = {
data: {
nextSyncToken: "new-sync-token",
items: [
{
id: "event-1",
iCalUID: "event-1@cal.com",
summary: "Test Event",
description: "Test Description",
location: "Test Location",
start: {
dateTime: "2023-12-01T10:00:00Z",
timeZone: "UTC",
},
end: {
dateTime: "2023-12-01T11:00:00Z",
},
status: "confirmed",
transparency: "opaque",
kind: "calendar#event",
etag: "test-etag",
created: "2023-12-01T09:00:00Z",
updated: "2023-12-01T09:30:00Z",
},
],
},
};
mockClient.events.list.mockResolvedValue(mockEventsResponse);
const result = await adapter.fetchEvents(mockSelectedCalendar, mockCredential);
expect(mockClient.events.list).toHaveBeenCalledWith({
calendarId: "test@example.com",
pageToken: undefined,
singleEvents: true,
syncToken: "test-sync-token",
});
expect(result).toEqual({
provider: "google_calendar",
syncToken: "new-sync-token",
items: [
{
id: "event-1",
iCalUID: "event-1@cal.com",
start: new Date("2023-12-01T10:00:00Z"),
end: new Date("2023-12-01T11:00:00Z"),
busy: true,
summary: "Test Event",
description: "Test Description",
location: "Test Location",
kind: "calendar#event",
etag: "test-etag",
status: "confirmed",
isAllDay: false,
timeZone: "UTC",
recurringEventId: null,
originalStartDate: null,
createdAt: new Date("2023-12-01T09:00:00Z"),
updatedAt: new Date("2023-12-01T09:30:00Z"),
},
],
});
});
test("should fetch events without sync token (initial sync)", async () => {
const calendarWithoutSyncToken = {
...mockSelectedCalendar,
syncToken: null,
};
const mockEventsResponse = {
data: {
nextSyncToken: "initial-sync-token",
items: [],
},
};
mockClient.events.list.mockResolvedValue(mockEventsResponse);
const result = await adapter.fetchEvents(calendarWithoutSyncToken, mockCredential);
expect(mockClient.events.list).toHaveBeenCalledWith({
calendarId: "test@example.com",
pageToken: undefined,
singleEvents: true,
timeMin: expect.any(String),
timeMax: expect.any(String),
});
expect(result.syncToken).toBe("initial-sync-token");
});
test("should handle all-day events", async () => {
const mockEventsResponse = {
data: {
nextSyncToken: "new-sync-token",
items: [
{
id: "event-1",
iCalUID: "event-1@cal.com",
summary: "All Day Event",
start: {
date: "2023-12-01",
},
end: {
date: "2023-12-02",
},
status: "confirmed",
transparency: "opaque",
},
],
},
};
mockClient.events.list.mockResolvedValue(mockEventsResponse);
const result = await adapter.fetchEvents(mockSelectedCalendar, mockCredential);
expect(result.items[0].isAllDay).toBe(true);
expect(result.items[0].start).toEqual(new Date("2023-12-01"));
expect(result.items[0].end).toEqual(new Date("2023-12-02"));
});
test("should handle free events (transparent)", async () => {
const mockEventsResponse = {
data: {
nextSyncToken: "new-sync-token",
items: [
{
id: "event-1",
iCalUID: "event-1@cal.com",
summary: "Free Event",
start: {
dateTime: "2023-12-01T10:00:00Z",
},
end: {
dateTime: "2023-12-01T11:00:00Z",
},
status: "confirmed",
transparency: "transparent",
},
],
},
};
mockClient.events.list.mockResolvedValue(mockEventsResponse);
const result = await adapter.fetchEvents(mockSelectedCalendar, mockCredential);
expect(result.items[0].busy).toBe(false);
});
test("should handle pagination", async () => {
const mockEventsResponse1 = {
data: {
nextPageToken: "page-2",
items: [
{
id: "event-1",
summary: "Event 1",
start: { dateTime: "2023-12-01T10:00:00Z" },
end: { dateTime: "2023-12-01T11:00:00Z" },
},
],
},
};
const mockEventsResponse2 = {
data: {
nextSyncToken: "final-sync-token",
items: [
{
id: "event-2",
summary: "Event 2",
start: { dateTime: "2023-12-01T12:00:00Z" },
end: { dateTime: "2023-12-01T13:00:00Z" },
},
],
},
};
mockClient.events.list
.mockResolvedValueOnce(mockEventsResponse1)
.mockResolvedValueOnce(mockEventsResponse2);
const result = await adapter.fetchEvents(mockSelectedCalendar, mockCredential);
expect(mockClient.events.list).toHaveBeenCalledTimes(2);
expect(result.items).toHaveLength(2);
expect(result.syncToken).toBe("final-sync-token");
});
test("should filter out events without ID", async () => {
const mockEventsResponse = {
data: {
nextSyncToken: "new-sync-token",
items: [
{
id: "event-1",
summary: "Valid Event",
start: { dateTime: "2023-12-01T10:00:00Z" },
end: { dateTime: "2023-12-01T11:00:00Z" },
},
{
summary: "Invalid Event",
start: { dateTime: "2023-12-01T12:00:00Z" },
end: { dateTime: "2023-12-01T13:00:00Z" },
},
{
id: "",
summary: "Empty ID Event",
start: { dateTime: "2023-12-01T14:00:00Z" },
end: { dateTime: "2023-12-01T15:00:00Z" },
},
],
},
};
mockClient.events.list.mockResolvedValue(mockEventsResponse);
const result = await adapter.fetchEvents(mockSelectedCalendar, mockCredential);
expect(result.items).toHaveLength(1);
expect(result.items[0].id).toBe("event-1");
});
});
});

Some files were not shown because too many files have changed in this diff Show More