diff --git a/.vscode/settings.json b/.vscode/settings.json index 5ab2664b7d..45ec7dca4d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,10 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, - "eslint.validate": ["javascript", "typescript"], + "eslint.validate": [ + "javascript", + "typescript" + ], "editor.formatOnSave": true, "[javascript]": { "editor.formatOnSave": false @@ -16,10 +19,11 @@ }, "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "typescript.enablePromptUseWorkspaceTsdk": true, "typescript.tsdk": "node_modules/typescript/lib", "markdown.extension.toc.omittedFromToc": { "README.md": [ "## Table of Contents" - ] + ] } -} +} \ No newline at end of file diff --git a/__tests__/unit/backend/helpers/generate-form-data.ts b/__tests__/unit/backend/helpers/generate-form-data.ts index fb67e18b72..995d8b33e7 100644 --- a/__tests__/unit/backend/helpers/generate-form-data.ts +++ b/__tests__/unit/backend/helpers/generate-form-data.ts @@ -51,6 +51,8 @@ export const generateDefaultField = ( fieldType, required: true, disabled: false, + descriptionTranslations: [], + titleTranslations: [], } switch (fieldType) { case BasicField.Table: @@ -77,6 +79,7 @@ export const generateDefaultField = ( return { ...defaultParams, fieldOptions: ['Option 1', 'Option 2'], + fieldOptionsTranslations: [], getQuestion: () => defaultParams.title, ValidationOptions: { customMin: null, @@ -116,6 +119,7 @@ export const generateDefaultField = ( return { ...defaultParams, fieldOptions: ['Option 1', 'Option 2'], + fieldOptionsTranslations: [], getQuestion: () => defaultParams.title, ...customParams, } as IDropdownFieldSchema @@ -385,6 +389,7 @@ export const generateTableDropdownColumn = ( required: true, _id: new ObjectId().toHexString(), fieldOptions: ['a', 'b', 'c'], + fieldOptionsTranslations: [], ...customParams, } }, diff --git a/frontend/public/index.html b/frontend/public/index.html index dfe9ac656c..ae5ace661b 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -63,7 +63,7 @@ name="twitter:image" content="%PUBLIC_URL%/static/images/__OG_IMAGE__" /> - +
diff --git a/frontend/src/app/AppRouter.tsx b/frontend/src/app/AppRouter.tsx index 1859d81b86..c04cfe08aa 100644 --- a/frontend/src/app/AppRouter.tsx +++ b/frontend/src/app/AppRouter.tsx @@ -7,6 +7,7 @@ import { ADMINFORM_PREVIEW_ROUTE, ADMINFORM_RESULTS_SUBROUTE, ADMINFORM_ROUTE, + ADMINFORM_SETTINGS_MULTI_LANGUAGE_TRANSLATION_SUBROUTE, ADMINFORM_SETTINGS_SUBROUTE, ADMINFORM_USETEMPLATE_ROUTE, BILLING_ROUTE, @@ -165,7 +166,14 @@ export const AppRouter = (): JSX.Element => { > } /> }> - } /> + }> + }> + } + /> + + , +): JSX.Element => ( + + + + +) diff --git a/frontend/src/components/FormEndPage/EndPageBlock.tsx b/frontend/src/components/FormEndPage/EndPageBlock.tsx index d141ce524b..182cd6305c 100644 --- a/frontend/src/components/FormEndPage/EndPageBlock.tsx +++ b/frontend/src/components/FormEndPage/EndPageBlock.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef } from 'react' import { Box, Text, VisuallyHidden } from '@chakra-ui/react' import { format } from 'date-fns' -import { FormColorTheme, FormDto } from '~shared/types/form' +import { FormColorTheme, FormDto, Language } from '~shared/types/form' import { useMdComponents } from '~hooks/useMdComponents' import Button from '~components/Button' @@ -17,6 +17,7 @@ export interface EndPageBlockProps { colorTheme?: FormColorTheme focusOnMount?: boolean isButtonHidden?: boolean + selectedLanguage: Language } export const EndPageBlock = ({ @@ -26,6 +27,7 @@ export const EndPageBlock = ({ colorTheme = FormColorTheme.Blue, focusOnMount, isButtonHidden, + selectedLanguage, }: EndPageBlockProps): JSX.Element => { const focusRef = useRef(null) useEffect(() => { @@ -43,6 +45,40 @@ export const EndPageBlock = ({ }, }) + const title = useMemo(() => { + let content = endPage.title + + if (selectedLanguage !== Language.ENGLISH) { + const translations = endPage.titleTranslations ?? [] + const titleTranslationIdx = translations.findIndex( + (translation) => translation.language === selectedLanguage, + ) + + if (titleTranslationIdx !== -1) { + content = translations[titleTranslationIdx].translation + } + } + + return content + }, [endPage.title, endPage.titleTranslations, selectedLanguage]) + + const paragraph = useMemo(() => { + let content = endPage?.paragraph + + if (selectedLanguage !== Language.ENGLISH) { + const translations = endPage.paragraphTranslations ?? [] + const paragraphTranslationIdx = translations.findIndex( + (translation) => translation.language === selectedLanguage, + ) + + if (paragraphTranslationIdx !== -1) { + content = translations[paragraphTranslationIdx].translation + } + } + + return content + }, [endPage?.paragraph, endPage.paragraphTranslations, selectedLanguage]) + const submissionTimestamp = useMemo( () => format(new Date(submissionData.timestamp), 'dd MMM yyyy, HH:mm:ss z'), [submissionData.timestamp], @@ -62,13 +98,11 @@ export const EndPageBlock = ({ {submittedAriaText} - {endPage.title} + {title} - {endPage.paragraph ? ( + {paragraph ? ( - - {endPage.paragraph} - + {paragraph} ) : null} diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index a7501e7eb8..1f834db34a 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -27,6 +27,10 @@ export const ADMINFORM_USETEMPLATE_ROUTE = 'use-template' export const ADMINFORM_SETTINGS_SINGPASS_SUBROUTE = `${ADMINFORM_SETTINGS_SUBROUTE}/singpass` export const ADMINFORM_SETTINGS_PAYMENTS_SUBROUTE = `${ADMINFORM_SETTINGS_SUBROUTE}/payments` +// sub route for multi-language translation +export const ADMINFORM_SETTINGS_MULTI_LANGUAGE_TRANSLATION_SUBROUTE = + 'translation' + /** * Regex for active path matching on adminform builder routes/subroutes. * @example Breakdown of regex: diff --git a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/StartPageView.tsx b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/StartPageView.tsx index 87c7dc52e3..b461890557 100644 --- a/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/StartPageView.tsx +++ b/frontend/src/features/admin-form/create/builder-and-design/BuilderAndDesignContent/StartPageView.tsx @@ -194,6 +194,7 @@ export const StartPageView = () => { /> { + diff --git a/frontend/src/features/admin-form/preview/PreviewFormProvider.tsx b/frontend/src/features/admin-form/preview/PreviewFormProvider.tsx index 0244c66437..985b9eef66 100644 --- a/frontend/src/features/admin-form/preview/PreviewFormProvider.tsx +++ b/frontend/src/features/admin-form/preview/PreviewFormProvider.tsx @@ -6,7 +6,7 @@ import { datadogLogs } from '@datadog/browser-logs' import get from 'lodash/get' import simplur from 'simplur' -import { FormAuthType, FormResponseMode } from '~shared/types/form' +import { FormAuthType, FormResponseMode, Language } from '~shared/types/form' import { usePreviewForm } from '~/features/admin-form/common/queries' import { FormNotFound } from '~/features/public-form/components/FormNotFound' @@ -45,6 +45,10 @@ export const PreviewFormProvider = ({ /* enabled= */ !submissionData, ) + const [publicFormLanguage, setPublicFormLanguage] = useState( + Language.ENGLISH, + ) + const { data: { useFetchForSubmissions } = {} } = useEnv() const { isNotFormId, toast, vfnToastIdRef, expiryInMs, ...commonFormValues } = @@ -337,6 +341,8 @@ export const PreviewFormProvider = ({ handleLogout: undefined, isPaymentEnabled, isPreview: true, + setPublicFormLanguage, + publicFormLanguage, ...commonFormValues, ...data, ...rest, diff --git a/frontend/src/features/admin-form/settings/SettingsMultiLangPage.tsx b/frontend/src/features/admin-form/settings/SettingsMultiLangPage.tsx new file mode 100644 index 0000000000..60dada8725 --- /dev/null +++ b/frontend/src/features/admin-form/settings/SettingsMultiLangPage.tsx @@ -0,0 +1,9 @@ +import { MultiLanguageSection } from './components/MultiLanguageSection/MultiLangugageSection' + +export const SettingsMultiLangPage = (): JSX.Element => { + return ( + <> + + + ) +} diff --git a/frontend/src/features/admin-form/settings/SettingsPage.tsx b/frontend/src/features/admin-form/settings/SettingsPage.tsx index 9dd5f9b144..25cb6a9e20 100644 --- a/frontend/src/features/admin-form/settings/SettingsPage.tsx +++ b/frontend/src/features/admin-form/settings/SettingsPage.tsx @@ -1,6 +1,6 @@ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { BiCodeBlock, BiCog, BiDollar, BiKey, BiMessage } from 'react-icons/bi' -import { useNavigate, useParams } from 'react-router-dom' +import { useLocation, useNavigate, useParams } from 'react-router-dom' import { Box, Flex, @@ -11,11 +11,15 @@ import { Tabs, } from '@chakra-ui/react' +import { LanguageTranslation } from '~assets/icons/LanguageTranslation' import { ADMINFORM_RESULTS_SUBROUTE, ADMINFORM_ROUTE } from '~constants/routes' import { useDraggable } from '~hooks/useDraggable' import { useAdminFormCollaborators } from '../common/queries' +import { MultiLanguageSection } from './components/MultiLanguageSection/MultiLangugageSection' +import { TranslationListSection } from './components/MultiLanguageSection/TranslationListSection' +import { TranslationSection } from './components/MultiLanguageSection/TranslationSection' import { SettingsTab } from './components/SettingsTab' import { SettingsAuthPage } from './SettingsAuthPage' import { SettingsGeneralPage } from './SettingsGeneralPage' @@ -29,10 +33,19 @@ const settingsTabsOrder = [ 'twilio', 'webhooks', 'payments', + 'multi-language', ] export const SettingsPage = (): JSX.Element => { - const { formId, settingsTab } = useParams() + const { formId, settingsTab, language } = useParams() + const { state } = useLocation() + + const translationParams = state as { + isTranslation: boolean + formFieldNum: number + isStartPage: boolean + isEndPage: boolean + } if (!formId) throw new Error('No formId provided') @@ -52,6 +65,23 @@ export const SettingsPage = (): JSX.Element => { settingsTabsOrder.indexOf(settingsTab ?? ''), ) + const startPageTranslations = useMemo(() => { + return translationParams?.isStartPage + }, [translationParams?.isStartPage]) + + const endPageTranslations = useMemo(() => { + return translationParams?.isEndPage + }, [translationParams?.isEndPage]) + + const currentIsTranslation = useMemo(() => { + return translationParams?.isTranslation + }, [translationParams?.isTranslation]) + + const formFieldNumToBeTranslated = useMemo(() => { + return (state as { isTranslation?: boolean; formFieldNum: number }) + ?.formFieldNum + }, [state]) + const handleTabChange = (index: number) => { setTabIndex(index) navigate( @@ -103,6 +133,7 @@ export const SettingsPage = (): JSX.Element => { + { + + {!language && !currentIsTranslation && } + {language && !currentIsTranslation && ( + + )} + {language && currentIsTranslation && ( + + )} + diff --git a/frontend/src/features/admin-form/settings/SettingsService.ts b/frontend/src/features/admin-form/settings/SettingsService.ts index bb1691c9e8..2d60edfbf2 100644 --- a/frontend/src/features/admin-form/settings/SettingsService.ts +++ b/frontend/src/features/admin-form/settings/SettingsService.ts @@ -49,6 +49,23 @@ export const updateFormLimit: UpdateFormFn<'submissionLimit'> = async ( return updateFormSettings(formId, { submissionLimit: newLimit }) } +export const updateFormHasMultiLang: UpdateFormFn<'hasMultiLang'> = async ( + formId, + newHasMultiLang, +) => { + return updateFormSettings(formId, { + hasMultiLang: newHasMultiLang, + }) +} + +export const updateFormSupportedLanguages: UpdateFormFn< + 'supportedLanguages' +> = async (formId, newSupportedLanguages) => { + return updateFormSettings(formId, { + supportedLanguages: newSupportedLanguages, + }) +} + export const updateFormCaptcha: UpdateFormFn<'hasCaptcha'> = async ( formId, newHasCaptcha, diff --git a/frontend/src/features/admin-form/settings/components/MultiLanguageSection/FormMultiLangToggle.tsx b/frontend/src/features/admin-form/settings/components/MultiLanguageSection/FormMultiLangToggle.tsx new file mode 100644 index 0000000000..36a59f4502 --- /dev/null +++ b/frontend/src/features/admin-form/settings/components/MultiLanguageSection/FormMultiLangToggle.tsx @@ -0,0 +1,215 @@ +import { useCallback, useMemo, useState } from 'react' +import { BiEditAlt } from 'react-icons/bi' +import { GoEye, GoEyeClosed } from 'react-icons/go' +import { useNavigate, useParams } from 'react-router-dom' +import { + Box, + Divider, + Flex, + HStack, + IconButton, + Skeleton, + Text, +} from '@chakra-ui/react' +import _ from 'lodash' + +import { Language } from '~shared/types' + +import { ADMINFORM_ROUTE } from '~constants/routes' +import Badge from '~components/Badge' +import Toggle from '~components/Toggle' + +import { useMutateFormSettings } from '../../mutations' +import { useAdminFormSettings } from '../../queries' + +interface LanguageTranslationRowProps { + language: Language + isDefaultLanguage: boolean + isLast?: boolean +} + +interface LanguageTranslationSectionProps { + defaultLanguage: Language +} + +const LanguageTranslationRow = ({ + language, + isDefaultLanguage, + isLast, +}: LanguageTranslationRowProps): JSX.Element => { + const { formId } = useParams() + const { data: settings } = useAdminFormSettings() + const navigate = useNavigate() + + const supportedLanguages = settings?.supportedLanguages ?? null + + const [isLanguageSupported, setIsLanguageSupported] = useState( + supportedLanguages?.indexOf(language) !== -1, + ) + + const { mutateFormSupportedLanguages } = useMutateFormSettings() + + const handleToggleSupportedLanague = useCallback( + (language: Language) => { + if (supportedLanguages == null) return + + // remove support for this language if it exists in supportedLanguages + const existingSupportedLanguageIdx = supportedLanguages.indexOf(language) + if (existingSupportedLanguageIdx !== -1) { + supportedLanguages.splice(existingSupportedLanguageIdx, 1) + setIsLanguageSupported(false) + return mutateFormSupportedLanguages.mutate({ + nextSupportedLanguages: supportedLanguages, + selectedLanguage: language, + }) + } + // if selected language is not in supportedLanguages then add this language + // to supportedLanguages + else { + const updatedSupportedLanguages = supportedLanguages.concat(language) + setIsLanguageSupported(true) + return mutateFormSupportedLanguages.mutate({ + nextSupportedLanguages: updatedSupportedLanguages, + selectedLanguage: language, + }) + } + }, + [mutateFormSupportedLanguages, supportedLanguages], + ) + + const handleLanguageTranslationEditClick = useCallback( + (language: Language) => { + const lowerCaseLanguage = language.toLowerCase() + navigate( + `${ADMINFORM_ROUTE}/${formId}/settings/multi-language/${lowerCaseLanguage}`, + ) + }, + [formId, navigate], + ) + + return ( + <> + + + {language} + {isDefaultLanguage && ( + + Default + + )} + + {!isDefaultLanguage && ( + + + ) : ( + + ) + } + colorScheme="secondary" + aria-label={`Select ${language} as the form's default language`} + onClick={() => handleToggleSupportedLanague(language)} + /> + } + colorScheme="secondary" + aria-label={`Add ${language} translations`} + onClick={() => handleLanguageTranslationEditClick(language)} + /> + + )} + + {!isLast && } + + ) +} + +const LanguageTranslationSection = ({ + defaultLanguage, +}: LanguageTranslationSectionProps): JSX.Element => { + let languages = Object.values(Language) + + const idxToRemove = languages.indexOf(defaultLanguage) + languages.splice(idxToRemove, 1) + + languages = [defaultLanguage, ...languages] + + return ( + + {languages.map((language, id, arr) => { + return ( + <> + + + ) + })} + + ) +} + +export const FormMultiLangToggle = (): JSX.Element => { + const { data: settings, isLoading: isLoadingSettings } = + useAdminFormSettings() + + const { mutateFormSupportedLanguages, mutateFormHasMultiLang } = + useMutateFormSettings() + + const hasMultiLang = useMemo( + () => settings && settings?.hasMultiLang, + [settings], + ) + + const handleToggleMultiLang = useCallback(() => { + if (!settings || isLoadingSettings || mutateFormHasMultiLang.isLoading) + return + + // get next hasMultiLang value + const nextHasMultiLang = !settings.hasMultiLang + + const currentSupportedLanguages = settings?.supportedLanguages + + // restore back previously supported languages + // English will also be a supported language + let nextSupportedLanguages = currentSupportedLanguages + + // this is the first instance where user turns on multi lang feature + // add all supported languages. + if (_.isEmpty(currentSupportedLanguages) && nextHasMultiLang) { + nextSupportedLanguages = Object.values(Language) + } + + mutateFormSupportedLanguages.mutate({ nextSupportedLanguages }) + + return mutateFormHasMultiLang.mutate(nextHasMultiLang) + }, [ + isLoadingSettings, + mutateFormHasMultiLang, + mutateFormSupportedLanguages, + settings, + ]) + + return ( + + + {settings && hasMultiLang && ( + + + + )} + + ) +} diff --git a/frontend/src/features/admin-form/settings/components/MultiLanguageSection/MultiLangugageSection.tsx b/frontend/src/features/admin-form/settings/components/MultiLanguageSection/MultiLangugageSection.tsx new file mode 100644 index 0000000000..abc86cd054 --- /dev/null +++ b/frontend/src/features/admin-form/settings/components/MultiLanguageSection/MultiLangugageSection.tsx @@ -0,0 +1,12 @@ +import { CategoryHeader } from '../CategoryHeader' + +import { FormMultiLangToggle } from './FormMultiLangToggle' + +export const MultiLanguageSection = (): JSX.Element => { + return ( + <> + Multi-language + + + ) +} diff --git a/frontend/src/features/admin-form/settings/components/MultiLanguageSection/TranslationListSection.tsx b/frontend/src/features/admin-form/settings/components/MultiLanguageSection/TranslationListSection.tsx new file mode 100644 index 0000000000..9957b6df35 --- /dev/null +++ b/frontend/src/features/admin-form/settings/components/MultiLanguageSection/TranslationListSection.tsx @@ -0,0 +1,284 @@ +import { useCallback, useMemo } from 'react' +import { BiArrowBack, BiCheck, BiError } from 'react-icons/bi' +import { useNavigate, useParams } from 'react-router-dom' +import { + As, + Divider, + Flex, + Icon, + IconButton, + Skeleton, + Text, + Tooltip, +} from '@chakra-ui/react' +import _ from 'lodash' + +import { BasicField, FormField, FormFieldDto, Language } from '~shared/types' + +import { PhHandsClapping } from '~assets/icons' +import { BxsDockTop } from '~assets/icons/BxsDockTop' +import { ADMINFORM_ROUTE } from '~constants/routes' + +import { useAdminForm } from '~features/admin-form/common/queries' +import { FieldListOption } from '~features/admin-form/create/builder-and-design/BuilderAndDesignDrawer/FieldListDrawer/FieldListOption' +import { + BASICFIELD_TO_DRAWER_META, + MYINFO_FIELD_TO_DRAWER_META, +} from '~features/admin-form/create/constants' +import { isMyInfo } from '~features/myinfo/utils' + +import { CategoryHeader } from '../CategoryHeader' + +interface QuestionRowProps { + questionTitle: string + icon: As + isMyInfoField: boolean + formFieldNum: number + hasTranslations: boolean + isStartPage?: boolean + isEndPage?: boolean +} + +export const QuestionRow = ({ + questionTitle, + icon, + isMyInfoField, + formFieldNum, + hasTranslations, + isStartPage, + isEndPage, +}: QuestionRowProps): JSX.Element => { + const { formId, language } = useParams() + const navigate = useNavigate() + const isTranslationRowDisabled = isMyInfoField + + const handleOnListClick = useCallback(() => { + // check if translation row is not disabled + if (!isTranslationRowDisabled) + navigate( + `${ADMINFORM_ROUTE}/${formId}/settings/multi-language/${language}`, + { + state: { + isTranslation: true, + formFieldNum, + isStartPage: isStartPage ?? false, + isEndPage: isEndPage ?? false, + }, + }, + ) + }, [ + formFieldNum, + formId, + isEndPage, + isStartPage, + isTranslationRowDisabled, + language, + navigate, + ]) + + return ( + + + + + + + {questionTitle} + + + + {!isMyInfoField && !hasTranslations && ( + + )} + {!isMyInfoField && hasTranslations && ( + + )} + + + + ) +} + +export const TranslationListSection = ({ + language, +}: { + language: string +}): JSX.Element => { + const { formId } = useParams() + const { data: form, isLoading } = useAdminForm() + const navigate = useNavigate() + + const uppercaseLanguage = language.charAt(0).toUpperCase() + language.slice(1) + + const startPageParagraph = form?.startPage?.paragraph + const endPage = form?.endPage + + const hasStartPageTranslations = useMemo(() => { + const startPageTranslations = form?.startPage.translations ?? [] + return ( + startPageTranslations.filter( + (translations) => + translations.language === (uppercaseLanguage as Language), + ).length > 0 + ) + }, [form?.startPage.translations, uppercaseLanguage]) + + const hasEndPageTranslations = useMemo(() => { + const endPageTitleTranslations = form?.endPage.titleTranslations ?? [] + const hasEndPageTitleTranslations = + !_.isUndefined(form?.endPage.title) && + endPageTitleTranslations.filter( + (translations) => + translations.language === (uppercaseLanguage as Language), + ).length > 0 + + const endPageParagraphTranslations = + form?.endPage.paragraphTranslations ?? [] + const hasEndPageParagraphTranslations = + _.isUndefined(form?.endPage.paragraph) || + endPageParagraphTranslations.filter( + (translations) => + translations.language === (uppercaseLanguage as Language), + ).length > 0 + + return hasEndPageTitleTranslations && hasEndPageParagraphTranslations + }, [form?.endPage, uppercaseLanguage]) + + const handleOnBackClick = useCallback(() => { + navigate(`${ADMINFORM_ROUTE}/${formId}/settings/multi-language`) + }, [formId, navigate]) + + const getHasTranslations = useCallback( + (form_field: FormFieldDto): boolean => { + let hasTranslations = true + if (!_.isEmpty(form_field.title) && form_field.titleTranslations) { + hasTranslations = + hasTranslations && + (form_field.titleTranslations?.some( + (titleTranslation) => + titleTranslation.language === uppercaseLanguage && + !_.isEmpty(titleTranslation.translation), + ) ?? + false) + } + + if ( + !_.isEmpty(form_field.description) && + form_field.descriptionTranslations + ) { + hasTranslations = + hasTranslations && + (form_field.descriptionTranslations?.some( + (descriptionTranslation) => + descriptionTranslation.language === uppercaseLanguage && + !_.isEmpty(descriptionTranslation.translation), + ) ?? + false) + } + + // Check if all the field options have their own translations + if ( + form_field.fieldType === BasicField.Checkbox || + form_field.fieldType === BasicField.Radio || + form_field.fieldType === BasicField.Dropdown + ) { + const fieldOptionsTranslation = + form_field.fieldOptionsTranslations.find( + (translation) => translation.language === uppercaseLanguage, + )?.translation ?? [] + + hasTranslations = + hasTranslations && + form_field.fieldOptions.length === fieldOptionsTranslation.length + } + return hasTranslations + }, + [uppercaseLanguage], + ) + + return ( + + + } + onClick={handleOnBackClick} + marginRight="2.25rem" + /> + + {uppercaseLanguage} + + {/* Start Page Translation */} + {!_.isEmpty(startPageParagraph) && ( + <> + + + + )} + + {form?.form_fields.map((form_field, id) => { + const isMyInfoField = isMyInfo(form_field) + + const questionIcon = isMyInfoField + ? MYINFO_FIELD_TO_DRAWER_META[form_field.myInfo.attr].icon + : BASICFIELD_TO_DRAWER_META[form_field.fieldType].icon + return ( + <> + + + + ) + })} + + {/* End Page Translation */} + + {!_.isEmpty(endPage) && ( + + )} + + + + ) +} diff --git a/frontend/src/features/admin-form/settings/components/MultiLanguageSection/TranslationSection.tsx b/frontend/src/features/admin-form/settings/components/MultiLanguageSection/TranslationSection.tsx new file mode 100644 index 0000000000..3215fcf600 --- /dev/null +++ b/frontend/src/features/admin-form/settings/components/MultiLanguageSection/TranslationSection.tsx @@ -0,0 +1,1011 @@ +import { useCallback, useEffect } from 'react' +import { FormProvider, useForm, useFormContext } from 'react-hook-form' +import { BiChevronLeft } from 'react-icons/bi' +import { useNavigate, useParams } from 'react-router-dom' +import { + Button, + Divider, + Flex, + FormControl, + Input, + Skeleton, + Text, + Textarea, +} from '@chakra-ui/react' +import _ from 'lodash' + +import { + BasicField, + Column, + FormEndPage, + FormField, + FormFieldDto, + FormStartPage, + Language, + TranslationMapping, + TranslationOptionMapping, +} from '~shared/types' +import { TableFieldDto } from '~shared/types/field' + +import { ADMINFORM_ROUTE } from '~constants/routes' +import { useToast } from '~hooks/useToast' + +import { useMutateFormPage } from '~features/admin-form/common/mutations' +import { useAdminForm } from '~features/admin-form/common/queries' +import { useEditFormField } from '~features/admin-form/create/builder-and-design/mutations/useEditFormField' +import { + updateEditStateSelector, + useFieldBuilderStore, +} from '~features/admin-form/create/builder-and-design/useFieldBuilderStore' + +export type TranslationInput = { + titleTranslation: string + descriptionTranslation: string + paragraphTranslations: string + fieldOptionsTranslations: string + tableColumnTitleTranslations: string[] + tableColumnDropdownTranslations: string[] +} + +export const TranslationContainer = ({ + language, + defaultString, + editingTranslation, + previousTranslation, +}: { + language: string + defaultString: string | undefined + editingTranslation: keyof TranslationInput + previousTranslation?: string +}): JSX.Element => { + const { register } = useFormContext() + + return ( + + + + Default + +