From 7b2cf9f3d87091f3630d31eade0e475edfe2089e Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Wed, 20 Mar 2024 23:07:01 +0530 Subject: [PATCH] fix: translate survey and migration script (#2290) Co-authored-by: Matthias Nannt --- .../components/dummyUI/types.ts | 1 - .../components/WidgetStatusIndicator.tsx | 4 +- .../components/MultipleChoiceMultiForm.tsx | 2 +- .../components/MultipleChoiceSingleForm.tsx | 2 +- .../edit/components/QuestionsView.tsx | 2 +- .../edit/components/SurveyMenuBar.tsx | 2 +- .../Validation.ts => lib/validation.ts} | 29 +- .../surveys/templates/templates.ts | 2 - apps/web/app/lib/questions.ts | 1 - .../data-migration-fix.ts | 49 +++ .../lib/i18n.ts | 261 ++++++++++----- packages/database/package.json | 3 +- packages/lib/i18n/i18n.mock.ts | 3 - packages/lib/i18n/i18n.test.ts | 29 +- packages/lib/i18n/utils.ts | 316 ++++++++++-------- packages/lib/survey/service.ts | 25 -- packages/types/LegacySurvey.ts | 1 - packages/types/surveys.ts | 1 - 18 files changed, 431 insertions(+), 302 deletions(-) rename apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/{components/Validation.ts => lib/validation.ts} (81%) create mode 100644 packages/database/migrations/20240318050527_add_languages_and_survey_languages/data-migration-fix.ts diff --git a/apps/formbricks-com/components/dummyUI/types.ts b/apps/formbricks-com/components/dummyUI/types.ts index 8a4b12db8b..e08a97dc48 100644 --- a/apps/formbricks-com/components/dummyUI/types.ts +++ b/apps/formbricks-com/components/dummyUI/types.ts @@ -241,7 +241,6 @@ export const ZSurveyConsentQuestion = ZSurveyQuestionBase.extend({ type: z.literal(TSurveyQuestionType.Consent), html: z.string().optional(), label: z.string(), - dismissButtonLabel: z.string().optional(), placeholder: z.string().optional(), logic: z.array(ZSurveyConsentLogic).optional(), }); diff --git a/apps/web/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator.tsx b/apps/web/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator.tsx index 64c8a75166..a32850814a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/components/WidgetStatusIndicator.tsx @@ -70,10 +70,10 @@ export default async function WidgetStatusIndicator({ environmentId, type }: Wid
- +
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/MultipleChoiceMultiForm.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/MultipleChoiceMultiForm.tsx index eb48b707f5..a7686720ab 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/MultipleChoiceMultiForm.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/MultipleChoiceMultiForm.tsx @@ -237,7 +237,7 @@ export default function MultipleChoiceMultiForm({ {question.choices && question.choices.map((choice, choiceIdx) => (
-
+
{ + return question.placeholder && + getLocalizedValue(question.placeholder, "default").trim() !== "" && + languages.length > 1 + ? isLabelValidForAllLanguages(question.placeholder, languages) + : true; + }, multipleChoiceMulti: (question: TSurveyMultipleChoiceMultiQuestion, languages: TSurveyLanguage[]) => { return handleI18nCheckForMultipleChoice(question, languages); }, @@ -44,14 +53,16 @@ const validationRules = { pictureSelection: (question: TSurveyPictureSelectionQuestion) => { return question.choices.length >= 2; }, + cta: (question: TSurveyCTAQuestion, languages: TSurveyLanguage[]) => { + return !question.required && question.dismissButtonLabel + ? isLabelValidForAllLanguages(question.dismissButtonLabel, languages) + : true; + }, // Assuming headline is of type TI18nString defaultValidation: (question: TSurveyQuestion, languages: TSurveyLanguage[]) => { - let isValid = isLabelValidForAllLanguages(question.headline, languages); - let isValidCTADismissLabel = true; + const isHeadlineValid = isLabelValidForAllLanguages(question.headline, languages); + let isValid = isHeadlineValid; const defaultLanguageCode = "default"; - if (question.type === "cta" && !question.required && question.dismissButtonLabel) { - isValidCTADismissLabel = isLabelValidForAllLanguages(question.dismissButtonLabel, languages); - } const fieldsToValidate = [ "subheader", "html", @@ -59,13 +70,11 @@ const validationRules = { "upperLabel", "backButtonLabel", "lowerLabel", - "placeholder", ]; for (const field of fieldsToValidate) { - if (question[field] && question[field][defaultLanguageCode]) { - isValid = - isValid && isLabelValidForAllLanguages(question[field], languages) && isValidCTADismissLabel; + if (question[field] && typeof question[field][defaultLanguageCode] !== "undefined") { + isValid = isValid && isLabelValidForAllLanguages(question[field], languages); } } diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/templates.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/templates.ts index 33ac95807a..a86c254c6d 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/templates/templates.ts +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/templates/templates.ts @@ -326,7 +326,6 @@ export const testTemplate: TTemplate = { headline: { default: "This is a Consent question" }, required: true, label: { default: "I agree to the terms and conditions" }, - dismissButtonLabel: "Skip", }, { id: createId(), @@ -334,7 +333,6 @@ export const testTemplate: TTemplate = { headline: { default: "This is a Consent question" }, required: false, label: { default: "I agree to the terms and conditions" }, - dismissButtonLabel: "Skip", }, ], thankYouCard: thankYouCardDefault, diff --git a/apps/web/app/lib/questions.ts b/apps/web/app/lib/questions.ts index 8738282a94..a38928af87 100644 --- a/apps/web/app/lib/questions.ts +++ b/apps/web/app/lib/questions.ts @@ -137,7 +137,6 @@ export const questionTypes: TSurveyQuestionType[] = [ headline: { default: "Terms and Conditions" }, html: { default: "" }, label: { default: "I agree to the terms and conditions" }, - dismissButtonLabel: "Skip", }, }, { diff --git a/packages/database/migrations/20240318050527_add_languages_and_survey_languages/data-migration-fix.ts b/packages/database/migrations/20240318050527_add_languages_and_survey_languages/data-migration-fix.ts new file mode 100644 index 0000000000..7412163e9f --- /dev/null +++ b/packages/database/migrations/20240318050527_add_languages_and_survey_languages/data-migration-fix.ts @@ -0,0 +1,49 @@ +// migration script to translate surveys where thankYouCard buttonLabel is a string or question subheaders are strings +import { PrismaClient } from "@prisma/client"; + +import { hasStringSubheaders, translateSurvey } from "./lib/i18n"; + +const prisma = new PrismaClient(); + +async function main() { + await prisma.$transaction( + async (tx) => { + // Translate Surveys + const surveys = await tx.survey.findMany({ + select: { + id: true, + questions: true, + thankYouCard: true, + welcomeCard: true, + }, + }); + + if (!surveys) { + // stop the migration if there are no surveys + return; + } + + for (const survey of surveys) { + if (typeof survey.thankYouCard.buttonLabel === "string" || hasStringSubheaders(survey.questions)) { + const translatedSurvey = translateSurvey(survey, []); + + // Save the translated survey + await tx.survey.update({ + where: { id: survey.id }, + data: { ...translatedSurvey }, + }); + } + } + }, + { + timeout: 50000, + } + ); +} + +main() + .catch(async (e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => await prisma.$disconnect()); diff --git a/packages/database/migrations/20240318050527_add_languages_and_survey_languages/lib/i18n.ts b/packages/database/migrations/20240318050527_add_languages_and_survey_languages/lib/i18n.ts index 153c84a94c..c00b2b048f 100644 --- a/packages/database/migrations/20240318050527_add_languages_and_survey_languages/lib/i18n.ts +++ b/packages/database/migrations/20240318050527_add_languages_and_survey_languages/lib/i18n.ts @@ -1,13 +1,35 @@ +import { + TLegacySurveyChoice, + TLegacySurveyQuestion, + TLegacySurveyThankYouCard, + TLegacySurveyWelcomeCard, +} from "@formbricks/types/LegacySurvey"; import { TLanguage } from "@formbricks/types/product"; import { TI18nString, TSurveyCTAQuestion, + TSurveyChoice, TSurveyConsentQuestion, + TSurveyMultipleChoiceSingleQuestion, TSurveyNPSQuestion, TSurveyOpenTextQuestion, + TSurveyQuestions, TSurveyRatingQuestion, TSurveyThankYouCard, TSurveyWelcomeCard, + ZSurveyCTAQuestion, + ZSurveyCalQuestion, + ZSurveyConsentQuestion, + ZSurveyFileUploadQuestion, + ZSurveyMultipleChoiceMultiQuestion, + ZSurveyMultipleChoiceSingleQuestion, + ZSurveyNPSQuestion, + ZSurveyOpenTextQuestion, + ZSurveyPictureSelectionQuestion, + ZSurveyQuestion, + ZSurveyRatingQuestion, + ZSurveyThankYouCard, + ZSurveyWelcomeCard, } from "@formbricks/types/surveys"; import { TSurvey, TSurveyMultipleChoiceMultiQuestion, TSurveyQuestion } from "@formbricks/types/surveys"; @@ -49,120 +71,177 @@ export const createI18nString = (text: string | TI18nString, languages: string[] }; // Function to translate a choice label -const translateChoice = (choice: any, languages: string[]) => { - // Assuming choice is a simple object and choice.label is a string. - return { - ...choice, - label: createI18nString(choice.label, languages), - }; +const translateChoice = (choice: TSurveyChoice | TLegacySurveyChoice, languages: string[]): TSurveyChoice => { + if (typeof choice.label !== "undefined") { + return { + ...choice, + label: createI18nString(choice.label, languages), + }; + } else { + return { + ...choice, + label: choice.label, + }; + } }; + export const translateWelcomeCard = ( - welcomeCard: TSurveyWelcomeCard, + welcomeCard: TSurveyWelcomeCard | TLegacySurveyWelcomeCard, languages: string[] ): TSurveyWelcomeCard => { const clonedWelcomeCard = structuredClone(welcomeCard); - clonedWelcomeCard.headline = createI18nString(welcomeCard.headline, languages); - clonedWelcomeCard.html = createI18nString(welcomeCard.html ?? "", languages); - if (clonedWelcomeCard.buttonLabel) { - clonedWelcomeCard.buttonLabel = createI18nString(clonedWelcomeCard.buttonLabel, languages); + if (typeof welcomeCard.headline !== "undefined") { + clonedWelcomeCard.headline = createI18nString(welcomeCard.headline ?? "", languages); + } + if (typeof welcomeCard.html !== "undefined") { + clonedWelcomeCard.html = createI18nString(welcomeCard.html ?? "", languages); + } + if (typeof welcomeCard.buttonLabel !== "undefined") { + clonedWelcomeCard.buttonLabel = createI18nString(clonedWelcomeCard.buttonLabel ?? "", languages); } - return clonedWelcomeCard; + return ZSurveyWelcomeCard.parse(clonedWelcomeCard); }; const translateThankYouCard = ( - thankYouCard: TSurveyThankYouCard, + thankYouCard: TSurveyThankYouCard | TLegacySurveyThankYouCard, languages: string[] ): TSurveyThankYouCard => { const clonedThankYouCard = structuredClone(thankYouCard); - clonedThankYouCard.headline = createI18nString( - thankYouCard.headline ? thankYouCard.headline : "", - languages - ); - if (clonedThankYouCard.subheader) { - clonedThankYouCard.subheader = createI18nString( - thankYouCard.subheader ? thankYouCard.subheader : "", - languages - ); + + if (typeof thankYouCard.headline !== "undefined") { + clonedThankYouCard.headline = createI18nString(thankYouCard.headline ?? "", languages); } - return clonedThankYouCard; + if (typeof thankYouCard.subheader !== "undefined") { + clonedThankYouCard.subheader = createI18nString(thankYouCard.subheader ?? "", languages); + } + + if (typeof clonedThankYouCard.buttonLabel !== "undefined") { + clonedThankYouCard.buttonLabel = createI18nString(thankYouCard.buttonLabel ?? "", languages); + } + return ZSurveyThankYouCard.parse(clonedThankYouCard); }; // Function that will translate a single question -const translateQuestion = (question: TSurveyQuestion, languages: string[]) => { +const translateQuestion = ( + question: TLegacySurveyQuestion | TSurveyQuestion, + languages: string[] +): TSurveyQuestion => { // Clone the question to avoid mutating the original const clonedQuestion = structuredClone(question); - clonedQuestion.headline = createI18nString(question.headline, languages); - if (clonedQuestion.subheader) { + //common question properties + if (typeof question.headline !== "undefined") { + clonedQuestion.headline = createI18nString(question.headline ?? "", languages); + } + + if (typeof question.subheader !== "undefined") { clonedQuestion.subheader = createI18nString(question.subheader ?? "", languages); } - if (clonedQuestion.buttonLabel) { + if (typeof question.buttonLabel !== "undefined") { clonedQuestion.buttonLabel = createI18nString(question.buttonLabel ?? "", languages); } - if (clonedQuestion.backButtonLabel) { + if (typeof question.backButtonLabel !== "undefined") { clonedQuestion.backButtonLabel = createI18nString(question.backButtonLabel ?? "", languages); } - if (question.type === "multipleChoiceSingle" || question.type === "multipleChoiceMulti") { - (clonedQuestion as TSurveyMultipleChoiceMultiQuestion | TSurveyMultipleChoiceMultiQuestion).choices = - question.choices.map((choice) => translateChoice(structuredClone(choice), languages)); - ( - clonedQuestion as TSurveyMultipleChoiceMultiQuestion | TSurveyMultipleChoiceMultiQuestion - ).otherOptionPlaceholder = question.otherOptionPlaceholder - ? createI18nString(question.otherOptionPlaceholder, languages) - : undefined; - } - if (question.type === "openText") { - (clonedQuestion as TSurveyOpenTextQuestion).placeholder = createI18nString( - question.placeholder ?? "", - languages - ); - } - if (question.type === "cta") { - if (question.dismissButtonLabel) { - (clonedQuestion as TSurveyCTAQuestion).dismissButtonLabel = createI18nString( - question.dismissButtonLabel, - languages - ); - } - if (question.html) { - (clonedQuestion as TSurveyCTAQuestion).html = createI18nString(question.html, languages); - } - } - if (question.type === "consent") { - if (question.html) { - (clonedQuestion as TSurveyConsentQuestion).html = createI18nString(question.html, languages); - } + switch (question.type) { + case "openText": + if (typeof question.placeholder !== "undefined") { + (clonedQuestion as TSurveyOpenTextQuestion).placeholder = createI18nString( + question.placeholder ?? "", + languages + ); + } + return ZSurveyOpenTextQuestion.parse(clonedQuestion); - if (question.label) { - (clonedQuestion as TSurveyConsentQuestion).label = createI18nString(question.label, languages); - } + case "multipleChoiceSingle": + case "multipleChoiceMulti": + (clonedQuestion as TSurveyMultipleChoiceSingleQuestion | TSurveyMultipleChoiceMultiQuestion).choices = + question.choices.map((choice) => { + return translateChoice(choice, languages); + }); + if ( + typeof (clonedQuestion as TSurveyMultipleChoiceSingleQuestion | TSurveyMultipleChoiceMultiQuestion) + .otherOptionPlaceholder !== "undefined" + ) { + ( + clonedQuestion as TSurveyMultipleChoiceSingleQuestion | TSurveyMultipleChoiceMultiQuestion + ).otherOptionPlaceholder = createI18nString(question.otherOptionPlaceholder ?? "", languages); + } + if (question.type === "multipleChoiceSingle") { + return ZSurveyMultipleChoiceSingleQuestion.parse(clonedQuestion); + } else return ZSurveyMultipleChoiceMultiQuestion.parse(clonedQuestion); + + case "cta": + if (typeof question.dismissButtonLabel !== "undefined") { + (clonedQuestion as TSurveyCTAQuestion).dismissButtonLabel = createI18nString( + question.dismissButtonLabel ?? "", + languages + ); + } + if (typeof question.html !== "undefined") { + (clonedQuestion as TSurveyCTAQuestion).html = createI18nString(question.html ?? "", languages); + } + return ZSurveyCTAQuestion.parse(clonedQuestion); + + case "consent": + if (typeof question.html !== "undefined") { + (clonedQuestion as TSurveyConsentQuestion).html = createI18nString(question.html ?? "", languages); + } + + if (typeof question.label !== "undefined") { + (clonedQuestion as TSurveyConsentQuestion).label = createI18nString(question.label ?? "", languages); + } + + return ZSurveyConsentQuestion.parse(clonedQuestion); + + case "nps": + if (typeof question.lowerLabel !== "undefined") { + (clonedQuestion as TSurveyNPSQuestion).lowerLabel = createI18nString( + question.lowerLabel ?? "", + languages + ); + } + if (typeof question.upperLabel !== "undefined") { + (clonedQuestion as TSurveyNPSQuestion).upperLabel = createI18nString( + question.upperLabel ?? "", + languages + ); + } + return ZSurveyNPSQuestion.parse(clonedQuestion); + + case "rating": + if (typeof question.lowerLabel !== "undefined") { + (clonedQuestion as TSurveyRatingQuestion).lowerLabel = createI18nString( + question.lowerLabel ?? "", + languages + ); + } + + if (typeof question.upperLabel !== "undefined") { + (clonedQuestion as TSurveyRatingQuestion).upperLabel = createI18nString( + question.upperLabel ?? "", + languages + ); + } + return ZSurveyRatingQuestion.parse(clonedQuestion); + + case "fileUpload": + return ZSurveyFileUploadQuestion.parse(clonedQuestion); + + case "pictureSelection": + return ZSurveyPictureSelectionQuestion.parse(clonedQuestion); + + case "cal": + return ZSurveyCalQuestion.parse(clonedQuestion); + + default: + return ZSurveyQuestion.parse(clonedQuestion); } - if (question.type === "nps") { - (clonedQuestion as TSurveyNPSQuestion).lowerLabel = createI18nString( - question.lowerLabel ?? "", - languages - ); - (clonedQuestion as TSurveyNPSQuestion).upperLabel = createI18nString( - question.upperLabel ?? "", - languages - ); - } - if (question.type === "rating") { - (clonedQuestion as TSurveyRatingQuestion).lowerLabel = createI18nString( - question.lowerLabel ?? "", - languages - ); - (clonedQuestion as TSurveyRatingQuestion).upperLabel = createI18nString( - question.upperLabel ?? "", - languages - ); - } - return clonedQuestion; }; export const extractLanguageIds = (languages: TLanguage[]): string[] => { @@ -172,14 +251,13 @@ export const extractLanguageIds = (languages: TLanguage[]): string[] => { // Function to translate an entire survey export const translateSurvey = ( survey: Pick, - surveyLanguages: TLanguage[] + languageCodes: string[] ): Pick => { - const languages = extractLanguageIds(surveyLanguages); const translatedQuestions = survey.questions.map((question) => { - return translateQuestion(question, languages); + return translateQuestion(question, languageCodes); }); - const translatedWelcomeCard = translateWelcomeCard(survey.welcomeCard, languages); - const translatedThankYouCard = translateThankYouCard(survey.thankYouCard, languages); + const translatedWelcomeCard = translateWelcomeCard(survey.welcomeCard, languageCodes); + const translatedThankYouCard = translateThankYouCard(survey.thankYouCard, languageCodes); const translatedSurvey = structuredClone(survey); return { ...translatedSurvey, @@ -188,3 +266,12 @@ export const translateSurvey = ( thankYouCard: translatedThankYouCard, }; }; + +export const hasStringSubheaders = (questions: TSurveyQuestions): boolean => { + for (const question of questions) { + if (typeof question.subheader !== "undefined") { + return true; + } + } + return false; +}; diff --git a/packages/database/package.json b/packages/database/package.json index ecc976a73a..20156c023e 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -24,7 +24,8 @@ "post-install": "pnpm generate", "predev": "pnpm generate", "data-migration:v1.6": "ts-node ./migrations/20240207041922_advanced_targeting/data-migration.ts", - "data-migration:mls": "ts-node ./migrations/20240318050527_add_languages_and_survey_languages/data-migration.ts" + "data-migration:mls": "ts-node ./migrations/20240318050527_add_languages_and_survey_languages/data-migration.ts", + "data-migration:mls-fix": "ts-node ./migrations/20240318050527_add_languages_and_survey_languages/data-migration-fix.ts" }, "dependencies": { "@prisma/client": "^5.11.0", diff --git a/packages/lib/i18n/i18n.mock.ts b/packages/lib/i18n/i18n.mock.ts index b86f074d4b..fa7993fa25 100644 --- a/packages/lib/i18n/i18n.mock.ts +++ b/packages/lib/i18n/i18n.mock.ts @@ -195,7 +195,6 @@ export const mockConsentQuestion: TSurveyConsentQuestion = { label: { default: "I agree to the terms and conditions", }, - dismissButtonLabel: "Skip", id: "av561aoif3i2hjlsl6krnsfm", type: TSurveyQuestionType.Consent, isDraft: true, @@ -449,14 +448,12 @@ export const mockTranslatedConsentQuestion = { ...mockConsentQuestion, headline: { default: "Terms and Conditions", de: "" }, label: { default: "I agree to the terms and conditions", de: "" }, - dismissButtonLabel: "Skip", }; export const mockLegacyConsentQuestion = { ...mockConsentQuestion, headline: "Terms and Conditions", label: "I agree to the terms and conditions", - dismissButtonLabel: "Skip", }; export const mockTranslatedDateQuestion = { diff --git a/packages/lib/i18n/i18n.test.ts b/packages/lib/i18n/i18n.test.ts index e9097bf8db..f96e2f2d88 100644 --- a/packages/lib/i18n/i18n.test.ts +++ b/packages/lib/i18n/i18n.test.ts @@ -1,7 +1,5 @@ import { describe, expect, it } from "vitest"; -import { TSurveyLanguage } from "@formbricks/types/surveys"; - import { mockLegacySurvey, mockSurvey, @@ -64,31 +62,8 @@ describe("translateThankYouCard", () => { describe("translateSurvey", () => { it("should translate all questions of a Survey", () => { - const languages: TSurveyLanguage[] = [ - { - default: true, - enabled: true, - language: { - id: "rp2di001zicbm3mk8je1ue9u", - code: "en", - alias: null, - createdAt: new Date(), - updatedAt: new Date(), - }, - }, - { - default: false, - enabled: true, - language: { - id: "cuuxfzls09sjkueg6lm6n7i0", - code: "de", - alias: null, - createdAt: new Date(), - updatedAt: new Date(), - }, - }, - ]; - const translatedSurvey = translateSurvey(mockSurvey, languages); + const languageCodes = ["default", "de"]; + const translatedSurvey = translateSurvey(mockSurvey, languageCodes); expect(translatedSurvey).toEqual(mockTranslatedSurvey); }); }); diff --git a/packages/lib/i18n/utils.ts b/packages/lib/i18n/utils.ts index 70b397eddf..3ee77da0ae 100644 --- a/packages/lib/i18n/utils.ts +++ b/packages/lib/i18n/utils.ts @@ -1,16 +1,40 @@ +import { + TLegacySurveyChoice, + TLegacySurveyQuestion, + TLegacySurveyThankYouCard, + TLegacySurveyWelcomeCard, +} from "@formbricks/types/LegacySurvey"; +import { TLanguage } from "@formbricks/types/product"; import { TI18nString, - TSurvey, TSurveyCTAQuestion, + TSurveyChoice, TSurveyConsentQuestion, - TSurveyLanguage, - TSurveyMultipleChoiceMultiQuestion, + TSurveyMultipleChoiceSingleQuestion, TSurveyNPSQuestion, TSurveyOpenTextQuestion, - TSurveyQuestion, TSurveyRatingQuestion, TSurveyThankYouCard, TSurveyWelcomeCard, + ZSurveyCTAQuestion, + ZSurveyCalQuestion, + ZSurveyConsentQuestion, + ZSurveyFileUploadQuestion, + ZSurveyMultipleChoiceMultiQuestion, + ZSurveyMultipleChoiceSingleQuestion, + ZSurveyNPSQuestion, + ZSurveyOpenTextQuestion, + ZSurveyPictureSelectionQuestion, + ZSurveyQuestion, + ZSurveyRatingQuestion, + ZSurveyThankYouCard, + ZSurveyWelcomeCard, +} from "@formbricks/types/surveys"; +import { + TSurvey, + TSurveyLanguage, + TSurveyMultipleChoiceMultiQuestion, + TSurveyQuestion, } from "@formbricks/types/surveys"; // Helper function to create an i18nString from a regular string. @@ -59,7 +83,7 @@ export function isI18nObject(obj: any): obj is TI18nString { return ( obj !== null && typeof obj === "object" && - Object.values(obj).every((value) => typeof value === "string") && + Object.values(obj).every((value) => typeof value !== "undefined") && Object.keys(obj).includes("default") ); } @@ -92,180 +116,198 @@ export const getEnabledLanguages = (surveyLanguages: TSurveyLanguage[]) => { return surveyLanguages.filter((surveyLanguage) => surveyLanguage.enabled); }; +const translateChoice = (choice: TSurveyChoice | TLegacySurveyChoice, languages: string[]): TSurveyChoice => { + if (typeof choice.label !== "undefined") { + return { + ...choice, + label: createI18nString(choice.label, languages), + }; + } else { + return { + ...choice, + label: choice.label, + }; + } +}; + // LGEGACY // Helper function to maintain backwards compatibility for old survey objects before Multi Language export const translateWelcomeCard = ( - welcomeCard: TSurveyWelcomeCard, - languages: string[], - targetLanguageCode?: string + welcomeCard: TSurveyWelcomeCard | TLegacySurveyWelcomeCard, + languages: string[] ): TSurveyWelcomeCard => { const clonedWelcomeCard = structuredClone(welcomeCard); - if (welcomeCard.headline) { - clonedWelcomeCard.headline = createI18nString(welcomeCard.headline, languages, targetLanguageCode); + if (typeof welcomeCard.headline !== "undefined") { + clonedWelcomeCard.headline = createI18nString(welcomeCard.headline ?? "", languages); } - if (welcomeCard.html) { - clonedWelcomeCard.html = createI18nString(welcomeCard.html, languages, targetLanguageCode); + if (typeof welcomeCard.html !== "undefined") { + clonedWelcomeCard.html = createI18nString(welcomeCard.html ?? "", languages); } - if (clonedWelcomeCard.buttonLabel) { - clonedWelcomeCard.buttonLabel = createI18nString( - clonedWelcomeCard.buttonLabel, - languages, - targetLanguageCode - ); + if (typeof welcomeCard.buttonLabel !== "undefined") { + clonedWelcomeCard.buttonLabel = createI18nString(clonedWelcomeCard.buttonLabel ?? "", languages); } - return clonedWelcomeCard; + return ZSurveyWelcomeCard.parse(clonedWelcomeCard); }; // LGEGACY // Helper function to maintain backwards compatibility for old survey objects before Multi Language export const translateThankYouCard = ( - thankYouCard: TSurveyThankYouCard, - languages: string[], - targetLanguageCode?: string + thankYouCard: TSurveyThankYouCard | TLegacySurveyThankYouCard, + languages: string[] ): TSurveyThankYouCard => { const clonedThankYouCard = structuredClone(thankYouCard); - if (thankYouCard.headline) { - clonedThankYouCard.headline = createI18nString(thankYouCard.headline, languages, targetLanguageCode); - } - if (thankYouCard.subheader) { - clonedThankYouCard.subheader = createI18nString(thankYouCard.subheader, languages, targetLanguageCode); - } - if (thankYouCard.buttonLabel) { - clonedThankYouCard.buttonLabel = createI18nString( - thankYouCard.buttonLabel, - languages, - targetLanguageCode - ); + + if (typeof thankYouCard.headline !== "undefined") { + clonedThankYouCard.headline = createI18nString(thankYouCard.headline ?? "", languages); } - return clonedThankYouCard; + if (typeof thankYouCard.subheader !== "undefined") { + clonedThankYouCard.subheader = createI18nString(thankYouCard.subheader ?? "", languages); + } + + if (typeof clonedThankYouCard.buttonLabel !== "undefined") { + clonedThankYouCard.buttonLabel = createI18nString(thankYouCard.buttonLabel ?? "", languages); + } + return ZSurveyThankYouCard.parse(clonedThankYouCard); }; // LGEGACY // Helper function to maintain backwards compatibility for old survey objects before Multi Language export const translateQuestion = ( - question: TSurveyQuestion, - languages: string[], - targetLanguageCode?: string -) => { + question: TLegacySurveyQuestion | TSurveyQuestion, + languages: string[] +): TSurveyQuestion => { // Clone the question to avoid mutating the original const clonedQuestion = structuredClone(question); - clonedQuestion.headline = createI18nString(question.headline, languages, targetLanguageCode); - if (clonedQuestion.subheader) { - clonedQuestion.subheader = createI18nString(question.subheader ?? "", languages, targetLanguageCode); + //common question properties + if (typeof question.headline !== "undefined") { + clonedQuestion.headline = createI18nString(question.headline ?? "", languages); } - if (clonedQuestion.buttonLabel) { - clonedQuestion.buttonLabel = createI18nString(question.buttonLabel ?? "", languages, targetLanguageCode); + if (typeof question.subheader !== "undefined") { + clonedQuestion.subheader = createI18nString(question.subheader ?? "", languages); } - if (clonedQuestion.backButtonLabel) { - clonedQuestion.backButtonLabel = createI18nString( - question.backButtonLabel ?? "", - languages, - targetLanguageCode - ); + if (typeof question.buttonLabel !== "undefined") { + clonedQuestion.buttonLabel = createI18nString(question.buttonLabel ?? "", languages); } - if (question.type === "multipleChoiceSingle" || question.type === "multipleChoiceMulti") { - (clonedQuestion as TSurveyMultipleChoiceMultiQuestion | TSurveyMultipleChoiceMultiQuestion).choices = - question.choices.map((choice) => ({ - ...choice, - label: createI18nString(choice.label, languages, targetLanguageCode), - })); - ( - clonedQuestion as TSurveyMultipleChoiceMultiQuestion | TSurveyMultipleChoiceMultiQuestion - ).otherOptionPlaceholder = question.otherOptionPlaceholder - ? createI18nString(question.otherOptionPlaceholder, languages, targetLanguageCode) - : undefined; + if (typeof question.backButtonLabel !== "undefined") { + clonedQuestion.backButtonLabel = createI18nString(question.backButtonLabel ?? "", languages); } - if (question.type === "openText") { - if (question.placeholder) { - (clonedQuestion as TSurveyOpenTextQuestion).placeholder = createI18nString( - question.placeholder, - languages, - targetLanguageCode - ); - } - } - if (question.type === "cta") { - if (question.dismissButtonLabel) { - (clonedQuestion as TSurveyCTAQuestion).dismissButtonLabel = createI18nString( - question.dismissButtonLabel, - languages, - targetLanguageCode - ); - } - if (question.html) { - (clonedQuestion as TSurveyCTAQuestion).html = createI18nString( - question.html, - languages, - targetLanguageCode - ); - } - } - if (question.type === "consent") { - if (question.html) { - (clonedQuestion as TSurveyConsentQuestion).html = createI18nString( - question.html, - languages, - targetLanguageCode - ); - } - if (question.label) { - (clonedQuestion as TSurveyConsentQuestion).label = createI18nString( - question.label, - languages, - targetLanguageCode - ); - } + switch (question.type) { + case "openText": + if (typeof question.placeholder !== "undefined") { + (clonedQuestion as TSurveyOpenTextQuestion).placeholder = createI18nString( + question.placeholder ?? "", + languages + ); + } + return ZSurveyOpenTextQuestion.parse(clonedQuestion); + + case "multipleChoiceSingle": + case "multipleChoiceMulti": + (clonedQuestion as TSurveyMultipleChoiceSingleQuestion | TSurveyMultipleChoiceMultiQuestion).choices = + question.choices.map((choice) => { + return translateChoice(choice, languages); + }); + if ( + typeof (clonedQuestion as TSurveyMultipleChoiceSingleQuestion | TSurveyMultipleChoiceMultiQuestion) + .otherOptionPlaceholder !== "undefined" + ) { + ( + clonedQuestion as TSurveyMultipleChoiceSingleQuestion | TSurveyMultipleChoiceMultiQuestion + ).otherOptionPlaceholder = createI18nString(question.otherOptionPlaceholder ?? "", languages); + } + if (question.type === "multipleChoiceSingle") { + return ZSurveyMultipleChoiceSingleQuestion.parse(clonedQuestion); + } else return ZSurveyMultipleChoiceMultiQuestion.parse(clonedQuestion); + + case "cta": + if (typeof question.dismissButtonLabel !== "undefined") { + (clonedQuestion as TSurveyCTAQuestion).dismissButtonLabel = createI18nString( + question.dismissButtonLabel ?? "", + languages + ); + } + if (typeof question.html !== "undefined") { + (clonedQuestion as TSurveyCTAQuestion).html = createI18nString(question.html ?? "", languages); + } + return ZSurveyCTAQuestion.parse(clonedQuestion); + + case "consent": + if (typeof question.html !== "undefined") { + (clonedQuestion as TSurveyConsentQuestion).html = createI18nString(question.html ?? "", languages); + } + + if (typeof question.label !== "undefined") { + (clonedQuestion as TSurveyConsentQuestion).label = createI18nString(question.label ?? "", languages); + } + return ZSurveyConsentQuestion.parse(clonedQuestion); + + case "nps": + if (typeof question.lowerLabel !== "undefined") { + (clonedQuestion as TSurveyNPSQuestion).lowerLabel = createI18nString( + question.lowerLabel ?? "", + languages + ); + } + if (typeof question.upperLabel !== "undefined") { + (clonedQuestion as TSurveyNPSQuestion).upperLabel = createI18nString( + question.upperLabel ?? "", + languages + ); + } + return ZSurveyNPSQuestion.parse(clonedQuestion); + + case "rating": + if (typeof question.lowerLabel !== "undefined") { + (clonedQuestion as TSurveyRatingQuestion).lowerLabel = createI18nString( + question.lowerLabel ?? "", + languages + ); + } + + if (typeof question.upperLabel !== "undefined") { + (clonedQuestion as TSurveyRatingQuestion).upperLabel = createI18nString( + question.upperLabel ?? "", + languages + ); + } + return ZSurveyRatingQuestion.parse(clonedQuestion); + + case "fileUpload": + return ZSurveyFileUploadQuestion.parse(clonedQuestion); + + case "pictureSelection": + return ZSurveyPictureSelectionQuestion.parse(clonedQuestion); + + case "cal": + return ZSurveyCalQuestion.parse(clonedQuestion); + + default: + return ZSurveyQuestion.parse(clonedQuestion); } - if (question.type === "nps") { - (clonedQuestion as TSurveyNPSQuestion).lowerLabel = createI18nString( - question.lowerLabel ?? "", - languages, - targetLanguageCode - ); - (clonedQuestion as TSurveyNPSQuestion).upperLabel = createI18nString( - question.upperLabel ?? "", - languages, - targetLanguageCode - ); - } - if (question.type === "rating") { - (clonedQuestion as TSurveyRatingQuestion).lowerLabel = createI18nString( - question.lowerLabel ?? "", - languages, - targetLanguageCode - ); - (clonedQuestion as TSurveyRatingQuestion).upperLabel = createI18nString( - question.upperLabel ?? "", - languages, - targetLanguageCode - ); - } - return clonedQuestion; +}; + +export const extractLanguageIds = (languages: TLanguage[]): string[] => { + return languages.map((language) => language.code); }; // LGEGACY // Helper function to maintain backwards compatibility for old survey objects before Multi Language export const translateSurvey = ( - survey: TSurvey, - surveyLanguages: TSurveyLanguage[], - targetLanguageCode?: string -): TSurvey => { - const languages = extractLanguageCodes(surveyLanguages); - + survey: Pick, + languageCodes: string[] +): Pick => { const translatedQuestions = survey.questions.map((question) => { - return translateQuestion(question, languages, targetLanguageCode); + return translateQuestion(question, languageCodes); }); - const translatedWelcomeCard = - survey.welcomeCard && translateWelcomeCard(survey.welcomeCard, languages, targetLanguageCode); - const translatedThankYouCard = - survey.thankYouCard && translateThankYouCard(survey.thankYouCard, languages, targetLanguageCode); + const translatedWelcomeCard = translateWelcomeCard(survey.welcomeCard, languageCodes); + const translatedThankYouCard = translateThankYouCard(survey.thankYouCard, languageCodes); const translatedSurvey = structuredClone(survey); return { ...translatedSurvey, diff --git a/packages/lib/survey/service.ts b/packages/lib/survey/service.ts index 024d52baee..c76014af77 100644 --- a/packages/lib/survey/service.ts +++ b/packages/lib/survey/service.ts @@ -19,7 +19,6 @@ import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants"; import { displayCache } from "../display/cache"; import { getDisplaysByPersonId } from "../display/service"; import { reverseTranslateSurvey } from "../i18n/reverseTranslation"; -import { translateSurvey } from "../i18n/utils"; import { personCache } from "../person/cache"; import { getPerson } from "../person/service"; import { productCache } from "../product/cache"; @@ -366,30 +365,6 @@ export const transformToLegacySurvey = async ( return formatDateFields(transformedSurvey, ZLegacySurvey); }; -export const transformSurveyToSpecificLanguage = async ( - survey: TSurvey, - targetLanguageCode?: string -): Promise => { - // if target language code is not available, it will be transformed to default language - const transformedSurvey = await unstable_cache( - async () => { - if (!survey.languages || survey.languages.length === 0) { - //survey do not have any translations - return survey; - } - return translateSurvey(survey, [], targetLanguageCode); - }, - [`transformSurveyToSpecificLanguage-${survey}-${targetLanguageCode}`], - { - tags: [surveyCache.tag.byId(survey.id)], - revalidate: SERVICES_REVALIDATION_INTERVAL, - } - )(); - // since the unstable_cache function does not support deserialization of dates, we need to manually deserialize them - // https://github.com/vercel/next.js/issues/51613 - return formatDateFields(transformedSurvey, ZSurvey); -}; - export const getSurveyCount = async (environmentId: string): Promise => { const count = await unstable_cache( async () => { diff --git a/packages/types/LegacySurvey.ts b/packages/types/LegacySurvey.ts index 5a3b8e3c99..189cf8e63f 100644 --- a/packages/types/LegacySurvey.ts +++ b/packages/types/LegacySurvey.ts @@ -40,7 +40,6 @@ export const ZLegacySurveyConsentQuestion = ZLegacySurveyQuestionBase.extend({ type: z.literal(TSurveyQuestionType.Consent), html: z.string().optional(), label: z.string(), - dismissButtonLabel: z.string().optional(), placeholder: z.string().optional(), logic: z.array(ZSurveyConsentLogic).optional(), }); diff --git a/packages/types/surveys.ts b/packages/types/surveys.ts index bd1f6326d0..171cf5541f 100644 --- a/packages/types/surveys.ts +++ b/packages/types/surveys.ts @@ -272,7 +272,6 @@ export const ZSurveyConsentQuestion = ZSurveyQuestionBase.extend({ type: z.literal(TSurveyQuestionType.Consent), html: ZI18nString.optional(), label: ZI18nString, - dismissButtonLabel: z.string().optional(), placeholder: z.string().optional(), logic: z.array(ZSurveyConsentLogic).optional(), });