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
+
+
+
+
+
+ {language}
+
+
+
+
+
+
+ )
+}
+
+const OptionsTranslationContainer = ({
+ language,
+ defaultString,
+ editingTranslation,
+ previousTranslation,
+}: {
+ language: string
+ defaultString: string | undefined
+ editingTranslation: keyof TranslationInput
+ previousTranslation?: string
+}) => {
+ const { register } = useFormContext()
+ return (
+
+
+
+ Default
+
+
+
+
+
+ {language}
+
+
+
+
+
+
+ )
+}
+
+/**
+ * Parts of table that requires translation:
+ * - Title
+ * - Description
+ * - Columns name -> Text field
+ * - Field options for columns that have dropdown -> Dropdown field
+ * Each column will either have a dropdown or text input
+ */
+const TableTranslationContainer = ({
+ language,
+ columns,
+}: {
+ language: string
+ columns: Column[]
+}): JSX.Element | null => {
+ const { register } = useFormContext()
+ return (
+ <>
+ {columns.map((column, index) => {
+ let fieldOptionsString = ''
+
+ if (column.columnType === BasicField.Dropdown) {
+ fieldOptionsString = column.fieldOptions.join('\n')
+ }
+
+ const previousColumnTitleTranslation =
+ column?.titleTranslations?.find(
+ (translation) => translation.language === language,
+ )?.translation ?? ''
+
+ let previousFieldOptionsTranslations: string[] = []
+
+ if (column.columnType === BasicField.Dropdown) {
+ previousFieldOptionsTranslations =
+ column?.fieldOptionsTranslations?.find(
+ (translation) => translation.language === language,
+ )?.translation ?? []
+ }
+
+ const previousFieldOptionsTranslationsString =
+ previousFieldOptionsTranslations.join('\n')
+
+ return (
+
+
+ Column
+
+
+
+
+ Default
+
+
+
+
+
+ {language}
+
+
+
+
+
+
+ {column.columnType === BasicField.Dropdown && (
+
+
+ Options
+
+
+
+ Default
+
+
+
+
+
+ {language}
+
+
+
+
+
+
+ )}
+ {index !== columns.length - 1 && }
+
+ )
+ })}
+ >
+ )
+}
+
+const StartPageTranslationContainer = ({
+ startPage,
+ capitalisedLanguage,
+}: {
+ startPage?: FormStartPage
+ capitalisedLanguage: string
+}): JSX.Element | null => {
+ if (_.isUndefined(startPage)) {
+ return null
+ }
+
+ const currentTranslations = startPage.translations ?? []
+
+ const previousTranslation =
+ currentTranslations.find(
+ (translation) => translation.language === capitalisedLanguage,
+ )?.translation ?? ''
+
+ return (
+ <>
+
+
+ Question
+
+
+
+ >
+ )
+}
+
+const FormFieldTranslationContainer = ({
+ formFieldData,
+ language,
+}: {
+ formFieldData: FormField | undefined
+ language: string
+}): JSX.Element | null => {
+ if (_.isUndefined(formFieldData)) {
+ return null
+ }
+
+ const hasDescription =
+ !_.isUndefined(formFieldData?.description) &&
+ formFieldData?.description !== ''
+
+ const titleTranslations = formFieldData.titleTranslations ?? []
+ const descriptionTranslations = formFieldData.descriptionTranslations ?? []
+
+ const prevTitleTranslations =
+ titleTranslations.find((translation) => translation.language === language)
+ ?.translation ?? ''
+
+ const prevDescriptionTranslations =
+ descriptionTranslations.find(
+ (translation) => translation.language === language,
+ )?.translation ?? ''
+
+ let hasFieldOptions = false
+ let defaultFieldOptions = ''
+ let previousFieldOptionsTranslations = ''
+
+ if (
+ formFieldData.fieldType === BasicField.Radio ||
+ formFieldData.fieldType === BasicField.Checkbox ||
+ formFieldData.fieldType === BasicField.Dropdown
+ ) {
+ hasFieldOptions = true
+ defaultFieldOptions = formFieldData.fieldOptions.join('\n')
+
+ const existingFieldOptionsTranslations =
+ formFieldData?.fieldOptionsTranslations ?? []
+
+ const idx = existingFieldOptionsTranslations.findIndex((translation) => {
+ return translation.language === language
+ })
+
+ if (idx !== -1) {
+ previousFieldOptionsTranslations =
+ existingFieldOptionsTranslations[idx].translation.join('\n')
+ }
+ }
+
+ let isTableField = false
+ let columns: Column[] = []
+ if (formFieldData.fieldType === BasicField.Table) {
+ isTableField = true
+ columns = formFieldData.columns
+ }
+
+ return (
+ <>
+
+
+ Question
+
+
+
+ {hasDescription && (
+ <>
+
+
+
+ Description
+
+
+
+ >
+ )}
+ {hasFieldOptions && (
+ <>
+
+
+
+ Options
+
+
+
+ >
+ )}
+ {isTableField && (
+ <>
+
+
+ >
+ )}
+ >
+ )
+}
+
+const EndPageTranslationsContainer = ({
+ endPage,
+ capitalisedLanguage,
+}: {
+ endPage?: FormEndPage
+ capitalisedLanguage: string
+}): JSX.Element | null => {
+ if (_.isUndefined(endPage)) {
+ return null
+ }
+
+ const hasParagraph = !_.isEmpty(endPage.paragraph)
+
+ const currentTitleTranslations = endPage.titleTranslations ?? []
+ const currentParagraphTranslations = endPage.paragraphTranslations ?? []
+
+ const previousTranslation =
+ currentTitleTranslations.find(
+ (translation) => translation.language === capitalisedLanguage,
+ )?.translation ?? ''
+
+ const prevParagraphTranslations =
+ currentParagraphTranslations.find(
+ (translation) => translation.language === capitalisedLanguage,
+ )?.translation ?? ''
+
+ return (
+ <>
+
+
+ Question
+
+
+
+
+ {hasParagraph && (
+
+
+ Follow-up instructions
+
+
+
+ )}
+ >
+ )
+}
+
+export const TranslationSection = ({
+ language,
+ formFieldNumToBeTranslated,
+ isStartPageTranslations,
+ isEndPageTranslations,
+ isFormField,
+}: {
+ language: string
+ formFieldNumToBeTranslated: number
+ isStartPageTranslations?: boolean
+ isEndPageTranslations?: boolean
+ isFormField: boolean
+}): JSX.Element => {
+ const { data: form, isLoading } = useAdminForm()
+ const { formId } = useParams()
+ const navigate = useNavigate()
+ const { editFieldMutation } = useEditFormField()
+ const { endPageMutation, startPageMutation } = useMutateFormPage()
+ const methods = useForm()
+ const updateEditState = useFieldBuilderStore(updateEditStateSelector)
+
+ const { getValues, watch } = methods
+
+ const titleTranslationInput = watch('titleTranslation')
+ const descriptionTranslationInput = watch('descriptionTranslation')
+ const paragraphTranslationInput = watch('paragraphTranslations')
+
+ const toast = useToast({ status: 'danger' })
+
+ if (!isLoading && !form) {
+ toast({
+ description:
+ 'There was an error retrieving your form. Please try again later.',
+ })
+ }
+
+ const formFieldData = form?.form_fields[formFieldNumToBeTranslated]
+ const formStartPage = form?.startPage
+ const formEndPage = form?.endPage
+ const fieldId = formFieldData?._id
+ const capitalisedLanguage =
+ language.charAt(0).toUpperCase() + language.slice(1)
+
+ useEffect(() => {
+ if (formFieldData) updateEditState(formFieldData)
+ }, [formFieldData, updateEditState])
+
+ const handleOnBackClick = useCallback(() => {
+ navigate(`${ADMINFORM_ROUTE}/${formId}/settings/multi-language/${language}`)
+ }, [formId, language, navigate])
+
+ const handleOnSaveStartPageTranslation = useCallback(
+ (startPage: FormStartPage): TranslationMapping[] => {
+ // get current translations if any
+ const translations = startPage?.translations ?? []
+
+ // get index of current translations if any
+ const translationIdx = translations.findIndex(
+ (translation: TranslationMapping) =>
+ translation.language === capitalisedLanguage,
+ )
+
+ let updatedTranslations = translations
+
+ if (translationIdx !== -1) {
+ updatedTranslations[translationIdx].translation = titleTranslationInput
+ } else {
+ updatedTranslations = [
+ ...updatedTranslations,
+ {
+ language: capitalisedLanguage as Language,
+ translation: titleTranslationInput,
+ },
+ ]
+ }
+ return updatedTranslations
+ },
+ [capitalisedLanguage, titleTranslationInput],
+ )
+
+ const handleOnSaveEndPageTitleTranslations = useCallback(
+ (endPage: FormEndPage) => {
+ // get current title translations if any
+ const translations = endPage?.titleTranslations ?? []
+
+ // get index of current title translations if any
+ const translationIdx = translations.findIndex(
+ (translation: TranslationMapping) =>
+ translation.language === capitalisedLanguage,
+ )
+
+ let updatedTranslations = translations
+
+ if (translationIdx !== -1) {
+ updatedTranslations[translationIdx].translation = titleTranslationInput
+ } else {
+ updatedTranslations = [
+ ...updatedTranslations,
+ {
+ language: capitalisedLanguage as Language,
+ translation: titleTranslationInput,
+ },
+ ]
+ }
+
+ return updatedTranslations
+ },
+ [capitalisedLanguage, titleTranslationInput],
+ )
+
+ const handleOnSaveEndPageParagraphTranslation = useCallback(
+ (endPage: FormEndPage) => {
+ // get current paragraph translations if any
+ const translations = endPage?.paragraphTranslations ?? []
+
+ // get index of current paragraph translations if any
+ const translationIdx = translations.findIndex(
+ (translation: TranslationMapping) =>
+ translation.language === capitalisedLanguage,
+ )
+
+ let updatedTranslations = translations
+
+ if (translationIdx !== -1) {
+ updatedTranslations[translationIdx].translation =
+ paragraphTranslationInput
+ } else {
+ updatedTranslations = [
+ ...updatedTranslations,
+ {
+ language: capitalisedLanguage as Language,
+ translation: titleTranslationInput,
+ },
+ ]
+ }
+
+ return updatedTranslations
+ },
+ [capitalisedLanguage, paragraphTranslationInput, titleTranslationInput],
+ )
+
+ const handleOnSaveTableTranslations = useCallback(
+ (field: TableFieldDto): TableFieldDto => {
+ // for each column, update the title translation if any and update the field options translation
+ // if any
+ const updatedTableField = field
+ updatedTableField.columns.forEach((column, index) => {
+ // update title translations
+ const translatedColumnTitle = getValues(
+ `tableColumnTitleTranslations.${index}`,
+ )
+
+ let updatedColumnTitleTranslations = column.titleTranslations ?? []
+ const columnTitleTranslationIdx =
+ updatedColumnTitleTranslations.findIndex(
+ (translation) => translation.translation === capitalisedLanguage,
+ )
+
+ if (columnTitleTranslationIdx !== -1) {
+ updatedColumnTitleTranslations[
+ columnTitleTranslationIdx
+ ].translation = translatedColumnTitle
+ } else {
+ updatedColumnTitleTranslations = [
+ ...updatedColumnTitleTranslations,
+ {
+ language: capitalisedLanguage as Language,
+ translation: translatedColumnTitle,
+ },
+ ]
+ }
+
+ column.titleTranslations = updatedColumnTitleTranslations
+
+ // get translated options
+ if (column.columnType === BasicField.Dropdown) {
+ const optionsTranslationsInput = getValues(
+ `tableColumnDropdownTranslations.${index}`,
+ )
+
+ let optionsTranslationsArr = optionsTranslationsInput.split('\n')
+
+ optionsTranslationsArr = optionsTranslationsArr.filter(
+ (optionsTranslation) => !_.isEmpty(optionsTranslation),
+ )
+
+ // find if there exists translations for the options
+ let updatedOptionsTranslations = column.fieldOptionsTranslations ?? []
+ const translationIdx = updatedOptionsTranslations.findIndex(
+ (translation) => translation.language === capitalisedLanguage,
+ )
+
+ if (translationIdx !== -1) {
+ updatedOptionsTranslations[translationIdx].translation =
+ optionsTranslationsArr
+ } else {
+ updatedOptionsTranslations = [
+ ...updatedOptionsTranslations,
+ {
+ language: capitalisedLanguage as Language,
+ translation: optionsTranslationsArr,
+ },
+ ]
+ }
+
+ column.fieldOptionsTranslations = updatedOptionsTranslations
+ }
+
+ return column
+ })
+
+ return updatedTableField
+ },
+ [capitalisedLanguage, getValues],
+ )
+
+ const handleOnSaveTitleTranslation = useCallback(
+ (formFieldData: FormFieldDto): TranslationMapping[] => {
+ const titleTranslations = formFieldData.titleTranslations ?? []
+
+ const translationIdx = titleTranslations.findIndex(
+ (translation: TranslationMapping) =>
+ translation.language === capitalisedLanguage,
+ )
+
+ let updatedTitleTranslations = titleTranslations
+
+ // title translations for this language exists, need to
+ // override it with new translations on save
+ if (translationIdx !== -1) {
+ updatedTitleTranslations[translationIdx].translation =
+ titleTranslationInput
+ } else {
+ updatedTitleTranslations = [
+ ...updatedTitleTranslations,
+ {
+ language: capitalisedLanguage as Language,
+ translation: titleTranslationInput,
+ },
+ ]
+ }
+
+ return updatedTitleTranslations
+ },
+ [capitalisedLanguage, titleTranslationInput],
+ )
+
+ const handleOnSaveDescriptionTranslations = useCallback(
+ (formFieldData: FormFieldDto): TranslationMapping[] => {
+ const descriptionTranslations =
+ formFieldData.descriptionTranslations ?? []
+
+ const translationIdx = descriptionTranslations.findIndex(
+ (translation: TranslationMapping) =>
+ translation.language === capitalisedLanguage,
+ )
+
+ let updatedDescriptionTranslations = descriptionTranslations
+
+ if (translationIdx !== -1) {
+ updatedDescriptionTranslations[translationIdx].translation =
+ descriptionTranslationInput
+ } else {
+ updatedDescriptionTranslations = [
+ ...updatedDescriptionTranslations,
+ {
+ language: capitalisedLanguage as Language,
+ translation: descriptionTranslationInput,
+ },
+ ]
+ }
+
+ return updatedDescriptionTranslations
+ },
+ [capitalisedLanguage, descriptionTranslationInput],
+ )
+
+ const handleOnSaveOptionsTranslations = useCallback(
+ (formFieldData: FormField): TranslationOptionMapping[] => {
+ if (
+ formFieldData.fieldType === BasicField.Radio ||
+ formFieldData.fieldType === BasicField.Checkbox ||
+ formFieldData.fieldType === BasicField.Dropdown
+ ) {
+ const fieldOptionsTranslationInput = watch('fieldOptionsTranslations')
+
+ // filter out empty strings from input
+ let fieldOptionsTranslationsArr =
+ fieldOptionsTranslationInput.split('\n')
+
+ fieldOptionsTranslationsArr = fieldOptionsTranslationsArr.filter(
+ (fieldOptionsTranslation) => !_.isEmpty(fieldOptionsTranslation),
+ )
+
+ // get existing options translations if any
+ const existingOptionsTranslations =
+ formFieldData?.fieldOptionsTranslations ?? []
+
+ const translationIdx = existingOptionsTranslations.findIndex(
+ (translations) => {
+ return translations.language === capitalisedLanguage
+ },
+ )
+
+ let updatedOptionsTranslations = existingOptionsTranslations
+
+ // there are existing translations for the options
+ if (translationIdx !== -1) {
+ updatedOptionsTranslations[translationIdx].translation =
+ fieldOptionsTranslationsArr
+ } else {
+ updatedOptionsTranslations = [
+ ...existingOptionsTranslations,
+ {
+ language: capitalisedLanguage as Language,
+ translation: fieldOptionsTranslationsArr,
+ },
+ ]
+ }
+
+ return updatedOptionsTranslations
+ }
+ return []
+ },
+ [capitalisedLanguage, watch],
+ )
+
+ const handleOnSaveClick = useCallback(() => {
+ if (formFieldData) {
+ const updatedTitleTranslations =
+ handleOnSaveTitleTranslation(formFieldData)
+
+ let updatedDescriptionTranslations: TranslationMapping[] = []
+
+ // update translations for form fields
+ if (!_.isUndefined(formFieldData.description)) {
+ updatedDescriptionTranslations =
+ handleOnSaveDescriptionTranslations(formFieldData)
+ }
+
+ let updatedFormData = {
+ ...formFieldData,
+ titleTranslations: updatedTitleTranslations,
+ descriptionTranslations: updatedDescriptionTranslations,
+ }
+
+ if (
+ formFieldData.fieldType === BasicField.Radio ||
+ formFieldData.fieldType === BasicField.Checkbox ||
+ formFieldData.fieldType === BasicField.Dropdown
+ ) {
+ let updatedFieldOptionsTranslations: TranslationOptionMapping[] = []
+
+ // check if there are any options to be translated
+ if (!_.isEmpty(formFieldData.fieldOptions)) {
+ updatedFieldOptionsTranslations =
+ handleOnSaveOptionsTranslations(formFieldData)
+ }
+
+ updatedFormData = {
+ ...updatedFormData,
+ fieldOptionsTranslations: updatedFieldOptionsTranslations,
+ }
+ }
+
+ if (formFieldData.fieldType === BasicField.Table) {
+ const updatedFormFieldData =
+ handleOnSaveTableTranslations(formFieldData)
+
+ updatedFormData = {
+ ...updatedFormData,
+ columns: updatedFormFieldData.columns,
+ }
+ }
+
+ editFieldMutation.mutate(
+ {
+ ...updatedFormData,
+ _id: fieldId,
+ } as FormFieldDto,
+ { onSuccess: () => handleOnBackClick() },
+ )
+ }
+
+ // update translations for start page
+ if (isStartPageTranslations && formStartPage) {
+ const updatedTranslations =
+ handleOnSaveStartPageTranslation(formStartPage)
+
+ const updatedFormStartPage = {
+ ...formStartPage,
+ translations: updatedTranslations,
+ }
+
+ startPageMutation.mutate(
+ {
+ ...updatedFormStartPage,
+ },
+ {
+ onSuccess: () => handleOnBackClick(),
+ },
+ )
+ }
+
+ // update translations for end page
+ if (isEndPageTranslations && formEndPage) {
+ const updatedTitleTranslations =
+ handleOnSaveEndPageTitleTranslations(formEndPage)
+
+ let updatedParagraphTranslations: TranslationMapping[] = []
+
+ if (!_.isUndefined(formEndPage.paragraph)) {
+ updatedParagraphTranslations =
+ handleOnSaveEndPageParagraphTranslation(formEndPage)
+ }
+
+ const updatedFormEndPage = {
+ ...formEndPage,
+ titleTranslations: updatedTitleTranslations,
+ paragraphTranslations: updatedParagraphTranslations,
+ }
+
+ endPageMutation.mutate(
+ {
+ ...updatedFormEndPage,
+ },
+ {
+ onSuccess: () => handleOnBackClick(),
+ },
+ )
+ }
+ }, [
+ editFieldMutation,
+ endPageMutation,
+ fieldId,
+ formEndPage,
+ formFieldData,
+ formStartPage,
+ handleOnBackClick,
+ handleOnSaveDescriptionTranslations,
+ handleOnSaveEndPageParagraphTranslation,
+ handleOnSaveEndPageTitleTranslations,
+ handleOnSaveOptionsTranslations,
+ handleOnSaveStartPageTranslation,
+ handleOnSaveTableTranslations,
+ handleOnSaveTitleTranslation,
+ isEndPageTranslations,
+ isStartPageTranslations,
+ startPageMutation,
+ ])
+
+ return (
+
+
+ }
+ onClick={handleOnBackClick}
+ marginRight="2.25rem"
+ >
+ Back to all questions
+
+
+
+
+ {isStartPageTranslations && (
+
+ )}
+ {isFormField && formFieldData && (
+
+ )}
+ {isEndPageTranslations && formEndPage && (
+
+ )}
+
+
+
+
+ )
+}
diff --git a/frontend/src/features/admin-form/settings/mutations.ts b/frontend/src/features/admin-form/settings/mutations.ts
index 72c8d79ab4..e92795a56a 100644
--- a/frontend/src/features/admin-form/settings/mutations.ts
+++ b/frontend/src/features/admin-form/settings/mutations.ts
@@ -9,6 +9,7 @@ import {
FormResponseMode,
FormSettings,
FormStatus,
+ FormSupportedLanguages,
StorageFormSettings,
} from '~shared/types/form/form'
import { TwilioCredentials } from '~shared/types/twilio'
@@ -31,10 +32,12 @@ import {
updateFormCaptcha,
updateFormEmails,
updateFormEsrvcId,
+ updateFormHasMultiLang,
updateFormInactiveMessage,
updateFormIssueNotification,
updateFormLimit,
updateFormStatus,
+ updateFormSupportedLanguages,
updateFormTitle,
updateFormWebhookRetries,
updateFormWebhookUrl,
@@ -134,6 +137,50 @@ export const useMutateFormSettings = () => {
},
)
+ const mutateFormHasMultiLang = useMutation(
+ (nextHasMultiLang: boolean) =>
+ updateFormHasMultiLang(formId, nextHasMultiLang),
+ {
+ onSuccess: (newData) => {
+ handleSuccess({
+ newData,
+ toastDescription: newData.hasMultiLang
+ ? 'Multi-language enabled. Respondents can now select other languages to view your form in.'
+ : 'Multi-language disabled.',
+ })
+ },
+ },
+ )
+
+ const mutateFormSupportedLanguages = useMutation(
+ (nextSupportedLanguages?: FormSupportedLanguages) =>
+ updateFormSupportedLanguages(
+ formId,
+ nextSupportedLanguages?.nextSupportedLanguages,
+ ),
+ {
+ onSuccess: (newData, newSupportedLanguages) => {
+ if (newSupportedLanguages && newSupportedLanguages.selectedLanguage) {
+ const supportedLanguages =
+ newSupportedLanguages.nextSupportedLanguages ?? []
+ let successMessage: string
+ if (
+ supportedLanguages.includes(newSupportedLanguages.selectedLanguage)
+ ) {
+ successMessage = `Respondents will now be able to select and view your form in ${newSupportedLanguages.selectedLanguage}.`
+ } else {
+ successMessage = `${newSupportedLanguages.selectedLanguage} is now hidden. Respondents will not be able to see it.`
+ }
+ handleSuccess({
+ newData,
+ toastDescription: successMessage,
+ })
+ }
+ },
+ onError: handleError,
+ },
+ )
+
const mutateFormCaptcha = useMutation(
(nextHasCaptcha: boolean) => updateFormCaptcha(formId, nextHasCaptcha),
{
@@ -340,6 +387,8 @@ export const useMutateFormSettings = () => {
mutateFormWebhookUrl,
mutateFormStatus,
mutateFormLimit,
+ mutateFormHasMultiLang,
+ mutateFormSupportedLanguages,
mutateFormInactiveMessage,
mutateFormCaptcha,
mutateFormIssueNotification,
diff --git a/frontend/src/features/public-form/PublicFormContext.tsx b/frontend/src/features/public-form/PublicFormContext.tsx
index b354d44100..e7fc795dc2 100644
--- a/frontend/src/features/public-form/PublicFormContext.tsx
+++ b/frontend/src/features/public-form/PublicFormContext.tsx
@@ -9,7 +9,7 @@ import {
import { UseQueryResult } from 'react-query'
import { MultirespondentSubmissionDto } from '~shared/types'
-import { PublicFormViewDto } from '~shared/types/form'
+import { Language, PublicFormViewDto } from '~shared/types/form'
import { decryptSubmission } from './utils/decryptSubmission'
@@ -72,6 +72,12 @@ export interface PublicFormContextProps
setPreviousSubmission?: (
previousSubmission: ReturnType,
) => void
+
+ // callback to set public form's language
+ setPublicFormLanguage: (language: Language) => void
+
+ // public form's language
+ publicFormLanguage: Language
}
export const PublicFormContext = createContext<
diff --git a/frontend/src/features/public-form/PublicFormPage.tsx b/frontend/src/features/public-form/PublicFormPage.tsx
index a37128e497..e55b16e793 100644
--- a/frontend/src/features/public-form/PublicFormPage.tsx
+++ b/frontend/src/features/public-form/PublicFormPage.tsx
@@ -12,6 +12,7 @@ import FormInstructions from './components/FormInstructions'
import FormIssueFeedback from './components/FormIssueFeedback'
import { PublicFormLogo } from './components/FormLogo'
import FormStartPage from './components/FormStartPage'
+import LanguageControl from './components/LanguageControl'
import { PublicFormWrapper } from './components/PublicFormWrapper'
import { PublicFormProvider } from './PublicFormProvider'
@@ -34,6 +35,7 @@ export const PublicFormPage = (): JSX.Element => {
+
diff --git a/frontend/src/features/public-form/PublicFormProvider.tsx b/frontend/src/features/public-form/PublicFormProvider.tsx
index 0d4604e011..b2b98744c3 100644
--- a/frontend/src/features/public-form/PublicFormProvider.tsx
+++ b/frontend/src/features/public-form/PublicFormProvider.tsx
@@ -27,6 +27,7 @@ import { CaptchaTypes } from '~shared/types/captcha'
import {
FormAuthType,
FormResponseMode,
+ Language,
ProductItem,
PublicFormDto,
} from '~shared/types/form'
@@ -130,6 +131,9 @@ export const PublicFormProvider = ({
// Once form has been submitted, submission data will be set here.
const [submissionData, setSubmissionData] = useState()
const [numVisibleFields, setNumVisibleFields] = useState(0)
+ const [publicFormLanguage, setPublicFormLanguage] = useState(
+ Language.ENGLISH,
+ )
const {
data,
@@ -829,6 +833,8 @@ export const PublicFormProvider = ({
previousSubmission,
previousAttachments,
setPreviousSubmission,
+ publicFormLanguage,
+ setPublicFormLanguage,
...commonFormValues,
...data,
...rest,
diff --git a/frontend/src/features/public-form/components/FormEndPage/FormEndPage.tsx b/frontend/src/features/public-form/components/FormEndPage/FormEndPage.tsx
index bf467c93ea..1712d6b35d 100644
--- a/frontend/src/features/public-form/components/FormEndPage/FormEndPage.tsx
+++ b/frontend/src/features/public-form/components/FormEndPage/FormEndPage.tsx
@@ -1,6 +1,6 @@
import { Container, Flex, Stack, StackDivider } from '@chakra-ui/react'
-import { FormColorTheme, FormDto } from '~shared/types/form'
+import { FormColorTheme, FormDto, Language } from '~shared/types/form'
import {
SubmissionData,
@@ -19,6 +19,7 @@ export interface FormEndPageProps {
handleSubmitFeedback: (inputs: FeedbackFormInput) => void
isFeedbackSectionHidden: boolean
colorTheme: FormColorTheme
+ selectedLanguage: Language
}
export const FormEndPage = ({
diff --git a/frontend/src/features/public-form/components/FormEndPage/FormEndPageContainer.tsx b/frontend/src/features/public-form/components/FormEndPage/FormEndPageContainer.tsx
index c5ee1497ed..97015d2601 100644
--- a/frontend/src/features/public-form/components/FormEndPage/FormEndPageContainer.tsx
+++ b/frontend/src/features/public-form/components/FormEndPage/FormEndPageContainer.tsx
@@ -13,7 +13,8 @@ import { PaymentEndPagePreview } from './components/PaymentEndPagePreview'
import { FormEndPage } from './FormEndPage'
export const FormEndPageContainer = (): JSX.Element | null => {
- const { form, formId, submissionData, isPreview } = usePublicFormContext()
+ const { form, formId, submissionData, isPreview, publicFormLanguage } =
+ usePublicFormContext()
const { submitFormFeedbackMutation } = useSubmitFormFeedbackMutation(
formId,
submissionData?.id ?? '',
@@ -87,6 +88,7 @@ export const FormEndPageContainer = (): JSX.Element | null => {
endPage={form.endPage}
isFeedbackSectionHidden={isFeedbackHidden}
handleSubmitFeedback={handleSubmitFeedback}
+ selectedLanguage={publicFormLanguage}
/>
)
diff --git a/frontend/src/features/public-form/components/FormFields/FieldFactory.tsx b/frontend/src/features/public-form/components/FormFields/FieldFactory.tsx
index a2ff05cd52..7419bbd86e 100644
--- a/frontend/src/features/public-form/components/FormFields/FieldFactory.tsx
+++ b/frontend/src/features/public-form/components/FormFields/FieldFactory.tsx
@@ -50,34 +50,113 @@ interface FieldFactoryProps {
export const FieldFactory = memo(
({ field, ...rest }: FieldFactoryProps) => {
- const { myInfoChildrenBirthRecords, form } = usePublicFormContext()
+ const { myInfoChildrenBirthRecords, publicFormLanguage, form } =
+ usePublicFormContext()
switch (field.fieldType) {
case BasicField.Section:
- return
+ return (
+
+ )
case BasicField.Checkbox:
- return
+ return (
+
+ )
case BasicField.Radio:
- return
+ return (
+
+ )
case BasicField.Nric:
- return
+ return (
+
+ )
case BasicField.Number:
- return
+ return (
+
+ )
case BasicField.Decimal:
- return
+ return (
+
+ )
case BasicField.ShortText:
- return
+ return (
+
+ )
case BasicField.LongText:
- return
+ return (
+
+ )
case BasicField.YesNo:
- return
+ return (
+
+ )
case BasicField.Dropdown:
- return
+ return (
+
+ )
case BasicField.CountryRegion:
- return
+ return (
+
+ )
case BasicField.Date:
- return
+ return (
+
+ )
case BasicField.Uen:
- return
+ return (
+
+ )
case BasicField.Attachment: {
const enableDownload =
form?.responseMode === FormResponseMode.Multirespondent
@@ -86,39 +165,80 @@ export const FieldFactory = memo(
schema={field}
{...rest}
enableDownload={enableDownload}
+ selectedLanguage={publicFormLanguage}
/>
)
}
case BasicField.HomeNo:
- return
+ return (
+
+ )
case BasicField.Mobile: {
return field.isVerifiable ? (
) : (
-
+
)
}
case BasicField.Statement:
- return
+ return (
+
+ )
case BasicField.Rating:
- return
+ return (
+
+ )
case BasicField.Email: {
return field.isVerifiable ? (
) : (
-
+
)
}
case BasicField.Image:
- return
+ return (
+
+ )
case BasicField.Table:
- return
+ return (
+
+ )
case BasicField.Children:
return (
{
const { sectionRefs } = useFormSections()
- const { form, submissionData } = usePublicFormContext()
+ const { form, submissionData, publicFormLanguage } = usePublicFormContext()
if (submissionData || !form?.startPage.paragraph) return null
+ let content = form?.startPage.paragraph
+
+ // English is the default even if multi lang is not supported for this form
+ if (publicFormLanguage !== Language.ENGLISH) {
+ const translations = form?.startPage?.translations ?? []
+
+ const contentTranslationIdx = translations.findIndex(
+ (translation) => translation.language === publicFormLanguage,
+ )
+
+ if (contentTranslationIdx !== -1) {
+ content = translations[contentTranslationIdx].translation
+ }
+ }
+
return (
{
tabIndex={-1}
>
diff --git a/frontend/src/features/public-form/components/FormStartPage/FormHeader.tsx b/frontend/src/features/public-form/components/FormStartPage/FormHeader.tsx
index d3100f4331..0c2753cca1 100644
--- a/frontend/src/features/public-form/components/FormStartPage/FormHeader.tsx
+++ b/frontend/src/features/public-form/components/FormStartPage/FormHeader.tsx
@@ -162,7 +162,8 @@ export const FormHeader = ({
>((props, ref) => (
+
+ )),
+)
+
+export const FontDefaultSvgr = chakra(MemoFontDefaultSvgr)
diff --git a/frontend/src/features/public-form/components/LanguageControl/FontLargeSvgr.tsx b/frontend/src/features/public-form/components/LanguageControl/FontLargeSvgr.tsx
new file mode 100644
index 0000000000..84ba29ec36
--- /dev/null
+++ b/frontend/src/features/public-form/components/LanguageControl/FontLargeSvgr.tsx
@@ -0,0 +1,23 @@
+import { forwardRef, memo, SVGProps } from 'react'
+import { chakra } from '@chakra-ui/react'
+
+const MemoFontLargeSvgr = memo(
+ forwardRef>((props, ref) => (
+
+ )),
+)
+
+export const FontLargeSvgr = chakra(MemoFontLargeSvgr)
diff --git a/frontend/src/features/public-form/components/LanguageControl/FontLargestSvgr.tsx b/frontend/src/features/public-form/components/LanguageControl/FontLargestSvgr.tsx
new file mode 100644
index 0000000000..597ef71c38
--- /dev/null
+++ b/frontend/src/features/public-form/components/LanguageControl/FontLargestSvgr.tsx
@@ -0,0 +1,23 @@
+import { forwardRef, memo, SVGProps } from 'react'
+import { chakra } from '@chakra-ui/react'
+
+const MemoFontLargestSvgr = memo(
+ forwardRef>((props, ref) => (
+
+ )),
+)
+
+export const FontLargestSvgr = chakra(MemoFontLargestSvgr)
diff --git a/frontend/src/features/public-form/components/LanguageControl/LanguageControl.tsx b/frontend/src/features/public-form/components/LanguageControl/LanguageControl.tsx
new file mode 100644
index 0000000000..160f4e7478
--- /dev/null
+++ b/frontend/src/features/public-form/components/LanguageControl/LanguageControl.tsx
@@ -0,0 +1,104 @@
+import { BiChevronDown } from 'react-icons/bi'
+import {
+ Button,
+ Flex,
+ HStack,
+ Menu,
+ MenuButton,
+ MenuItem,
+ MenuList,
+} from '@chakra-ui/react'
+
+import { Language } from '~shared/types'
+
+import { usePublicFormContext } from '~features/public-form/PublicFormContext'
+
+import { useBgColor } from '../PublicFormWrapper'
+
+type LanguageListType = {
+ language: Language
+ title: string
+}
+
+const LANGUAGES: LanguageListType[] = [
+ { language: Language.ENGLISH, title: 'English' },
+ { language: Language.CHINESE, title: '中文' },
+ { language: Language.MALAY, title: 'Melayu' },
+ { language: Language.TAMIL, title: 'தமிழ்' },
+]
+
+export const LanguageControl = (): JSX.Element | null => {
+ const { form, publicFormLanguage, setPublicFormLanguage, submissionData } =
+ usePublicFormContext()
+
+ const availableLanguages = new Set(form?.supportedLanguages ?? [])
+
+ const languagesList = LANGUAGES.filter((language) =>
+ availableLanguages.has(language.language),
+ )
+
+ // English language is always supported. Hence if form supports multi-lang
+ // and there is more than one supported language available, show the
+ // language dropdown.
+ const shouldShowLanguageDropdown =
+ form?.hasMultiLang && availableLanguages.size > 1
+
+ const bgColour = useBgColor({
+ colorTheme: form?.startPage.colorTheme,
+ })
+
+ const handleLanguageChange = (language: string) => {
+ if (setPublicFormLanguage) {
+ setPublicFormLanguage(language as Language)
+ }
+ }
+
+ const selectedLanguage = LANGUAGES.find(
+ (language) => language.language === publicFormLanguage,
+ )?.title
+
+ // Submission data is not undefinde in the form end page. Use
+ // this to not render the language control component in the form
+ // end page.
+ if (submissionData) return null
+
+ return (
+
+
+ {shouldShowLanguageDropdown && (
+
+ )}
+
+
+ )
+}
diff --git a/frontend/src/features/public-form/components/LanguageControl/index.ts b/frontend/src/features/public-form/components/LanguageControl/index.ts
new file mode 100644
index 0000000000..5fc11afd5c
--- /dev/null
+++ b/frontend/src/features/public-form/components/LanguageControl/index.ts
@@ -0,0 +1 @@
+export { LanguageControl as default } from './LanguageControl'
diff --git a/frontend/src/features/public-form/components/PublicFormWrapper.tsx b/frontend/src/features/public-form/components/PublicFormWrapper.tsx
index 385c1b0bff..8065f28c63 100644
--- a/frontend/src/features/public-form/components/PublicFormWrapper.tsx
+++ b/frontend/src/features/public-form/components/PublicFormWrapper.tsx
@@ -39,7 +39,14 @@ export const PublicFormWrapper = ({
})
return (
-
+
{isAuthRequired ? null : }
{children}
diff --git a/frontend/src/templates/Field/Attachment/AttachmentField.tsx b/frontend/src/templates/Field/Attachment/AttachmentField.tsx
index 3d20d22bf5..527ef9cb60 100644
--- a/frontend/src/templates/Field/Attachment/AttachmentField.tsx
+++ b/frontend/src/templates/Field/Attachment/AttachmentField.tsx
@@ -31,6 +31,7 @@ export const AttachmentField = ({
disableRequiredValidation,
enableDownload,
colorTheme = FormColorTheme.Blue,
+ selectedLanguage: language,
}: AttachmentFieldProps): JSX.Element => {
const fieldName = schema._id
const validationRules = useMemo(
@@ -102,7 +103,7 @@ export const AttachmentField = ({
)
return (
-
+
(
diff --git a/frontend/src/templates/Field/Checkbox/CheckboxField.tsx b/frontend/src/templates/Field/Checkbox/CheckboxField.tsx
index e101392b20..f02f5a4a09 100644
--- a/frontend/src/templates/Field/Checkbox/CheckboxField.tsx
+++ b/frontend/src/templates/Field/Checkbox/CheckboxField.tsx
@@ -8,7 +8,7 @@ import {
useMultiStyleConfig,
} from '@chakra-ui/react'
-import { FormColorTheme } from '~shared/types'
+import { FormColorTheme, Language } from '~shared/types'
import { CHECKBOX_THEME_KEY } from '~theme/components/Checkbox'
import { createCheckboxValidationRules } from '~utils/fieldValidation'
@@ -36,6 +36,7 @@ export const CheckboxField = ({
schema,
disableRequiredValidation,
colorTheme = FormColorTheme.Blue,
+ selectedLanguage = Language.ENGLISH,
}: CheckboxFieldProps): JSX.Element => {
const fieldColorScheme = useMemo(
() => `theme-${colorTheme}` as const,
@@ -59,11 +60,38 @@ export const CheckboxField = ({
[disableRequiredValidation, schema],
)
+ const defaultEnglishCheckboxOptions = schema.fieldOptions
+
const { register, getValues, control } = useFormContext()
const { isValid, isSubmitting, errors } = useFormState({
name: schema._id,
})
+ const fieldOptions = useMemo(() => {
+ const fieldOptionsTranslations = schema?.fieldOptionsTranslations ?? []
+
+ const translationIdx = fieldOptionsTranslations.findIndex((translation) => {
+ return translation.language === selectedLanguage
+ })
+
+ // Check if translations for field options exist and whether
+ // each field option has its own respective translation. If not
+ // render the default field options in English.
+ if (
+ translationIdx !== -1 &&
+ fieldOptionsTranslations[translationIdx].translation.length ===
+ defaultEnglishCheckboxOptions.length
+ ) {
+ return fieldOptionsTranslations[translationIdx].translation
+ } else {
+ return defaultEnglishCheckboxOptions
+ }
+ }, [
+ defaultEnglishCheckboxOptions,
+ schema?.fieldOptionsTranslations,
+ selectedLanguage,
+ ])
+
const othersValidationRules = useMemo(
() => ({
validate: (value?: string) => {
@@ -81,7 +109,11 @@ export const CheckboxField = ({
[checkboxInputName, getValues],
)
return (
-
+
(
- {schema.fieldOptions.map((o, idx) => (
+ {fieldOptions.map((o, idx) => (
{o}
))}
- {schema.fieldOptions.length === 1 ? (
+ {fieldOptions.length === 1 ? (
// React-hook-form quirk where the value will not be set in an array if there is only a single checkbox option.
// This is a workaround to set the value in an array by registering a hidden checkbox with the same id.
// See https://github.com/react-hook-form/react-hook-form/issues/7834#issuecomment-1040735711.
diff --git a/frontend/src/templates/Field/CountryRegionField/CountryRegionField.tsx b/frontend/src/templates/Field/CountryRegionField/CountryRegionField.tsx
index 6ef5f73d9a..5c18937292 100644
--- a/frontend/src/templates/Field/CountryRegionField/CountryRegionField.tsx
+++ b/frontend/src/templates/Field/CountryRegionField/CountryRegionField.tsx
@@ -30,6 +30,7 @@ export const CountryRegionField = ({
schema,
disableRequiredValidation,
colorTheme = FormColorTheme.Blue,
+ selectedLanguage: language,
...fieldContainerProps
}: CountryRegionFieldProps): JSX.Element => {
const schemaWithFieldOptions = useMemo(() => {
@@ -49,7 +50,11 @@ export const CountryRegionField = ({
const { control } = useFormContext()
return (
-
+
{
const validationRules = useMemo(
@@ -73,7 +74,11 @@ export const DateField = ({
const { control } = useFormContext()
return (
-
+
{
const validationRules = useMemo(
() => createDecimalValidationRules(schema, disableRequiredValidation),
@@ -27,7 +28,7 @@ export const DecimalField = ({
const { control } = useFormContext()
return (
-
+
{
const rules = useMemo(() => {
@@ -29,8 +31,50 @@ export const DropdownField = ({
const { control } = useFormContext()
+ const fieldOptions: ComboboxItem[] = useMemo(() => {
+ const defaultEnglishFieldOptions = schema.fieldOptions
+ const fieldOptionsTranslations = schema?.fieldOptionsTranslations ?? []
+
+ const translationIdx = fieldOptionsTranslations.findIndex((translation) => {
+ return translation.language === selectedLanguage
+ })
+
+ // Check if translations for field options exist and whether
+ // each field option has its own respective translation. If not
+ // render the default field options in English.
+ if (
+ translationIdx !== -1 &&
+ fieldOptionsTranslations[translationIdx].translation.length ===
+ defaultEnglishFieldOptions.length
+ ) {
+ const translatedFieldOptions =
+ fieldOptionsTranslations[translationIdx].translation
+
+ // The label will be the translated option while the value is the
+ // default English option so that upon form submission, the value recorded
+ // will be the default english option. The indexes of the translated options
+ // and the default English options are corresponding with each other.
+ return translatedFieldOptions.map((translatedFieldOption, index) => {
+ return {
+ value: defaultEnglishFieldOptions[index],
+ label: translatedFieldOption,
+ }
+ })
+ } else {
+ return schema.fieldOptions.map((fieldOption) => {
+ return {
+ value: fieldOption,
+ }
+ })
+ }
+ }, [schema.fieldOptions, schema?.fieldOptionsTranslations, selectedLanguage])
+
return (
-
+
(
)}
diff --git a/frontend/src/templates/Field/Email/EmailField.tsx b/frontend/src/templates/Field/Email/EmailField.tsx
index 89c50f027b..3b77cdcbf0 100644
--- a/frontend/src/templates/Field/Email/EmailField.tsx
+++ b/frontend/src/templates/Field/Email/EmailField.tsx
@@ -20,9 +20,14 @@ export const EmailField = ({
disableRequiredValidation,
errorVariant,
inputProps,
+ selectedLanguage: language,
}: EmailFieldProps): JSX.Element => {
return (
-
+
/**
* Color theme of form, if available. Defaults to `FormColorTheme.Blue`
@@ -45,6 +52,11 @@ export type BaseFieldProps = {
* Optional specification for error message variant.
*/
errorVariant?: 'white'
+
+ /**
+ * Form language selected by user.
+ */
+ selectedLanguage?: Language
}
export interface FieldContainerProps extends BaseFieldProps {
@@ -57,11 +69,43 @@ export const FieldContainer = ({
errorKey,
showMyInfoBadge,
errorVariant,
+ selectedLanguage = Language.ENGLISH,
}: FieldContainerProps): JSX.Element => {
const { errors, isSubmitting, isValid } = useFormState({ name: schema._id })
const error: FieldError | undefined = get(errors, errorKey ?? schema._id)
+ let title = schema.title
+
+ const titleTranslations = schema.titleTranslations ?? []
+ // check if there are any title translations for the selected language
+ const titleTranslationIdx = titleTranslations.findIndex(
+ (titleTranslation) => {
+ return titleTranslation.language === selectedLanguage
+ },
+ )
+
+ // If there are title translations for the selected language, use the translation.
+ // If not default it to English.
+ if (titleTranslationIdx !== -1) {
+ title = titleTranslations[titleTranslationIdx].translation
+ }
+
+ let description = schema.description
+
+ const descriptionTranslations = schema.descriptionTranslations ?? []
+ // check if there are any description translations for the selected language
+ const descriptionTranslationIdx = descriptionTranslations.findIndex(
+ (descriptionTranslation) =>
+ descriptionTranslation.language === selectedLanguage,
+ )
+
+ // If there are description translations for the language, use the translation.
+ // If not default it to English.
+ if (descriptionTranslationIdx !== -1) {
+ description = descriptionTranslations[descriptionTranslationIdx].translation
+ }
+
return (
- {schema.title}
+ {title}
{showMyInfoBadge && (
diff --git a/frontend/src/templates/Field/HomeNo/HomeNoField.tsx b/frontend/src/templates/Field/HomeNo/HomeNoField.tsx
index e20454a08c..4e7e3d22ab 100644
--- a/frontend/src/templates/Field/HomeNo/HomeNoField.tsx
+++ b/frontend/src/templates/Field/HomeNo/HomeNoField.tsx
@@ -16,6 +16,7 @@ export interface HomeNoFieldProps extends BaseFieldProps {
export const HomeNoField = ({
schema,
disableRequiredValidation,
+ selectedLanguage: language,
}: HomeNoFieldProps): JSX.Element => {
const validationRules = useMemo(
() => createHomeNoValidationRules(schema, disableRequiredValidation),
@@ -25,7 +26,7 @@ export const HomeNoField = ({
const { control } = useFormContext()
return (
-
+
{
const validationRules = useMemo(
() => createTextValidationRules(schema, disableRequiredValidation),
@@ -27,7 +30,7 @@ export const LongTextField = ({
const { register } = useFormContext()
return (
-
+