From 0b37520f0dcb007cf1be568fff230daf51b7edeb Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:28:31 +0200 Subject: [PATCH] fix: translate location dropdown group labels and options in event-type settings (#26784) * fix: translate location dropdown group labels in event-type settings Co-Authored-By: morgan@cal.com * fix: use requesting user's locale for team event type translations For team event types, the current user might not be in eventType.users, causing translations to default to English. This fix queries the user's locale directly from the database when not found in the event type users. Co-Authored-By: morgan@cal.com * refactor: pass userLocale as parameter instead of database call Per PR feedback, pass the authenticated user's locale as an optional parameter to getEventTypeById instead of making an extra database call. The locale is passed from ctx.user.locale in the tRPC handler. Co-Authored-By: morgan@cal.com * fix: provide userLocale in apiv2 getEventTypeById * fix: remove unused ts comments --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../event-types.repository.ts | 8 +- .../services/event-types-atom.service.ts | 62 ++++++++-------- apps/web/public/static/locales/en/common.json | 1 + apps/web/public/static/locales/fr/common.json | 1 + packages/app-store/locations.ts | 22 +++--- .../eventtypes/lib/getEventTypeById.ts | 74 ++++++++++--------- .../routers/viewer/eventTypes/get.handler.ts | 1 + 7 files changed, 87 insertions(+), 82 deletions(-) diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.repository.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.repository.ts index 84488df660..0a9c6b0d47 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.repository.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/event-types.repository.ts @@ -1,12 +1,11 @@ +import { getEventTypeById } from "@calcom/platform-libraries/event-types"; +import type { PrismaClient } from "@calcom/prisma"; +import { Injectable } from "@nestjs/common"; import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { UsersService } from "@/modules/users/services/users.service"; import { UserWithProfile } from "@/modules/users/users.repository"; -import { Injectable } from "@nestjs/common"; - -import { getEventTypeById } from "@calcom/platform-libraries/event-types"; -import type { PrismaClient } from "@calcom/prisma"; @Injectable() export class EventTypesRepository_2024_04_15 { @@ -54,6 +53,7 @@ export class EventTypesRepository_2024_04_15 { currentOrganizationId: this.usersService.getUserMainOrgId(user), eventTypeId, userId: user.id, + userLocale: user.locale ?? "en", prisma: this.dbRead.prisma as unknown as PrismaClient, isUserOrganizationAdmin, isTrpcCall: true, diff --git a/apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts b/apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts index 3750f2486f..210d5ec089 100644 --- a/apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts +++ b/apps/api/v2/src/modules/atoms/services/event-types-atom.service.ts @@ -1,3 +1,32 @@ +import type { TeamQuery } from "@calcom/platform-libraries"; +import { checkAdminOrOwner, getClientSecretFromPayment } from "@calcom/platform-libraries"; +import type { + App, + CredentialDataWithTeamName, + CredentialOwner, + CredentialPayload, + LocationOption, + TDependencyData, +} from "@calcom/platform-libraries/app-store"; +import { + enrichUserWithDelegationConferencingCredentialsWithoutOrgId, + getAppFromSlug, + getEnabledAppsFromCredentials, +} from "@calcom/platform-libraries/app-store"; +import { + bulkUpdateEventsToDefaultLocation, + bulkUpdateTeamEventsToDefaultLocation, + EventTypeMetaDataSchema, + getBulkTeamEventTypes, + getBulkUserEventTypes, + getEventTypeById, + getPublicEvent, + type PublicEventType, + TUpdateEventTypeInputSchema, + updateEventType, +} from "@calcom/platform-libraries/event-types"; +import type { PrismaClient } from "@calcom/prisma"; +import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common"; import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service"; import { systemBeforeFieldEmail } from "@/ee/event-types/event-types_2024_06_14/transformers"; import { AtomsRepository } from "@/modules/atoms/atoms.repository"; @@ -9,34 +38,6 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service"; import { UsersService } from "@/modules/users/services/users.service"; import { UserWithProfile } from "@/modules/users/users.repository"; -import { Injectable, NotFoundException, ForbiddenException, BadRequestException } from "@nestjs/common"; - -import { checkAdminOrOwner, getClientSecretFromPayment } from "@calcom/platform-libraries"; -import type { TeamQuery } from "@calcom/platform-libraries"; -import { enrichUserWithDelegationConferencingCredentialsWithoutOrgId } from "@calcom/platform-libraries/app-store"; -import { getEnabledAppsFromCredentials, getAppFromSlug } from "@calcom/platform-libraries/app-store"; -import type { - App, - TDependencyData, - CredentialOwner, - CredentialPayload, - CredentialDataWithTeamName, - LocationOption, -} from "@calcom/platform-libraries/app-store"; -import { type PublicEventType, getPublicEvent } from "@calcom/platform-libraries/event-types"; -import { - getEventTypeById, - bulkUpdateEventsToDefaultLocation, - bulkUpdateTeamEventsToDefaultLocation, - getBulkUserEventTypes, - getBulkTeamEventTypes, -} from "@calcom/platform-libraries/event-types"; -import { - updateEventType, - TUpdateEventTypeInputSchema, - EventTypeMetaDataSchema, -} from "@calcom/platform-libraries/event-types"; -import type { PrismaClient } from "@calcom/prisma"; type EnabledAppType = App & { credential: CredentialDataWithTeamName; @@ -81,6 +82,7 @@ export class EventTypesAtomService { currentOrganizationId: this.usersService.getUserMainOrgId(user), eventTypeId, userId: user.id, + userLocale: user.locale ?? "en", prisma: this.dbRead.prisma as unknown as PrismaClient, isUserOrganizationAdmin, isTrpcCall: true, @@ -136,8 +138,6 @@ export class EventTypesAtomService { input: { ...body, id: eventTypeId, bookingFields }, ctx: { user: eventTypeUser, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore prisma: this.dbWrite.prisma, }, }); @@ -166,8 +166,6 @@ export class EventTypesAtomService { input: { ...body, id: eventTypeId, bookingFields }, ctx: { user: eventTypeUser, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore prisma: this.dbWrite.prisma, }, }); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index bf207402ec..0e29c4a136 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -739,6 +739,7 @@ "booking_reschedule_confirmation": "Reschedule your {{eventTypeTitle}} with {{profileName}}", "in_person_meeting": "In-person meeting", "in_person": "In Person (Organizer Address)", + "in_person_category": "In person", "link_meeting": "Link meeting", "phone_number": "Phone number", "attendee_phone_number": "Attendee phone number", diff --git a/apps/web/public/static/locales/fr/common.json b/apps/web/public/static/locales/fr/common.json index 1a87726ca9..b6120cfc8d 100644 --- a/apps/web/public/static/locales/fr/common.json +++ b/apps/web/public/static/locales/fr/common.json @@ -719,6 +719,7 @@ "booking_reschedule_confirmation": "Replanifiez votre {{eventTypeTitle}} avec {{profileName}}", "in_person_meeting": "Rendez-vous en personne", "in_person": "En personne (adresse de l'organisateur)", + "in_person_category": "En personne", "link_meeting": "Rendez-vous avec lien", "phone_number": "Numéro de téléphone", "attendee_phone_number": "Numéro de téléphone du participant", diff --git a/packages/app-store/locations.ts b/packages/app-store/locations.ts index 366827f687..1d25f62dd7 100644 --- a/packages/app-store/locations.ts +++ b/packages/app-store/locations.ts @@ -1,20 +1,20 @@ /** * TODO: Consolidate this file with BookingLocationService and add tests */ -import type { TFunction } from "i18next"; -import { isValidPhoneNumber } from "libphonenumber-js/max"; -import { z } from "zod"; import { appStoreMetadata } from "@calcom/app-store/bookerAppsMetaData"; import logger from "@calcom/lib/logger"; import { BookingStatus } from "@calcom/prisma/enums"; import type { Ensure, Optional } from "@calcom/types/utils"; +import type { TFunction } from "i18next"; +import { isValidPhoneNumber } from "libphonenumber-js/max"; +import { z } from "zod"; import type { EventLocationTypeFromAppMeta } from "../types/App"; import { + DailyLocationType as importedDailyLocationType, MeetLocationType as importedMeetLocationType, MSTeamsLocationType as importedMSTeamsLocationType, - DailyLocationType as importedDailyLocationType, } from "./constants"; export const MeetLocationType = importedMeetLocationType; @@ -26,7 +26,7 @@ export type DefaultEventLocationType = { type: DefaultEventLocationTypeEnum; label: string; messageForOrganizer: string; - category: "in person" | "conferencing" | "other" | "phone"; + category: "in_person_category" | "conferencing" | "other" | "phone"; linkType: "static"; supportsCustomLabel?: boolean; @@ -114,7 +114,7 @@ export const defaultLocations: DefaultEventLocationType[] = [ attendeeInputPlaceholder: "enter_address", defaultValueVariable: "attendeeAddress", iconUrl: "/map-pin-dark.svg", - category: "in person", + category: "in_person_category", linkType: "static", supportsCustomLabel: true, }, @@ -143,7 +143,7 @@ export const defaultLocations: DefaultEventLocationType[] = [ variable: "locationAddress", defaultValueVariable: "address", iconUrl: "/map-pin-dark.svg", - category: "in person", + category: "in_person_category", linkType: "static", }, { @@ -242,7 +242,7 @@ for (const [appName, meta] of Object.entries(appStoreMetadata)) { for (const [key, value] of Object.entries(location)) { if (typeof value === "string") { // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore + // @ts-expect-error location[key] = value.replace(/{SLUG}/g, meta.slug).replace(/{TITLE}/g, meta.name); } } @@ -397,7 +397,7 @@ export const getLocationValueForDB = ( eventLocations: LocationObject[] ) => { let bookingLocation = bookingLocationTypeOrValue; - let conferenceCredentialId: number | undefined = undefined; + let conferenceCredentialId: number | undefined; eventLocations.forEach((location) => { if (location.type === bookingLocationTypeOrValue) { @@ -481,8 +481,8 @@ export const getTranslatedLocation = ( const translatedLocation = location.type.startsWith("integrations:") ? eventLocationType.label : translateAbleKeys.includes(locationKey) - ? t(locationKey) - : locationKey; + ? t(locationKey) + : locationKey; return translatedLocation; }; diff --git a/packages/features/eventtypes/lib/getEventTypeById.ts b/packages/features/eventtypes/lib/getEventTypeById.ts index a774c516c4..8897a347c0 100644 --- a/packages/features/eventtypes/lib/getEventTypeById.ts +++ b/packages/features/eventtypes/lib/getEventTypeById.ts @@ -4,7 +4,9 @@ import { getLocationGroupedOptions } from "@calcom/app-store/server"; import { getEventTypeAppData } from "@calcom/app-store/utils"; import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils"; import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields"; +import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; +import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository"; import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; import { WEBSITE_URL } from "@calcom/lib/constants"; @@ -16,11 +18,9 @@ import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; import { getTranslation } from "@calcom/lib/server/i18n"; import type { PrismaClient } from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; -import { SchedulingType, MembershipRole } from "@calcom/prisma/enums"; +import { MembershipRole, SchedulingType } from "@calcom/prisma/enums"; import { customInputSchema } from "@calcom/prisma/zod-utils"; -import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; import { TRPCError } from "@trpc/server"; -import { getOrganizationRepository } from "@calcom/features/ee/organizations/di/OrganizationRepository.container"; interface getEventTypeByIdProps { eventTypeId: number; @@ -29,6 +29,7 @@ interface getEventTypeByIdProps { isTrpcCall?: boolean; isUserOrganizationAdmin: boolean; currentOrganizationId: number | null; + userLocale?: string | null; } export type EventType = Awaited>; @@ -40,6 +41,7 @@ export const getEventTypeById = async ({ prisma, isTrpcCall = false, isUserOrganizationAdmin, + userLocale, }: getEventTypeByIdProps) => { const userSelect = { name: true, @@ -58,7 +60,7 @@ export const getEventTypeById = async ({ eventTypeId, isUserOrganizationAdmin, currentOrganizationId, - prisma + prisma, }); if (!rawEventType) { @@ -90,8 +92,8 @@ export const getEventTypeById = async ({ ...child, owner: child.owner ? await userRepo.enrichUserWithItsProfile({ - user: child.owner, - }) + user: child.owner, + }) : null, }); } @@ -141,19 +143,19 @@ export const getEventTypeById = async ({ children: childrenWithUserProfile.flatMap((ch) => ch.owner !== null ? { - ...ch, - owner: { - ...ch.owner, - avatar: getUserAvatarUrl(ch.owner), - email: ch.owner.email, - name: ch.owner.name ?? "", - username: ch.owner.username ?? "", - membership: - restEventType.team?.members.find((tm) => tm.user.id === ch.owner?.id)?.role || - MembershipRole.MEMBER, - }, - created: true, - } + ...ch, + owner: { + ...ch.owner, + avatar: getUserAvatarUrl(ch.owner), + email: ch.owner.email, + name: ch.owner.name ?? "", + username: ch.owner.username ?? "", + membership: + restEventType.team?.members.find((tm) => tm.user.id === ch.owner?.id)?.role || + MembershipRole.MEMBER, + }, + created: true, + } : [] ), }; @@ -187,7 +189,7 @@ export const getEventTypeById = async ({ const currentUser = eventType.users.find((u) => u.id === userId); - const t = await getTranslation(currentUser?.locale ?? "en", "common"); + const t = await getTranslation(userLocale ?? currentUser?.locale ?? "en", "common"); if (!currentUser?.id && !eventType.teamId) { throw new TRPCError({ @@ -224,19 +226,19 @@ export const getEventTypeById = async ({ const isOrgEventType = !!eventTypeObject.team?.parentId; const teamMembers = eventTypeObject.team ? eventTeamMembershipsWithUserProfile - .filter((member) => member.accepted || isOrgEventType) - .map((member) => { - const user: typeof member.user & { avatar: string } = { - ...member.user, - avatar: getUserAvatarUrl(member.user), - }; - return { - ...user, - profileId: user.profile.id, - eventTypes: user.eventTypes.map((evTy) => evTy.slug), - membership: member.role, - }; - }) + .filter((member) => member.accepted || isOrgEventType) + .map((member) => { + const user: typeof member.user & { avatar: string } = { + ...member.user, + avatar: getUserAvatarUrl(member.user), + }; + return { + ...user, + profileId: user.profile.id, + eventTypes: user.eventTypes.map((evTy) => evTy.slug), + membership: member.role, + }; + }) : []; // Find the current users membership so we can check role to enable/disable deletion. @@ -274,7 +276,9 @@ export async function getRawEventType({ }: Omit) { const eventTypeRepo = new EventTypeRepository(prisma); const organizationRepo = getOrganizationRepository(); - const isUserInPlatformOrganization = currentOrganizationId ? !!(await organizationRepo.findById({ id: currentOrganizationId }))?.isPlatform : false; + const isUserInPlatformOrganization = currentOrganizationId + ? !!(await organizationRepo.findById({ id: currentOrganizationId }))?.isPlatform + : false; if (isUserOrganizationAdmin && currentOrganizationId && isUserInPlatformOrganization) { // Platform Organization Admin can access any event of the organization even without being a member of the sub-teams @@ -284,7 +288,7 @@ export async function getRawEventType({ }); } - // Regular(Non Platform) Organization member(admin/non-admin) can access any event-type they are are a member of including sub-team events and Regular Team(non-subteam) events. + // Regular(Non Platform) Organization member(admin/non-admin) can access any event-type they are are a member of including sub-team events and Regular Team(non-subteam) events. // Remember an organization member can stay a part of Regular Team still if that team hasn't been moved to the organization yet. return await eventTypeRepo.findById({ id: eventTypeId, diff --git a/packages/trpc/server/routers/viewer/eventTypes/get.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/get.handler.ts index 8597879363..d94a858f93 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/get.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/get.handler.ts @@ -20,5 +20,6 @@ export const getHandler = ({ ctx, input }: GetOptions) => { prisma: ctx.prisma, isTrpcCall: true, isUserOrganizationAdmin: !!ctx.user?.organization?.isOrgAdmin, + userLocale: ctx.user.locale, }); };