From d27da8927e6ca95d53db8e2ac00ee967ef8c253c Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Tue, 25 Jun 2024 20:19:34 +0530 Subject: [PATCH] chore: Chinese to iso languages (#2794) Co-authored-by: Piyush Gupta --- .../data-migration.ts | 119 ++++++++ .../lib/utils.ts | 259 ++++++++++++++++++ packages/database/package.json | 3 +- packages/lib/i18n/utils.ts | 8 +- 4 files changed, 386 insertions(+), 3 deletions(-) create mode 100644 packages/database/data-migrations/20240625101352_update_zh_to_zh-Hans/data-migration.ts create mode 100644 packages/database/data-migrations/20240625101352_update_zh_to_zh-Hans/lib/utils.ts diff --git a/packages/database/data-migrations/20240625101352_update_zh_to_zh-Hans/data-migration.ts b/packages/database/data-migrations/20240625101352_update_zh_to_zh-Hans/data-migration.ts new file mode 100644 index 0000000000..1868c521fd --- /dev/null +++ b/packages/database/data-migrations/20240625101352_update_zh_to_zh-Hans/data-migration.ts @@ -0,0 +1,119 @@ +// migration script for converting zh code for chinese language to zh-Hans +import { PrismaClient } from "@prisma/client"; +import { + TSurveyLanguage, + TSurveyQuestion, + TSurveyThankYouCard, + TSurveyWelcomeCard, +} from "@formbricks/types/surveys"; +import { + updateLanguageCodeForQuestion, + updateLanguageCodeForThankYouCard, + updateLanguageCodeForWelcomeCard, +} from "./lib/utils"; + +const prisma = new PrismaClient(); + +const main = async () => { + await prisma.$transaction( + async (tx) => { + const startTime = Date.now(); + console.log("Starting data migration..."); + + // Fetch all surveys + const surveys: { + id: string; + questions: TSurveyQuestion[]; + welcomeCard: TSurveyWelcomeCard; + thankYouCard: TSurveyThankYouCard; + languages: TSurveyLanguage[]; + }[] = await tx.survey.findMany({ + select: { + id: true, + questions: true, + welcomeCard: true, + thankYouCard: true, + languages: { + select: { + default: true, + enabled: true, + language: { + select: { + id: true, + code: true, + alias: true, + createdAt: true, + updatedAt: true, + }, + }, + }, + }, + }, + }); + + if (surveys.length === 0) { + // stop the migration if there are no surveys + console.log("No Surveys found"); + return; + } + + console.log(`Total surveys found:${surveys.length}`); + let transformedSurveyCount = 0; + + const surveysWithChineseTranslations = surveys.filter((survey) => + survey.languages.some((surveyLanguage) => surveyLanguage.language.code === "zh") + ); + + const updatePromises = surveysWithChineseTranslations.map((survey) => { + transformedSurveyCount++; + const updatedSurvey = structuredClone(survey); + + // Update cards and questions + updatedSurvey.welcomeCard = updateLanguageCodeForWelcomeCard(survey.welcomeCard, "zh", "zh-Hans"); + updatedSurvey.thankYouCard = updateLanguageCodeForThankYouCard(survey.thankYouCard, "zh", "zh-Hans"); + updatedSurvey.questions = survey.questions.map((question) => + updateLanguageCodeForQuestion(question, "zh", "zh-Hans") + ); + + // Return the update promise + return tx.survey.update({ + where: { id: survey.id }, + data: { + welcomeCard: updatedSurvey.welcomeCard, + thankYouCard: updatedSurvey.thankYouCard, + questions: updatedSurvey.questions, + }, + }); + }); + + await Promise.all(updatePromises); + + console.log(transformedSurveyCount, " surveys transformed"); + + console.log("updating languages"); + await tx.language.updateMany({ + where: { + code: "zh", + }, + data: { + code: "zh-Hans", + }, + }); + console.log("survey language updated"); + const endTime = Date.now(); + console.log(`Data migration completed. Total time: ${(endTime - startTime) / 1000}s`); + }, + { + timeout: 180000, // 3 minutes + } + ); +}; + +main() + .catch((e: Error) => { + console.error("Error during migration: ", e.message); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/packages/database/data-migrations/20240625101352_update_zh_to_zh-Hans/lib/utils.ts b/packages/database/data-migrations/20240625101352_update_zh_to_zh-Hans/lib/utils.ts new file mode 100644 index 0000000000..8661a8a20d --- /dev/null +++ b/packages/database/data-migrations/20240625101352_update_zh_to_zh-Hans/lib/utils.ts @@ -0,0 +1,259 @@ +import { + TI18nString, + TSurveyCTAQuestion, + TSurveyChoice, + TSurveyConsentQuestion, + TSurveyMatrixQuestion, + TSurveyMultipleChoiceQuestion, + TSurveyNPSQuestion, + TSurveyOpenTextQuestion, + TSurveyQuestion, + TSurveyRatingQuestion, + TSurveyThankYouCard, + TSurveyWelcomeCard, + ZSurveyAddressQuestion, + ZSurveyCTAQuestion, + ZSurveyCalQuestion, + ZSurveyConsentQuestion, + ZSurveyFileUploadQuestion, + ZSurveyMatrixQuestion, + ZSurveyMultipleChoiceQuestion, + ZSurveyNPSQuestion, + ZSurveyOpenTextQuestion, + ZSurveyPictureSelectionQuestion, + ZSurveyQuestion, + ZSurveyRatingQuestion, + ZSurveyThankYouCard, + ZSurveyWelcomeCard, +} from "@formbricks/types/surveys"; + +export const updateLanguageCode = (i18nString: TI18nString, oldCode: string, newCode: string) => { + const updatedI18nString = structuredClone(i18nString); + if (Object.keys(i18nString).includes(oldCode)) { + const text = i18nString[oldCode]; + delete updatedI18nString[oldCode]; + updatedI18nString[newCode] = text; + } + return updatedI18nString; +}; + +export const updateChoiceLanguage = ( + choice: TSurveyChoice, + oldCode: string, + newCode: string +): TSurveyChoice => { + if (typeof choice.label !== "undefined" && Object.keys(choice.label).includes(oldCode)) { + return { + ...choice, + label: updateLanguageCode(choice.label, oldCode, newCode), + }; + } else { + return { + ...choice, + label: choice.label, + }; + } +}; + +export const updateLanguageCodeForWelcomeCard = ( + welcomeCard: TSurveyWelcomeCard, + oldCode: string, + newCode: string +) => { + const clonedWelcomeCard = structuredClone(welcomeCard); + if (typeof welcomeCard.headline !== "undefined" && Object.keys(welcomeCard.headline).includes(oldCode)) { + clonedWelcomeCard.headline = updateLanguageCode(welcomeCard.headline, oldCode, newCode); + } + + if (typeof welcomeCard.html !== "undefined" && Object.keys(welcomeCard.html).includes(oldCode)) { + clonedWelcomeCard.html = updateLanguageCode(welcomeCard.html, oldCode, newCode); + } + + if ( + typeof welcomeCard.buttonLabel !== "undefined" && + Object.keys(welcomeCard.buttonLabel).includes(oldCode) + ) { + clonedWelcomeCard.buttonLabel = updateLanguageCode(welcomeCard.buttonLabel, oldCode, newCode); + } + + return ZSurveyWelcomeCard.parse(clonedWelcomeCard); +}; + +export const updateLanguageCodeForThankYouCard = ( + thankYouCard: TSurveyThankYouCard, + oldCode: string, + newCode: string +) => { + const clonedThankYouCard = structuredClone(thankYouCard); + if (typeof thankYouCard.headline !== "undefined" && Object.keys(thankYouCard.headline).includes(oldCode)) { + clonedThankYouCard.headline = updateLanguageCode(thankYouCard.headline, oldCode, newCode); + } + + if ( + typeof thankYouCard.subheader !== "undefined" && + Object.keys(thankYouCard.subheader).includes(oldCode) + ) { + clonedThankYouCard.subheader = updateLanguageCode(thankYouCard.subheader, oldCode, newCode); + } + + if ( + typeof thankYouCard.buttonLabel !== "undefined" && + Object.keys(thankYouCard.buttonLabel).includes(oldCode) + ) { + clonedThankYouCard.buttonLabel = updateLanguageCode(thankYouCard.buttonLabel, oldCode, newCode); + } + return ZSurveyThankYouCard.parse(clonedThankYouCard); +}; + +export const updateLanguageCodeForQuestion = ( + question: TSurveyQuestion, + oldCode: string, + newCode: string +) => { + const clonedQuestion = structuredClone(question); + //common question properties + if (typeof question.headline !== "undefined" && Object.keys(question.headline).includes(oldCode)) { + clonedQuestion.headline = updateLanguageCode(question.headline, oldCode, newCode); + } + + if (typeof question.subheader !== "undefined" && Object.keys(question.subheader).includes(oldCode)) { + clonedQuestion.subheader = updateLanguageCode(question.subheader, oldCode, newCode); + } + + if (typeof question.buttonLabel !== "undefined" && Object.keys(question.buttonLabel).includes(oldCode)) { + clonedQuestion.buttonLabel = updateLanguageCode(question.buttonLabel, oldCode, newCode); + } + + if ( + typeof question.backButtonLabel !== "undefined" && + Object.keys(question.backButtonLabel).includes(oldCode) + ) { + clonedQuestion.backButtonLabel = updateLanguageCode(question.backButtonLabel, oldCode, newCode); + } + + switch (question.type) { + case "openText": + if ( + typeof question.placeholder !== "undefined" && + Object.keys(question.placeholder).includes(oldCode) + ) { + (clonedQuestion as TSurveyOpenTextQuestion).placeholder = updateLanguageCode( + question.placeholder, + oldCode, + newCode + ); + } + return ZSurveyOpenTextQuestion.parse(clonedQuestion); + + case "multipleChoiceSingle": + case "multipleChoiceMulti": + (clonedQuestion as TSurveyMultipleChoiceQuestion).choices = question.choices.map((choice) => { + return updateChoiceLanguage(choice, oldCode, newCode); + }); + if ( + typeof question.otherOptionPlaceholder !== "undefined" && + Object.keys(question.otherOptionPlaceholder).includes(oldCode) + ) { + (clonedQuestion as TSurveyMultipleChoiceQuestion).otherOptionPlaceholder = updateLanguageCode( + question.otherOptionPlaceholder, + oldCode, + newCode + ); + } + return ZSurveyMultipleChoiceQuestion.parse(clonedQuestion); + + case "cta": + if ( + typeof question.dismissButtonLabel !== "undefined" && + Object.keys(question.dismissButtonLabel).includes(oldCode) + ) { + (clonedQuestion as TSurveyCTAQuestion).dismissButtonLabel = updateLanguageCode( + question.dismissButtonLabel, + oldCode, + newCode + ); + } + if (typeof question.html !== "undefined" && Object.keys(question.html).includes(oldCode)) { + (clonedQuestion as TSurveyCTAQuestion).html = updateLanguageCode(question.html, oldCode, newCode); + } + return ZSurveyCTAQuestion.parse(clonedQuestion); + + case "consent": + if (typeof question.html !== "undefined" && Object.keys(question.html).includes(oldCode)) { + (clonedQuestion as TSurveyConsentQuestion).html = updateLanguageCode(question.html, oldCode, newCode); + } + + if (typeof question.label !== "undefined" && Object.keys(question.label).includes(oldCode)) { + (clonedQuestion as TSurveyConsentQuestion).label = updateLanguageCode( + question.label, + oldCode, + newCode + ); + } + return ZSurveyConsentQuestion.parse(clonedQuestion); + + case "nps": + if (typeof question.lowerLabel !== "undefined" && Object.keys(question.lowerLabel).includes(oldCode)) { + (clonedQuestion as TSurveyNPSQuestion).lowerLabel = updateLanguageCode( + question.lowerLabel, + oldCode, + newCode + ); + } + if (typeof question.upperLabel !== "undefined" && Object.keys(question.upperLabel).includes(oldCode)) { + (clonedQuestion as TSurveyNPSQuestion).upperLabel = updateLanguageCode( + question.upperLabel, + oldCode, + newCode + ); + } + return ZSurveyNPSQuestion.parse(clonedQuestion); + + case "rating": + if (typeof question.lowerLabel !== "undefined" && Object.keys(question.lowerLabel).includes(oldCode)) { + (clonedQuestion as TSurveyRatingQuestion).lowerLabel = updateLanguageCode( + question.lowerLabel, + oldCode, + newCode + ); + } + + if (typeof question.upperLabel !== "undefined" && Object.keys(question.upperLabel).includes(oldCode)) { + (clonedQuestion as TSurveyRatingQuestion).upperLabel = updateLanguageCode( + question.upperLabel, + oldCode, + newCode + ); + } + return ZSurveyRatingQuestion.parse(clonedQuestion); + + case "matrix": + (clonedQuestion as TSurveyMatrixQuestion).rows = question.rows.map((row) => { + if (typeof row !== "undefined" && Object.keys(row).includes(oldCode)) { + return updateLanguageCode(row, oldCode, newCode); + } else return row; + }); + + (clonedQuestion as TSurveyMatrixQuestion).columns = question.columns.map((column) => { + if (typeof column !== "undefined" && Object.keys(column).includes(oldCode)) { + return updateLanguageCode(column, oldCode, newCode); + } else return column; + }); + return ZSurveyMatrixQuestion.parse(clonedQuestion); + + case "fileUpload": + return ZSurveyFileUploadQuestion.parse(clonedQuestion); + + case "pictureSelection": + return ZSurveyPictureSelectionQuestion.parse(clonedQuestion); + + case "cal": + return ZSurveyCalQuestion.parse(clonedQuestion); + + case "address": + return ZSurveyAddressQuestion.parse(clonedQuestion); + + default: + return ZSurveyQuestion.parse(clonedQuestion); + } +}; diff --git a/packages/database/package.json b/packages/database/package.json index b2411143eb..df9ff4cd6d 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -38,7 +38,8 @@ "data-migration:v2.1": "pnpm data-migration:extended-noCodeActions", "data-migration:adds_app_and_website_status_indicator": "ts-node ./data-migrations/20240610055828_adds_app_and_website_status_indicators/data-migration.ts", "data-migration:product-config": "ts-node ./data-migrations/20240612115151_adds_product_config/data-migration.ts", - "data-migration:v2.2": "pnpm data-migration:adds_app_and_website_status_indicator && pnpm data-migration:product-config && pnpm data-migration:pricing-v2" + "data-migration:v2.2": "pnpm data-migration:adds_app_and_website_status_indicator && pnpm data-migration:product-config && pnpm data-migration:pricing-v2", + "data-migration:zh-to-zh-Hans": "ts-node ./data-migrations/20240625101352_update_zh_to_zh-Hans/data-migration.ts" }, "dependencies": { "@prisma/client": "^5.15.1", diff --git a/packages/lib/i18n/utils.ts b/packages/lib/i18n/utils.ts index ef5116a9f1..9af04a4fe8 100644 --- a/packages/lib/i18n/utils.ts +++ b/packages/lib/i18n/utils.ts @@ -1045,8 +1045,12 @@ export const iso639Languages = [ english: "Zhuang; Chuang", }, { - alpha2: "zh", - english: "Chinese", + alpha2: "zh-Hans", + english: "Chinese (Simplified)", + }, + { + alpha2: "zh-Hant", + english: "Chinese (Traditional)", }, { alpha2: "zu",