Files
calendar/packages/features/tasker/tasks/translateWorkflowStepData.ts
T
Udit TakkarGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Udit TakkarUdit Takkar
4081d11fbe feat: workflow auto translation (#27087)
* feat: workflow auto translation

* tests: add unit tests

* refactor: tests and workflow

* fix: type err

* fix: type err

* fix: remove redundant index on WorkflowStepTranslation

The @@index on [workflowStepId, field, targetLocale] duplicates the @@unique
constraint on the same columns. A unique index already provides efficient
lookups, so the separate @@index adds storage overhead and write latency
without benefit.

Addresses Cubic AI review feedback (confidence 9/10).

Co-Authored-By: unknown <>

* fix: correct locale mapping when translation API returns null

Map translations with their corresponding locales before filtering to
preserve correct locale-to-translation associations. Previously, filtering
out null translations would reindex the array, causing incorrect locale
mappings when any translation in the batch failed.

Also fixes pre-existing lint warnings:
- Move exports to end of file
- Add explicit return type to processTranslations
- Replace ternary with if-else for upsertMany selection

Co-Authored-By: udit@cal.com <udit222001@gmail.com>

* fix: address review feedback for workflow auto-translation

- Add change detection before creating translation tasks
- Rename userLocale to sourceLocale in task props for clarity
- Show source language in UI with new translation key
- Extract SUPPORTED_LOCALES to shared translationConstants.ts
- Fix locale mapping bug in translateEventTypeData.ts
- Add WhatsApp translation support
- Abstract translation lookup into shared translationLookup.ts helper
- Restore if-else readability for SCANNING_WORKFLOW_STEPS

Co-authored-by: Udit Takkar <udit.takkar@cal.com>
Co-Authored-By: unknown <>

* fix: update test to use sourceLocale instead of userLocale

Co-Authored-By: unknown <>

* refactor: feedback

* fix: handle first time

* fix: tests

* fix: tests

* fix: address Cubic AI review feedback (confidence 9/10 issues)

- WhatsApp translation: Apply variable substitution using getSMSMessageWithVariables
  and clear contentSid when using translated body to ensure Twilio uses the
  translated text instead of the original template

- update.handler.ts: Change sourceLocale assignment from ?? to || for consistency
  with tasker payload behavior (line 481)

- ITranslationService.ts: Rename methods from plural to singular naming:
  - getWorkflowStepTranslations -> getWorkflowStepTranslation
  - getEventTypeTranslations -> getEventTypeTranslation
  Updated all call sites and tests accordingly

Co-Authored-By: unknown <>

* fix: address Cubic AI review feedback (confidence 9/10+ issues)

- Fix getSMSMessageWithVariables to handle WHATSAPP_ATTENDEE action for
  locale and timezone (confidence 9/10)
- Remove WhatsApp translation feature that set contentSid to undefined
  since Twilio ignores body parameter for WhatsApp and requires
  pre-approved Message Templates (confidence 10/10)

Co-Authored-By: unknown <>

* fix: translatio

* Add tests: packages/features/eventTypeTranslation/repositories/EventTypeTranslationRepository.test.ts

Generated by Paragon from proposal for PR #27087

* Add tests: packages/features/tasker/tasks/translateWorkflowStepData.test.ts

Generated by Paragon from proposal for PR #27087

* chore: nit

* chore: verfied atg

* fix: set sourceLocale for new steps, add shouldDirty to checkbox, remove spec docs

- Set sourceLocale fallback in addedSteps mapping to fix stale detection mismatch
- Add { shouldDirty: true } to autoTranslateEnabled checkbox onChange
- Remove specs/workflow-translation/ directory (planning docs, not for repo)

Co-authored-by: Udit Takkar <udit.07.takkar@gmail.com>
Co-Authored-By: unknown <>

* chore: add specs back

* fix: type error

* fix: type error

* fix: type err

* fix: tests

* refactor: feedback

* fix: type err

* refactor

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.takkar@cal.com>
Co-authored-by: Udit Takkar <udit.07.takkar@gmail.com>
2026-02-25 01:03:55 +05:30

99 lines
3.5 KiB
TypeScript

import { getTranslationService } from "@calcom/features/di/containers/TranslationService";
import { getWorkflowStepTranslationRepository } from "@calcom/features/ee/workflows/di/WorkflowStepTranslationRepository.container";
import { WorkflowStepRepository } from "@calcom/features/ee/workflows/repositories/WorkflowStepRepository";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import { WorkflowStepAutoTranslatedField } from "@calcom/prisma/enums";
import { z } from "zod";
const ZTranslateWorkflowStepDataPayloadSchema = z.object({
workflowStepId: z.number(),
reminderBody: z.string().nullable().optional(),
emailSubject: z.string().nullable().optional(),
sourceLocale: z.string(),
});
async function processTranslations({
text,
sourceLocale,
workflowStepId,
field,
}: {
text: string;
sourceLocale: string;
workflowStepId: number;
field: WorkflowStepAutoTranslatedField;
}): Promise<void> {
try {
const translationService = await getTranslationService();
const result = await translationService.translateText({ text, sourceLocale });
if (result.translations.length > 0) {
const translationData = result.translations.map(({ translatedText, targetLocale }) => ({
workflowStepId,
sourceLocale,
targetLocale,
translatedText,
}));
const workflowStepTranslationRepository = getWorkflowStepTranslationRepository();
if (field === WorkflowStepAutoTranslatedField.REMINDER_BODY) {
await workflowStepTranslationRepository.upsertManyBodyTranslations(translationData);
} else {
await workflowStepTranslationRepository.upsertManySubjectTranslations(translationData);
}
}
if (result.failedLocales.length > 0) {
logger.warn(
`Failed to translate workflow step ${field} to locales: ${result.failedLocales.join(", ")}`
);
}
} catch (error) {
logger.error(`Failed to process workflow step ${field} translations:`, error);
}
}
async function translateWorkflowStepData(payload: string): Promise<void> {
const { workflowStepId, reminderBody, emailSubject, sourceLocale } =
ZTranslateWorkflowStepDataPayloadSchema.parse(JSON.parse(payload));
const workflowStepRepository = new WorkflowStepRepository(prisma);
const workflowStep = await workflowStepRepository.findTranslationDataById(workflowStepId);
if (!workflowStep) {
logger.warn(`Workflow step ${workflowStepId} not found for translation task`);
return;
}
const sourceLocaleMatches = workflowStep.sourceLocale === sourceLocale;
const shouldTranslateBody =
Boolean(reminderBody) && sourceLocaleMatches && workflowStep.reminderBody === reminderBody;
const shouldTranslateSubject =
Boolean(emailSubject) && sourceLocaleMatches && workflowStep.emailSubject === emailSubject;
if (!shouldTranslateBody && !shouldTranslateSubject) {
logger.info(`Skipping stale translation task for workflow step ${workflowStepId}`);
return;
}
await Promise.all([
shouldTranslateBody &&
processTranslations({
text: reminderBody as string,
sourceLocale,
workflowStepId,
field: WorkflowStepAutoTranslatedField.REMINDER_BODY,
}),
shouldTranslateSubject &&
processTranslations({
text: emailSubject as string,
sourceLocale,
workflowStepId,
field: WorkflowStepAutoTranslatedField.EMAIL_SUBJECT,
}),
]);
}
export { ZTranslateWorkflowStepDataPayloadSchema, translateWorkflowStepData };