diff --git a/.env.example b/.env.example index a59043e802..1e3bb1a739 100644 --- a/.env.example +++ b/.env.example @@ -408,3 +408,6 @@ NEXT_PUBLIC_WEBSITE_TERMS_URL= # NEXT_PUBLIC_LOGGER_LEVEL=3 sets to log info, warn, error and fatal logs. # [0: silly & upwards, 1: trace & upwards, 2: debug & upwards, 3: info & upwards, 4: warn & upwards, 5: error & fatal, 6: fatal] NEXT_PUBLIC_LOGGER_LEVEL= + +# Used to use Replexica SDK, a tool for real-time AI-powered localization +REPLEXICA_API_KEY= \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index d732e1120a..561a50f162 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -64,6 +64,7 @@ "@radix-ui/react-switch": "^1.0.0", "@radix-ui/react-toggle-group": "^1.0.0", "@radix-ui/react-tooltip": "^1.0.0", + "@replexica/sdk": "^0.6.0", "@sentry/nextjs": "^8.8.0", "@stripe/react-stripe-js": "^1.10.0", "@stripe/stripe-js": "^1.35.0", diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 26367507cb..e7ca29bddc 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2791,6 +2791,7 @@ "salesforce_route_to_custom_lookup_field": "Route to a user that matches a lookup field on an account", "salesforce_option": "Salesforce Option", "lookup_field_name": "Lookup Field Name", + "translate_description_button": "Translate description to the visitor's browser language using AI", "rr_distribution_method": "Distribution", "rr_distribution_method_description": "Allows for optimising distribution for maximum availability or to aim for a more balanced assignment.", "rr_distribution_method_availability_title": "Maximize availability", diff --git a/apps/web/test/lib/handleChildrenEventTypes.test.ts b/apps/web/test/lib/handleChildrenEventTypes.test.ts index 22c8652181..a028816818 100644 --- a/apps/web/test/lib/handleChildrenEventTypes.test.ts +++ b/apps/web/test/lib/handleChildrenEventTypes.test.ts @@ -112,6 +112,7 @@ describe("handleChildrenEventTypes", () => { lockTimeZoneToggleOnBookingPage, useEventTypeDestinationCalendarEmail, secondaryEmailId, + autoTranslateDescriptionEnabled, ...evType } = mockFindFirstEventType({ id: 123, @@ -182,7 +183,7 @@ describe("handleChildrenEventTypes", () => { bookingLimits: undefined, }, }); - const { profileId, ...rest } = evType; + const { profileId, autoTranslateDescriptionEnabled, ...rest } = evType; expect(prismaMock.eventType.update).toHaveBeenCalledWith({ data: { ...rest, @@ -265,6 +266,7 @@ describe("handleChildrenEventTypes", () => { lockTimeZoneToggleOnBookingPage, useEventTypeDestinationCalendarEmail, secondaryEmailId, + autoTranslateDescriptionEnabled, ...evType } = mockFindFirstEventType({ id: 123, @@ -337,7 +339,7 @@ describe("handleChildrenEventTypes", () => { length: 30, }, }); - const { profileId, ...rest } = evType; + const { profileId, autoTranslateDescriptionEnabled, ...rest } = evType; expect(prismaMock.eventType.update).toHaveBeenCalledWith({ data: { ...rest, @@ -378,6 +380,7 @@ describe("handleChildrenEventTypes", () => { lockTimeZoneToggleOnBookingPage, useEventTypeDestinationCalendarEmail, secondaryEmailId, + autoTranslateDescriptionEnabled, ...evType } = mockFindFirstEventType({ metadata: { managedEventConfig: {} }, diff --git a/packages/features/bookings/Booker/components/EventMeta.tsx b/packages/features/bookings/Booker/components/EventMeta.tsx index 32fad2b847..889ec090ac 100644 --- a/packages/features/bookings/Booker/components/EventMeta.tsx +++ b/packages/features/bookings/Booker/components/EventMeta.tsx @@ -48,6 +48,8 @@ export const EventMeta = ({ | "recurringEvent" | "price" | "isDynamic" + | "fieldTranslations" + | "autoTranslateDescriptionEnabled" > | null; isPending: boolean; isPlatform?: boolean; @@ -101,6 +103,10 @@ export const EventMeta = ({ : isHalfFull ? "text-yellow-500" : "text-bookinghighlight"; + const browserLocale = navigator.language; // e.g. "en-US", "es-ES", "fr-FR" + const translatedDescription = (event?.fieldTranslations ?? []).find((translation) => + browserLocale.startsWith(translation.targetLang) + )?.translatedText; return (
@@ -120,9 +126,9 @@ export const EventMeta = ({ /> )} {event?.title} - {event.description && ( + {(event.description || translatedDescription) && ( -
+
)}
diff --git a/packages/features/bookings/types.ts b/packages/features/bookings/types.ts index 41eb8d2451..e598e88bd3 100644 --- a/packages/features/bookings/types.ts +++ b/packages/features/bookings/types.ts @@ -51,6 +51,8 @@ export type BookerEvent = Pick< | "bookingFields" | "seatsShowAvailabilityCount" | "isInstantEvent" + | "fieldTranslations" + | "autoTranslateDescriptionEnabled" > & { users: BookerEventUser[]; showInstantEventConnectNowModal: boolean } & { profile: BookerEventProfile }; export type ValidationErrors = { key: FieldPath; error: ErrorOption }[]; diff --git a/packages/features/eventtypes/components/tabs/setup/EventSetupTab.tsx b/packages/features/eventtypes/components/tabs/setup/EventSetupTab.tsx index 7ae3ea5691..6e1bb89f75 100644 --- a/packages/features/eventtypes/components/tabs/setup/EventSetupTab.tsx +++ b/packages/features/eventtypes/components/tabs/setup/EventSetupTab.tsx @@ -1,3 +1,4 @@ +import { useSession } from "next-auth/react"; import { useState } from "react"; import { Controller, useFormContext } from "react-hook-form"; import type { UseFormGetValues, UseFormSetValue, Control, FormState } from "react-hook-form"; @@ -20,6 +21,7 @@ export type EventSetupTabProps = Pick< >; export const EventSetupTab = (props: EventSetupTabProps & { urlPrefix: string; hasOrgBranding: boolean }) => { const { t } = useLocale(); + const session = useSession(); const isPlatform = useIsPlatform(); const formMethods = useFormContext(); const { eventType, team, urlPrefix, hasOrgBranding } = props; @@ -29,6 +31,7 @@ export const EventSetupTab = (props: EventSetupTabProps & { urlPrefix: string; h const [firstRender, setFirstRender] = useState(true); const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled"); + const autoTranslateDescriptionEnabled = formMethods.watch("autoTranslateDescriptionEnabled"); const multipleDurationOptions = [ 5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 150, 180, 240, 300, 360, 420, 480, @@ -95,6 +98,17 @@ export const EventSetupTab = (props: EventSetupTabProps & { urlPrefix: string; h )}
+
+ { + formMethods.setValue("autoTranslateDescriptionEnabled", value, { shouldDirty: true }); + }} + disabled={!session.data?.user.org?.id} + tooltip={!session.data?.user.org?.id ? t("orgs_upgrade_to_enable_feature") : undefined} + /> +
()({ metadata: true, lockTimeZoneToggleOnBookingPage: true, requiresConfirmation: true, + autoTranslateDescriptionEnabled: true, + fieldTranslations: { + select: { + translatedText: true, + targetLang: true, + }, + }, requiresBookerEmailVerification: true, recurringEvent: true, price: true, @@ -293,6 +300,8 @@ export const getPublicEvent = async ( }, isInstantEvent: false, showInstantEventConnectNowModal: false, + autoTranslateDescriptionEnabled: false, + fieldTranslations: [], }; } diff --git a/packages/features/eventtypes/lib/types.ts b/packages/features/eventtypes/lib/types.ts index e9003b3112..382002f647 100644 --- a/packages/features/eventtypes/lib/types.ts +++ b/packages/features/eventtypes/lib/types.ts @@ -2,6 +2,7 @@ import type { z } from "zod"; import type { EventLocationType } from "@calcom/core/location"; import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect"; +import type { EventTypeTranslation } from "@calcom/prisma/client"; import type { PeriodType, SchedulingType } from "@calcom/prisma/enums"; import type { BookerLayoutSettings, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { customInputSchema } from "@calcom/prisma/zod-utils"; @@ -109,6 +110,8 @@ export type FormValues = { seatsShowAttendees: boolean | null; seatsShowAvailabilityCount: boolean | null; seatsPerTimeSlotEnabled: boolean; + autoTranslateDescriptionEnabled: boolean; + fieldTranslations: EventTypeTranslation[]; scheduleName: string; minimumBookingNotice: number; minimumBookingNoticeInDurationType: number; diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index b3197f46df..02e3fe7aac 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -168,6 +168,7 @@ export const CALCOM_PRIVATE_API_ROUTE = process.env.CALCOM_PRIVATE_API_ROUTE || export const WEBSITE_PRIVACY_POLICY_URL = process.env.NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL || "https://cal.com/privacy"; export const WEBSITE_TERMS_URL = process.env.NEXT_PUBLIC_WEBSITE_TERMS_URL || "https://cal.com/terms"; +export const REPLEXICA_API_KEY = process.env.REPLEXICA_API_KEY; /** * The maximum number of days we should check for if we don't find all required bookable days @@ -190,3 +191,4 @@ export const RECORDING_DEFAULT_ICON = IS_PRODUCTION export const RECORDING_IN_PROGRESS_ICON = IS_PRODUCTION ? `${WEBAPP_URL}/stop-recording.svg` : `https://app.cal.com/stop-recording.svg`; + diff --git a/packages/lib/defaultEvents.ts b/packages/lib/defaultEvents.ts index 2723c9c5d9..14dfaceeea 100644 --- a/packages/lib/defaultEvents.ts +++ b/packages/lib/defaultEvents.ts @@ -110,6 +110,8 @@ const commons = { useEventTypeDestinationCalendarEmail: false, secondaryEmailId: null, secondaryEmail: null, + autoTranslateDescriptionEnabled: false, + fieldTranslations: [], maxLeadThreshold: null, }; diff --git a/packages/lib/server/eventTypeSelect.ts b/packages/lib/server/eventTypeSelect.ts index 6613958d98..3748949e8f 100644 --- a/packages/lib/server/eventTypeSelect.ts +++ b/packages/lib/server/eventTypeSelect.ts @@ -13,6 +13,7 @@ export const eventTypeSelect = Prisma.validator()({ title: true, requiresConfirmation: true, requiresConfirmationWillBlockSlot: true, + autoTranslateDescriptionEnabled: true, position: true, offsetStart: true, profileId: true, diff --git a/packages/lib/server/repository/eventType.ts b/packages/lib/server/repository/eventType.ts index 6e124206df..9cbbd6fb97 100644 --- a/packages/lib/server/repository/eventType.ts +++ b/packages/lib/server/repository/eventType.ts @@ -445,6 +445,13 @@ export class EventTypeRepository { requiresConfirmation: true, requiresConfirmationWillBlockSlot: true, requiresBookerEmailVerification: true, + autoTranslateDescriptionEnabled: true, + fieldTranslations: { + select: { + translatedText: true, + targetLang: true, + }, + }, recurringEvent: true, hideCalendarNotes: true, hideCalendarEventDetails: true, diff --git a/packages/lib/server/service/replexica.ts b/packages/lib/server/service/replexica.ts new file mode 100644 index 0000000000..9d0954b32e --- /dev/null +++ b/packages/lib/server/service/replexica.ts @@ -0,0 +1,60 @@ +import { ReplexicaEngine } from "@replexica/sdk"; + +import { REPLEXICA_API_KEY } from "@calcom/lib/constants"; + +export class ReplexicaService { + private static engine = new ReplexicaEngine({ + apiKey: REPLEXICA_API_KEY, + }); + + /** + * Localizes text from one language to another + * @param text The text to localize + * @param sourceLocale The source language locale + * @param targetLocale The target language locale + * @returns The localized text + */ + static async localizeText(text: string, sourceLocale: string, targetLocale: string): Promise { + if (!text?.trim()) { + return text; + } + + try { + const result = await this.engine.localizeText(text, { + sourceLocale, + targetLocale, + }); + + return result; + } catch (error) { + return text; + } + } + + /** + * Localizes an array of texts from one language to another + * @param texts Array of texts to localize + * @param sourceLocale The source language locale + * @param targetLocale The target language locale + * @returns The localized texts array + */ + static async localizeTexts(texts: string[], sourceLocale: string, targetLocale: string): Promise { + if (!texts.length) { + return texts; + } + + try { + const result = await this.engine.localizeChat( + texts.map((text) => ({ name: "NO_NAME", text: text.trim() })), + { + sourceLocale, + targetLocale, + } + ); + + return result.map((chat: { name: string; text: string }) => chat.text); + } catch (error) { + return texts; + } + } +} diff --git a/packages/lib/test/builder.ts b/packages/lib/test/builder.ts index b1219af561..42c5de580b 100644 --- a/packages/lib/test/builder.ts +++ b/packages/lib/test/builder.ts @@ -138,6 +138,7 @@ export const buildEventType = (eventType?: Partial): EventType => { secondaryEmailId: null, isRRWeightsEnabled: false, eventTypeColor: null, + autoTranslateDescriptionEnabled: false, ...eventType, }; }; diff --git a/packages/platform/atoms/event-types/hooks/useEventTypeForm.ts b/packages/platform/atoms/event-types/hooks/useEventTypeForm.ts index 6ec614fe4a..eb1712ee01 100644 --- a/packages/platform/atoms/event-types/hooks/useEventTypeForm.ts +++ b/packages/platform/atoms/event-types/hooks/useEventTypeForm.ts @@ -109,6 +109,7 @@ export const useEventTypeForm = ({ }, })), seatsPerTimeSlotEnabled: eventType.seatsPerTimeSlot, + autoTranslateDescriptionEnabled: eventType.autoTranslateDescriptionEnabled, rescheduleWithSameRoundRobinHost: eventType.rescheduleWithSameRoundRobinHost, assignAllTeamMembers: eventType.assignAllTeamMembers, aiPhoneCallConfig: { diff --git a/packages/prisma/migrations/20241114210330_add_event_type_translation_model/migration.sql b/packages/prisma/migrations/20241114210330_add_event_type_translation_model/migration.sql new file mode 100644 index 0000000000..2b6be7c0e8 --- /dev/null +++ b/packages/prisma/migrations/20241114210330_add_event_type_translation_model/migration.sql @@ -0,0 +1,30 @@ +-- CreateEnum +CREATE TYPE "EventTypeAutoTranslatedField" AS ENUM ('DESCRIPTION'); + +-- AlterTable +ALTER TABLE "EventType" ADD COLUMN "autoTranslateDescriptionEnabled" BOOLEAN NOT NULL DEFAULT false; + +-- CreateTable +CREATE TABLE "EventTypeTranslation" ( + "id" TEXT NOT NULL, + "eventTypeId" INTEGER NOT NULL, + "field" "EventTypeAutoTranslatedField" NOT NULL, + "sourceLang" TEXT NOT NULL, + "targetLang" TEXT NOT NULL, + "translatedText" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" INTEGER NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + "updatedBy" INTEGER, + + CONSTRAINT "EventTypeTranslation_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "EventTypeTranslation_eventTypeId_field_targetLang_idx" ON "EventTypeTranslation"("eventTypeId", "field", "targetLang"); + +-- CreateIndex +CREATE UNIQUE INDEX "EventTypeTranslation_eventTypeId_field_targetLang_key" ON "EventTypeTranslation"("eventTypeId", "field", "targetLang"); + +-- AddForeignKey +ALTER TABLE "EventTypeTranslation" ADD CONSTRAINT "EventTypeTranslation_eventTypeId_fkey" FOREIGN KEY ("eventTypeId") REFERENCES "EventType"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index b1ac89be8f..4965f8272d 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -110,6 +110,7 @@ model EventType { requiresConfirmation Boolean @default(false) requiresConfirmationWillBlockSlot Boolean @default(false) requiresBookerEmailVerification Boolean @default(false) + autoTranslateDescriptionEnabled Boolean @default(false) /// @zod.custom(imports.recurringEventType) recurringEvent Json? disableGuests Boolean @default(false) @@ -149,6 +150,7 @@ model EventType { useEventTypeDestinationCalendarEmail Boolean @default(false) aiPhoneCallConfig AIPhoneCallConfiguration? isRRWeightsEnabled Boolean @default(false) + fieldTranslations EventTypeTranslation[] maxLeadThreshold Int? /// @zod.custom(imports.eventTypeColor) @@ -1590,3 +1592,24 @@ model AttributeToUser { @@unique([memberId, attributeOptionId]) } + +enum EventTypeAutoTranslatedField { + DESCRIPTION // Currently the only field we translate +} + +model EventTypeTranslation { + id String @id @default(cuid()) + eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade) + eventTypeId Int + field EventTypeAutoTranslatedField + sourceLang String + targetLang String + translatedText String @db.Text + createdAt DateTime @default(now()) + createdBy Int + updatedAt DateTime @updatedAt + updatedBy Int? + + @@unique([eventTypeId, field, targetLang]) + @@index([eventTypeId, field, targetLang]) +} diff --git a/packages/trpc/server/routers/viewer/eventTypes/update.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/update.handler.ts index 6706dbfdc0..9a73c89379 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/update.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/update.handler.ts @@ -35,6 +35,7 @@ type User = { id: SessionUser["profile"]["id"] | null; }; selectedCalendars: SessionUser["selectedCalendars"]; + organizationId: number | null; }; type UpdateOptions = { @@ -74,6 +75,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { secondaryEmailId, aiPhoneCallConfig, isRRWeightsEnabled, + autoTranslateDescriptionEnabled, ...rest } = input; @@ -153,6 +155,8 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { const data: Prisma.EventTypeUpdateInput = { ...rest, + // autoTranslate feature is allowed for org users only + autoTranslateDescriptionEnabled: !!(ctx.user.organizationId && autoTranslateDescriptionEnabled), bookingFields, isRRWeightsEnabled, metadata: rest.metadata === null ? Prisma.DbNull : (rest.metadata as Prisma.InputJsonObject), diff --git a/yarn.lock b/yarn.lock index 436318fee4..02ec1d6862 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5045,6 +5045,7 @@ __metadata: "@radix-ui/react-switch": ^1.0.0 "@radix-ui/react-toggle-group": ^1.0.0 "@radix-ui/react-tooltip": ^1.0.0 + "@replexica/sdk": ^0.6.0 "@sentry/nextjs": ^8.8.0 "@stripe/react-stripe-js": ^1.10.0 "@stripe/stripe-js": ^1.35.0 @@ -9980,7 +9981,7 @@ __metadata: languageName: node linkType: hard -"@paralleldrive/cuid2@npm:2.2.2": +"@paralleldrive/cuid2@npm:2.2.2, @paralleldrive/cuid2@npm:^2.2.2": version: 2.2.2 resolution: "@paralleldrive/cuid2@npm:2.2.2" dependencies: @@ -12366,6 +12367,31 @@ __metadata: languageName: node linkType: hard +"@replexica/sdk@npm:^0.6.0": + version: 0.6.0 + resolution: "@replexica/sdk@npm:0.6.0" + dependencies: + "@paralleldrive/cuid2": ^2.2.2 + "@replexica/spec": 0.18.0 + jsdom: ^25.0.1 + typescript: ^5.6.3 + vitest: ^2.1.4 + zod: ^3.23.8 + checksum: 95a239cf24025a8ea1a03b0720c8a1bd351f73777015fd3a8b74995740fb191c87d157ac04e7b1824f9b5b8f27cef3ce1b7126deb7bf7683b74b8435d3de1238 + languageName: node + linkType: hard + +"@replexica/spec@npm:0.18.0": + version: 0.18.0 + resolution: "@replexica/spec@npm:0.18.0" + dependencies: + typescript: ^5.6.3 + vitest: ^2.1.4 + zod: ^3.23.8 + checksum: d9afcfd965b804d395436cc6daf9a051142e13658297e45f742a6397699360e8f28d43399c4c31077b250ca965e1733200bf8c63eae328b4960320d2dd6ae5b5 + languageName: node + linkType: hard + "@resvg/resvg-wasm@npm:2.4.0": version: 2.4.0 resolution: "@resvg/resvg-wasm@npm:2.4.0" @@ -17462,6 +17488,18 @@ __metadata: languageName: node linkType: hard +"@vitest/expect@npm:2.1.5": + version: 2.1.5 + resolution: "@vitest/expect@npm:2.1.5" + dependencies: + "@vitest/spy": 2.1.5 + "@vitest/utils": 2.1.5 + chai: ^5.1.2 + tinyrainbow: ^1.2.0 + checksum: feb3dc9397d43a84525ee1966efc940d2175a4ca939b142239a875d43de1efbbfcd347440c2703f74560643c079b0ea1b52a46b80b7e4945f6d5a9ab5762c007 + languageName: node + linkType: hard + "@vitest/mocker@npm:2.1.1": version: 2.1.1 resolution: "@vitest/mocker@npm:2.1.1" @@ -17482,6 +17520,25 @@ __metadata: languageName: node linkType: hard +"@vitest/mocker@npm:2.1.5": + version: 2.1.5 + resolution: "@vitest/mocker@npm:2.1.5" + dependencies: + "@vitest/spy": 2.1.5 + estree-walker: ^3.0.3 + magic-string: ^0.30.12 + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 165db0dfdbd78da68be2e880638f5a8ff603745d80acbe0353d4d3c72233d9991545dda746db8c66e592baf9c8d880310681042da9b819affeca9f7a2dd7f389 + languageName: node + linkType: hard + "@vitest/pretty-format@npm:2.1.1, @vitest/pretty-format@npm:^2.1.1": version: 2.1.1 resolution: "@vitest/pretty-format@npm:2.1.1" @@ -17491,6 +17548,15 @@ __metadata: languageName: node linkType: hard +"@vitest/pretty-format@npm:2.1.5, @vitest/pretty-format@npm:^2.1.5": + version: 2.1.5 + resolution: "@vitest/pretty-format@npm:2.1.5" + dependencies: + tinyrainbow: ^1.2.0 + checksum: 0d470aabe0b27825c19da9e922bd2da073da5a7bdf0db7b101107c83f3071d728d98633b465d73b90c3fd68926f750fbf50d322140853fb30bad51389c766074 + languageName: node + linkType: hard + "@vitest/runner@npm:2.1.1": version: 2.1.1 resolution: "@vitest/runner@npm:2.1.1" @@ -17501,6 +17567,16 @@ __metadata: languageName: node linkType: hard +"@vitest/runner@npm:2.1.5": + version: 2.1.5 + resolution: "@vitest/runner@npm:2.1.5" + dependencies: + "@vitest/utils": 2.1.5 + pathe: ^1.1.2 + checksum: 93ee4c4e23a958a33b0214859f3e5552e2dcaf18625302ac8e824e22ffc4cadb52e6de0b62a1dcfeb7aeae59cde097c21792c9b753f4bde6fbb391f3902e5193 + languageName: node + linkType: hard + "@vitest/snapshot@npm:2.1.1": version: 2.1.1 resolution: "@vitest/snapshot@npm:2.1.1" @@ -17512,6 +17588,17 @@ __metadata: languageName: node linkType: hard +"@vitest/snapshot@npm:2.1.5": + version: 2.1.5 + resolution: "@vitest/snapshot@npm:2.1.5" + dependencies: + "@vitest/pretty-format": 2.1.5 + magic-string: ^0.30.12 + pathe: ^1.1.2 + checksum: bb61537367e4f51071234a7917f4c12eb564a243fb9dcd056b44d22cd3b108d2e0da428a8d0fe0363dd12e9b9aeabe155e3d4ae7c849c2e6be932329f6977801 + languageName: node + linkType: hard + "@vitest/spy@npm:2.1.1, @vitest/spy@npm:^2.1.0-beta.1": version: 2.1.1 resolution: "@vitest/spy@npm:2.1.1" @@ -17521,6 +17608,15 @@ __metadata: languageName: node linkType: hard +"@vitest/spy@npm:2.1.5": + version: 2.1.5 + resolution: "@vitest/spy@npm:2.1.5" + dependencies: + tinyspy: ^3.0.2 + checksum: db5aec91b3ac9130537c0e619940cbb8aebbd7fb2af4da9c42804d0bc7179dec75cd9573b56d8a1ec1c845e5751e3ba1dbc30b07fa38d637f241d77b45c37fbe + languageName: node + linkType: hard + "@vitest/ui@npm:^2.1.1": version: 2.1.1 resolution: "@vitest/ui@npm:2.1.1" @@ -17549,6 +17645,17 @@ __metadata: languageName: node linkType: hard +"@vitest/utils@npm:2.1.5": + version: 2.1.5 + resolution: "@vitest/utils@npm:2.1.5" + dependencies: + "@vitest/pretty-format": 2.1.5 + loupe: ^3.1.2 + tinyrainbow: ^1.2.0 + checksum: f12f6d95d058b537afd9e0b0af1075a80bf77789f32b75bf6a110a2bbf5afd08930ee71a0bd53bfb42bc923e4706d5e26c41df0e1c2854db1a668a011d6c010a + languageName: node + linkType: hard + "@volar/language-core@npm:1.11.1, @volar/language-core@npm:~1.11.1": version: 1.11.1 resolution: "@volar/language-core@npm:1.11.1" @@ -20429,6 +20536,19 @@ __metadata: languageName: node linkType: hard +"chai@npm:^5.1.2": + version: 5.1.2 + resolution: "chai@npm:5.1.2" + dependencies: + assertion-error: ^2.0.1 + check-error: ^2.1.1 + deep-eql: ^5.0.1 + loupe: ^3.1.0 + pathval: ^2.0.0 + checksum: f2341967ab5632612548d372c27b46219adad3af35021d8cba2ae3c262f588de2c60cb3f004e6ad40e363a9cad6d20d0de51f00e7e9ac31cce17fb05d4efa316 + languageName: node + linkType: hard + "chalk@npm:3.0.0, chalk@npm:^3.0.0": version: 3.0.0 resolution: "chalk@npm:3.0.0" @@ -22184,6 +22304,15 @@ __metadata: languageName: node linkType: hard +"cssstyle@npm:^4.1.0": + version: 4.1.0 + resolution: "cssstyle@npm:4.1.0" + dependencies: + rrweb-cssom: ^0.7.1 + checksum: a8f5746430c42347e76dc830548f3a296882e42a90af188ae44e4c1a4131aec246b0b6c8562e3e6e4fa0ff14aeee5cd14a0e2fe5a7105dcf39f98eb70d16b634 + languageName: node + linkType: hard + "csstype@npm:^3.0.2": version: 3.1.0 resolution: "csstype@npm:3.1.0" @@ -22374,6 +22503,16 @@ __metadata: languageName: node linkType: hard +"data-urls@npm:^5.0.0": + version: 5.0.0 + resolution: "data-urls@npm:5.0.0" + dependencies: + whatwg-mimetype: ^4.0.0 + whatwg-url: ^14.0.0 + checksum: 5c40568c31b02641a70204ff233bc4e42d33717485d074244a98661e5f2a1e80e38fe05a5755dfaf2ee549f2ab509d6a3af2a85f4b2ad2c984e5d176695eaf46 + languageName: node + linkType: hard + "data-view-buffer@npm:^1.0.1": version: 1.0.1 resolution: "data-view-buffer@npm:1.0.1" @@ -22498,7 +22637,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:^4.3.6": +"debug@npm:^4.3.6, debug@npm:^4.3.7": version: 4.3.7 resolution: "debug@npm:4.3.7" dependencies: @@ -23989,6 +24128,13 @@ __metadata: languageName: node linkType: hard +"es-module-lexer@npm:^1.5.4": + version: 1.5.4 + resolution: "es-module-lexer@npm:1.5.4" + checksum: a0cf04fb92d052647ac7d818d1913b98d3d3d0f5b9d88f0eafb993436e4c3e2c958599db68839d57f2dfa281fdf0f60e18d448eb78fc292c33c0f25635b6854f + languageName: node + linkType: hard + "es-object-atoms@npm:^1.0.0": version: 1.0.0 resolution: "es-object-atoms@npm:1.0.0" @@ -25199,6 +25345,13 @@ __metadata: languageName: node linkType: hard +"expect-type@npm:^1.1.0": + version: 1.1.0 + resolution: "expect-type@npm:1.1.0" + checksum: 65d25ec10bca32bcf650dcfe734532acc4b7a73677c656f299a7cbed273b5c4d6a3dab11af76f452645d54a95c4ef39fc73772f2c8eb6684ba35672958d6f3b3 + languageName: node + linkType: hard + "expect@npm:^29.0.0": version: 29.6.1 resolution: "expect@npm:29.6.1" @@ -27674,6 +27827,15 @@ __metadata: languageName: node linkType: hard +"html-encoding-sniffer@npm:^4.0.0": + version: 4.0.0 + resolution: "html-encoding-sniffer@npm:4.0.0" + dependencies: + whatwg-encoding: ^3.1.1 + checksum: 3339b71dab2723f3159a56acf541ae90a408ce2d11169f00fe7e0c4663d31d6398c8a4408b504b4eec157444e47b084df09b3cb039c816660f0dd04846b8957d + languageName: node + linkType: hard + "html-entities@npm:^2.1.0": version: 2.3.3 resolution: "html-entities@npm:2.3.3" @@ -27864,7 +28026,7 @@ __metadata: languageName: node linkType: hard -"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.1": +"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.1, http-proxy-agent@npm:^7.0.2": version: 7.0.2 resolution: "http-proxy-agent@npm:7.0.2" dependencies: @@ -27995,7 +28157,7 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^7.0.0": +"https-proxy-agent@npm:^7.0.0, https-proxy-agent@npm:^7.0.5": version: 7.0.5 resolution: "https-proxy-agent@npm:7.0.5" dependencies: @@ -30516,6 +30678,40 @@ __metadata: languageName: node linkType: hard +"jsdom@npm:^25.0.1": + version: 25.0.1 + resolution: "jsdom@npm:25.0.1" + dependencies: + cssstyle: ^4.1.0 + data-urls: ^5.0.0 + decimal.js: ^10.4.3 + form-data: ^4.0.0 + html-encoding-sniffer: ^4.0.0 + http-proxy-agent: ^7.0.2 + https-proxy-agent: ^7.0.5 + is-potential-custom-element-name: ^1.0.1 + nwsapi: ^2.2.12 + parse5: ^7.1.2 + rrweb-cssom: ^0.7.1 + saxes: ^6.0.0 + symbol-tree: ^3.2.4 + tough-cookie: ^5.0.0 + w3c-xmlserializer: ^5.0.0 + webidl-conversions: ^7.0.0 + whatwg-encoding: ^3.1.1 + whatwg-mimetype: ^4.0.0 + whatwg-url: ^14.0.0 + ws: ^8.18.0 + xml-name-validator: ^5.0.0 + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + checksum: b637d28445d570014195b3c77d06e54ef69d1f807eaf61388cb470e4d9227244e7fe2e0c32b6df03ac4fe35f746d7c721672d9136ecebb49d2e61a04ab9628e0 + languageName: node + linkType: hard + "jsesc@npm:^2.5.1": version: 2.5.2 resolution: "jsesc@npm:2.5.2" @@ -31753,6 +31949,13 @@ __metadata: languageName: node linkType: hard +"loupe@npm:^3.1.2": + version: 3.1.2 + resolution: "loupe@npm:3.1.2" + checksum: 4a75bbe8877a1ced3603e08b1095cd6f4c987c50fe63719fdc3009029560f91e07a915e7f6eff1322bb62bfb2a2beeef06b13ccb3c12f81bda9f3674434dcab9 + languageName: node + linkType: hard + "lower-case-first@npm:^1.0.0": version: 1.0.2 resolution: "lower-case-first@npm:1.0.2" @@ -32006,6 +32209,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.12": + version: 0.30.12 + resolution: "magic-string@npm:0.30.12" + dependencies: + "@jridgewell/sourcemap-codec": ^1.5.0 + checksum: 3f0d23b74371765f0e6cad4284eebba0ac029c7a55e39292de5aa92281afb827138cb2323d24d2924f6b31f138c3783596c5ccaa98653fe9cf122e1f81325b59 + languageName: node + linkType: hard + "mailhog@npm:^4.16.0": version: 4.16.0 resolution: "mailhog@npm:4.16.0" @@ -34148,6 +34360,13 @@ __metadata: languageName: node linkType: hard +"nwsapi@npm:^2.2.12": + version: 2.2.13 + resolution: "nwsapi@npm:2.2.13" + checksum: d34fb7838517c3c7e8cc824e443275b08b57f6a025a860693d18c56ddcfd176e32df9bf0ae7f5a95c7a32981501caa1f9fda31b59f28aa72a4b9d01f573a8e6b + languageName: node + linkType: hard + "nwsapi@npm:^2.2.4": version: 2.2.4 resolution: "nwsapi@npm:2.2.4" @@ -36840,6 +37059,13 @@ __metadata: languageName: node linkType: hard +"punycode@npm:^2.3.1": + version: 2.3.1 + resolution: "punycode@npm:2.3.1" + checksum: bb0a0ceedca4c3c57a9b981b90601579058903c62be23c5e8e843d2c2d4148a3ecf029d5133486fb0e1822b098ba8bba09e89d6b21742d02fa26bda6441a6fb2 + languageName: node + linkType: hard + "pupa@npm:^3.1.0": version: 3.1.0 resolution: "pupa@npm:3.1.0" @@ -39303,6 +39529,13 @@ __metadata: languageName: node linkType: hard +"rrweb-cssom@npm:^0.7.1": + version: 0.7.1 + resolution: "rrweb-cssom@npm:0.7.1" + checksum: 62e410ddbaaba6abc196c3bbfa8de4952e0a134d9f2b454ee293039bf9931322d806e14d52ed122a5c2bd332a868b9da2e99358fb6232c33758b5ede86d992c8 + languageName: node + linkType: hard + "rtl-css-js@npm:^1.16.1": version: 1.16.1 resolution: "rtl-css-js@npm:1.16.1" @@ -40611,6 +40844,13 @@ __metadata: languageName: node linkType: hard +"std-env@npm:^3.8.0": + version: 3.8.0 + resolution: "std-env@npm:3.8.0" + checksum: ad4554485c2d09138a1d0f03944245e169510e6f5200b7d30fcdd4536e27a2a9a2fd934caff7ef58ebbe21993fa0e2b9e5b1979f431743c925305863b7ff36d5 + languageName: node + linkType: hard + "stdin-discarder@npm:^0.2.1": version: 0.2.2 resolution: "stdin-discarder@npm:0.2.2" @@ -42119,6 +42359,13 @@ __metadata: languageName: node linkType: hard +"tinyexec@npm:^0.3.1": + version: 0.3.1 + resolution: "tinyexec@npm:0.3.1" + checksum: 691b531d464bdc09eeba934e43d8ac2a74c9d22a4bec9cd7f4991375c64e22712f7e5a95ba243a9369a478afd34d41171359012a2248ea49615cd2816ab12959 + languageName: node + linkType: hard + "tinyglobby@npm:^0.2.6": version: 0.2.6 resolution: "tinyglobby@npm:0.2.6" @@ -42146,6 +42393,13 @@ __metadata: languageName: node linkType: hard +"tinypool@npm:^1.0.1": + version: 1.0.2 + resolution: "tinypool@npm:1.0.2" + checksum: 752f23114d8fc95a9497fc812231d6d0a63728376aa11e6e8499c10423a91112e760e388887ea7854f1b16977c321f07c0eab061ec2f60f6761e58b184aac880 + languageName: node + linkType: hard + "tinyrainbow@npm:^1.2.0": version: 1.2.0 resolution: "tinyrainbow@npm:1.2.0" @@ -42153,7 +42407,7 @@ __metadata: languageName: node linkType: hard -"tinyspy@npm:^3.0.0": +"tinyspy@npm:^3.0.0, tinyspy@npm:^3.0.2": version: 3.0.2 resolution: "tinyspy@npm:3.0.2" checksum: 5db671b2ff5cd309de650c8c4761ca945459d7204afb1776db9a04fb4efa28a75f08517a8620c01ee32a577748802231ad92f7d5b194dc003ee7f987a2a06337 @@ -42170,6 +42424,24 @@ __metadata: languageName: node linkType: hard +"tldts-core@npm:^6.1.61": + version: 6.1.61 + resolution: "tldts-core@npm:6.1.61" + checksum: 659411060be80658bd492cf0f3a38223c0cc15e2ffb402e219e51dcfcffcdd581c00e63e494944d4e7ff0a0aeb321058f7811d9089fac455ae0c10b4f5beffd1 + languageName: node + linkType: hard + +"tldts@npm:^6.1.32": + version: 6.1.61 + resolution: "tldts@npm:6.1.61" + dependencies: + tldts-core: ^6.1.61 + bin: + tldts: bin/cli.js + checksum: 1d2feafe6160147a2f5ea520ebc84c77153edacd0d69510726ee0c13b2fce8b84d1bad79f2c5337ac361d297d0d16815fe5d3c51c88a7822d2320a75f6d2dea3 + languageName: node + linkType: hard + "tmp@npm:0.2.1": version: 0.2.1 resolution: "tmp@npm:0.2.1" @@ -42284,6 +42556,15 @@ __metadata: languageName: node linkType: hard +"tough-cookie@npm:^5.0.0": + version: 5.0.0 + resolution: "tough-cookie@npm:5.0.0" + dependencies: + tldts: ^6.1.32 + checksum: 774f6c939c96f74b5847361f7e11e0d69383681d21a35a2d37a20956638e614ec521782d2d20bcb32b58638ff337bba87cc72fb72c987bd02ea0fdfc93994cdb + languageName: node + linkType: hard + "tough-cookie@npm:~2.5.0": version: 2.5.0 resolution: "tough-cookie@npm:2.5.0" @@ -42303,6 +42584,15 @@ __metadata: languageName: node linkType: hard +"tr46@npm:^5.0.0": + version: 5.0.0 + resolution: "tr46@npm:5.0.0" + dependencies: + punycode: ^2.3.1 + checksum: 8d8b021f8e17675ebf9e672c224b6b6cfdb0d5b92141349e9665c14a2501c54a298d11264bbb0b17b447581e1e83d4fc3c038c929f3d210e3964d4be47460288 + languageName: node + linkType: hard + "tr46@npm:~0.0.3": version: 0.0.3 resolution: "tr46@npm:0.0.3" @@ -43215,6 +43505,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:^5.6.3": + version: 5.6.3 + resolution: "typescript@npm:5.6.3" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: ba302f8822777ebefb28b554105f3e074466b671e7444ec6b75dadc008a62f46f373d9e57ceced1c433756d06c8b7dc569a7eefdf3a9573122a49205ff99021a + languageName: node + linkType: hard + "typescript@patch:typescript@5.3.3#~builtin": version: 5.3.3 resolution: "typescript@patch:typescript@npm%3A5.3.3#~builtin::version=5.3.3&hash=1f5320" @@ -43245,6 +43545,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@^5.6.3#~builtin": + version: 5.6.3 + resolution: "typescript@patch:typescript@npm%3A5.6.3#~builtin::version=5.6.3&hash=1f5320" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: ade87bce2363ee963eed0e4ca8a312ea02c81873ebd53609bc3f6dc0a57f6e61ad7e3fb8cbb7f7ab8b5081cbee801b023f7c4823ee70b1c447eae050e6c7622b + languageName: node + linkType: hard + "tzdata@npm:^1.0.30": version: 1.0.40 resolution: "tzdata@npm:1.0.40" @@ -43901,6 +44211,21 @@ __metadata: languageName: node linkType: hard +"vite-node@npm:2.1.5": + version: 2.1.5 + resolution: "vite-node@npm:2.1.5" + dependencies: + cac: ^6.7.14 + debug: ^4.3.7 + es-module-lexer: ^1.5.4 + pathe: ^1.1.2 + vite: ^5.0.0 + bin: + vite-node: vite-node.mjs + checksum: b7c12c330f0cf5bb63fd6d36c38d7f1f9ed5cea5a7407db626945038c66e3e4165f79bdbcb928c6a289f299f434fb0a30bd1ab5c9c0d50eb892164bcdc37d167 + languageName: node + linkType: hard + "vite-plugin-dts@npm:^3.7.2, vite-plugin-dts@npm:^3.7.3": version: 3.7.3 resolution: "vite-plugin-dts@npm:3.7.3" @@ -44199,6 +44524,56 @@ __metadata: languageName: node linkType: hard +"vitest@npm:^2.1.4": + version: 2.1.5 + resolution: "vitest@npm:2.1.5" + dependencies: + "@vitest/expect": 2.1.5 + "@vitest/mocker": 2.1.5 + "@vitest/pretty-format": ^2.1.5 + "@vitest/runner": 2.1.5 + "@vitest/snapshot": 2.1.5 + "@vitest/spy": 2.1.5 + "@vitest/utils": 2.1.5 + chai: ^5.1.2 + debug: ^4.3.7 + expect-type: ^1.1.0 + magic-string: ^0.30.12 + pathe: ^1.1.2 + std-env: ^3.8.0 + tinybench: ^2.9.0 + tinyexec: ^0.3.1 + tinypool: ^1.0.1 + tinyrainbow: ^1.2.0 + vite: ^5.0.0 + vite-node: 2.1.5 + why-is-node-running: ^2.3.0 + peerDependencies: + "@edge-runtime/vm": "*" + "@types/node": ^18.0.0 || >=20.0.0 + "@vitest/browser": 2.1.5 + "@vitest/ui": 2.1.5 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@types/node": + optional: true + "@vitest/browser": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + bin: + vitest: vitest.mjs + checksum: 1a7ddc055aae55452d7fce86cd9aeb92dcdcc3d51105dd87c34c2c0ac2c508c3e20be36c855568686fe19fb06e9f2568fca511ae70511a782155a3449dbdc083 + languageName: node + linkType: hard + "vm-browserify@npm:^1.0.1, vm-browserify@npm:^1.1.2": version: 1.1.2 resolution: "vm-browserify@npm:1.1.2" @@ -44247,6 +44622,15 @@ __metadata: languageName: node linkType: hard +"w3c-xmlserializer@npm:^5.0.0": + version: 5.0.0 + resolution: "w3c-xmlserializer@npm:5.0.0" + dependencies: + xml-name-validator: ^5.0.0 + checksum: 593acc1fdab3f3207ec39d851e6df0f3fa41a36b5809b0ace364c7a6d92e351938c53424a7618ce8e0fbaffee8be2e8e070a5734d05ee54666a8bdf1a376cc40 + languageName: node + linkType: hard + "walker@npm:^1.0.8": version: 1.0.8 resolution: "walker@npm:1.0.8" @@ -44519,6 +44903,15 @@ __metadata: languageName: node linkType: hard +"whatwg-encoding@npm:^3.1.1": + version: 3.1.1 + resolution: "whatwg-encoding@npm:3.1.1" + dependencies: + iconv-lite: 0.6.3 + checksum: f75a61422421d991e4aec775645705beaf99a16a88294d68404866f65e92441698a4f5b9fa11dd609017b132d7b286c3c1534e2de5b3e800333856325b549e3c + languageName: node + linkType: hard + "whatwg-fetch@npm:^3.4.1, whatwg-fetch@npm:^3.6.2": version: 3.6.20 resolution: "whatwg-fetch@npm:3.6.20" @@ -44533,6 +44926,13 @@ __metadata: languageName: node linkType: hard +"whatwg-mimetype@npm:^4.0.0": + version: 4.0.0 + resolution: "whatwg-mimetype@npm:4.0.0" + checksum: f97edd4b4ee7e46a379f3fb0e745de29fe8b839307cc774300fd49059fcdd560d38cb8fe21eae5575b8f39b022f23477cc66e40b0355c2851ce84760339cef30 + languageName: node + linkType: hard + "whatwg-url@npm:^12.0.0, whatwg-url@npm:^12.0.1": version: 12.0.1 resolution: "whatwg-url@npm:12.0.1" @@ -44553,6 +44953,16 @@ __metadata: languageName: node linkType: hard +"whatwg-url@npm:^14.0.0": + version: 14.0.0 + resolution: "whatwg-url@npm:14.0.0" + dependencies: + tr46: ^5.0.0 + webidl-conversions: ^7.0.0 + checksum: 4b5887e50f786583bead70916413e67a381d2126899b9eb5c67ce664bba1e7ec07cdff791404581ce73c6190d83c359c9ca1d50711631217905db3877dec075c + languageName: node + linkType: hard + "whatwg-url@npm:^5.0.0": version: 5.0.0 resolution: "whatwg-url@npm:5.0.0" @@ -44906,6 +45316,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:^8.18.0": + version: 8.18.0 + resolution: "ws@npm:8.18.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 91d4d35bc99ff6df483bdf029b9ea4bfd7af1f16fc91231a96777a63d263e1eabf486e13a2353970efc534f9faa43bdbf9ee76525af22f4752cbc5ebda333975 + languageName: node + linkType: hard + "xdg-basedir@npm:^5.0.1, xdg-basedir@npm:^5.1.0": version: 5.1.0 resolution: "xdg-basedir@npm:5.1.0" @@ -44974,6 +45399,13 @@ __metadata: languageName: node linkType: hard +"xml-name-validator@npm:^5.0.0": + version: 5.0.0 + resolution: "xml-name-validator@npm:5.0.0" + checksum: 86effcc7026f437701252fcc308b877b4bc045989049cfc79b0cc112cb365cf7b009f4041fab9fb7cd1795498722c3e9fe9651afc66dfa794c16628a639a4c45 + languageName: node + linkType: hard + "xml-parse-from-string@npm:^1.0.0": version: 1.0.1 resolution: "xml-parse-from-string@npm:1.0.1" @@ -45386,6 +45818,13 @@ __metadata: languageName: node linkType: hard +"zod@npm:^3.23.8": + version: 3.23.8 + resolution: "zod@npm:3.23.8" + checksum: 15949ff82118f59c893dacd9d3c766d02b6fa2e71cf474d5aa888570c469dbf5446ac5ad562bb035bf7ac9650da94f290655c194f4a6de3e766f43febd432c5c + languageName: node + linkType: hard + "zustand@npm:^4.3.2": version: 4.5.2 resolution: "zustand@npm:4.5.2"