diff --git a/frontend/src/components/Field/YesNo/YesNo.tsx b/frontend/src/components/Field/YesNo/YesNo.tsx index e283770e00..c02d906e15 100644 --- a/frontend/src/components/Field/YesNo/YesNo.tsx +++ b/frontend/src/components/Field/YesNo/YesNo.tsx @@ -9,27 +9,12 @@ import { } from '@chakra-ui/react' import pick from 'lodash/pick' -import { Language } from '~shared/types' - import { FieldColorScheme } from '~theme/foundations/colours' import { YesNoOption } from './YesNoOption' export type YesNoOptionValue = 'Yes' | 'No' -// TODO: port to i18next -type YesNoTranslations = { - Yes: string - No: string -} - -const YES_NO_TRANSLATIONS: Record = { - [Language.ENGLISH]: { Yes: 'Yes', No: 'No' }, - [Language.CHINESE]: { Yes: '是', No: '否' }, - [Language.MALAY]: { Yes: 'Ya', No: 'Tidak' }, - [Language.TAMIL]: { Yes: 'ஆம்', No: 'இல்லை' }, -} - export interface YesNoProps { /** * Whether YesNo component is disabled. @@ -68,7 +53,7 @@ export const YesNo = forwardRef( ({ colorScheme, ...props }, ref) => { const formControlProps = useFormControlProps(props) const { getRootProps, getRadioProps, onChange } = useRadioGroup(props) - const { t, i18n } = useTranslation() + const { t } = useTranslation() const groupProps = getRootProps() const [noProps, yesProps] = useMemo(() => { @@ -95,10 +80,6 @@ export const YesNo = forwardRef( return [noRadioProps, yesRadioProps] }, [formControlProps, getRadioProps, props.name]) - const selectedLanguage = i18n.language as Language - const yesLabel = YES_NO_TRANSLATIONS[selectedLanguage].Yes - const noLabel = YES_NO_TRANSLATIONS[selectedLanguage].No - return ( ( {...noProps} onChange={(value) => onChange(value as YesNoOptionValue)} leftIcon={BiX} - label={noLabel ?? t('features.adminForm.sidebar.fields.yesNo.no')} + label={t('features.adminForm.sidebar.fields.yesNo.no')} // Ref is set here for tracking current value, and also so any errors // can focus this input. ref={ref} @@ -119,7 +100,7 @@ export const YesNo = forwardRef( {...yesProps} onChange={(value) => onChange(value as YesNoOptionValue)} leftIcon={BiCheck} - label={yesLabel ?? t('features.adminForm.sidebar.fields.yesNo.yes')} + label={t('features.adminForm.sidebar.fields.yesNo.yes')} title={props.title} /> diff --git a/frontend/src/constants/validation.ts b/frontend/src/constants/validation.ts index be6c551ac4..0bcda64011 100644 --- a/frontend/src/constants/validation.ts +++ b/frontend/src/constants/validation.ts @@ -1,17 +1,6 @@ -import { Language } from '~shared/types' - export const REQUIRED_ERROR = 'This field is required' export const INVALID_EMAIL_ERROR = 'Please enter a valid email' -export const INVALID_EMAIL_DOMAIN_ERROR: Record = { - [Language.ENGLISH]: - 'The entered email does not belong to an allowed email domain', - [Language.CHINESE]: '输入的电子邮箱不在允许域名之列', - [Language.MALAY]: - 'E-mel yang dimasukkan bukan milik domain e-mel yang dibenarkan', - [Language.TAMIL]: - 'உள்ளிடப்பட்ட மின்னஞ்சல் அனுமதிக்கப்பட்ட மின்னஞ்சலுக்குச் சொந்தமானதல்ல', -} export const INVALID_DROPDOWN_OPTION_ERROR = 'Entered value is not a valid dropdown option' diff --git a/frontend/src/features/verifiable-fields/Email/VerifiableEmailField.tsx b/frontend/src/features/verifiable-fields/Email/VerifiableEmailField.tsx index 48ae702302..ab41551262 100644 --- a/frontend/src/features/verifiable-fields/Email/VerifiableEmailField.tsx +++ b/frontend/src/features/verifiable-fields/Email/VerifiableEmailField.tsx @@ -2,8 +2,6 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { Box, VisuallyHidden } from '@chakra-ui/react' -import { Language } from '~shared/types' - import { baseEmailValidationFn } from '~utils/fieldValidation' import { EmailFieldInput, EmailFieldProps } from '~templates/Field/Email' import { EmailFieldSchema } from '~templates/Field/types' @@ -71,10 +69,13 @@ export const VerifiableEmailField = ({ schema, ...props }: VerifiableEmailFieldProps) => { - const { i18n } = useTranslation() + const { t } = useTranslation() const validateInputForVfn = baseEmailValidationFn({ schema, - selectedLanguage: i18n.language as Language, + validationErrorMessages: t( + 'features.adminForm.sidebar.fields.email.validation', + { allowObjects: true }, + ), }) return ( = { + yesNo: { + yes: 'Ya', + no: 'Tidak', + }, + email: { + validation: { + domainDisallowed: + 'E-mel yang dimasukkan bukan milik domain e-mel yang dibenarkan', + }, + }, +} diff --git a/frontend/src/i18n/locales/features/admin-form/sidebar/fields/ta-sg.ts b/frontend/src/i18n/locales/features/admin-form/sidebar/fields/ta-sg.ts new file mode 100644 index 0000000000..dab4e24e63 --- /dev/null +++ b/frontend/src/i18n/locales/features/admin-form/sidebar/fields/ta-sg.ts @@ -0,0 +1,16 @@ +import { DeepPartial } from 'ts-essentials' + +import { Fields } from '.' + +export const taSG: DeepPartial = { + yesNo: { + yes: 'ஆம்', + no: 'இல்லை', + }, + email: { + validation: { + domainDisallowed: + 'உள்ளிடப்பட்ட மின்னஞ்சல் அனுமதிக்கப்பட்ட மின்னஞ்சலுக்குச் சொந்தமானதல்ல', + }, + }, +} diff --git a/frontend/src/i18n/locales/features/admin-form/sidebar/fields/zh-sg.ts b/frontend/src/i18n/locales/features/admin-form/sidebar/fields/zh-sg.ts new file mode 100644 index 0000000000..ef0d979729 --- /dev/null +++ b/frontend/src/i18n/locales/features/admin-form/sidebar/fields/zh-sg.ts @@ -0,0 +1,15 @@ +import { DeepPartial } from 'ts-essentials' + +import { Fields } from '.' + +export const zhSG: DeepPartial = { + yesNo: { + yes: '是', + no: '否', + }, + email: { + validation: { + domainDisallowed: '输入的电子邮箱不在允许域名之列', + }, + }, +} diff --git a/frontend/src/i18n/locales/features/admin-form/sidebar/index.ts b/frontend/src/i18n/locales/features/admin-form/sidebar/index.ts index edcff4588e..76b590d3e4 100644 --- a/frontend/src/i18n/locales/features/admin-form/sidebar/index.ts +++ b/frontend/src/i18n/locales/features/admin-form/sidebar/index.ts @@ -2,4 +2,7 @@ export * from './en-sg' export { type Fields } from './fields' export { type HeaderAndInstructions } from './header-and-instructions' export { type Logic } from './logic' +export * from './ms-sg' +export * from './ta-sg' export { type ThankYou } from './thank-you' +export * from './zh-sg' diff --git a/frontend/src/i18n/locales/features/admin-form/sidebar/ms-sg.ts b/frontend/src/i18n/locales/features/admin-form/sidebar/ms-sg.ts new file mode 100644 index 0000000000..a7aa672949 --- /dev/null +++ b/frontend/src/i18n/locales/features/admin-form/sidebar/ms-sg.ts @@ -0,0 +1,5 @@ +import { msSG as fields } from './fields' + +export const msSG = { + fields, +} diff --git a/frontend/src/i18n/locales/features/admin-form/sidebar/ta-sg.ts b/frontend/src/i18n/locales/features/admin-form/sidebar/ta-sg.ts new file mode 100644 index 0000000000..a9636a62ea --- /dev/null +++ b/frontend/src/i18n/locales/features/admin-form/sidebar/ta-sg.ts @@ -0,0 +1,5 @@ +import { taSG as fields } from './fields' + +export const taSG = { + fields, +} diff --git a/frontend/src/i18n/locales/features/admin-form/sidebar/zh-sg.ts b/frontend/src/i18n/locales/features/admin-form/sidebar/zh-sg.ts new file mode 100644 index 0000000000..4be4b231e8 --- /dev/null +++ b/frontend/src/i18n/locales/features/admin-form/sidebar/zh-sg.ts @@ -0,0 +1,5 @@ +import { zhSG as fields } from './fields' + +export const zhSG = { + fields, +} diff --git a/frontend/src/i18n/locales/features/admin-form/ta-sg.ts b/frontend/src/i18n/locales/features/admin-form/ta-sg.ts new file mode 100644 index 0000000000..035bab847f --- /dev/null +++ b/frontend/src/i18n/locales/features/admin-form/ta-sg.ts @@ -0,0 +1,5 @@ +import { taSG as sidebar } from './sidebar' + +export const taSG = { + sidebar, +} diff --git a/frontend/src/i18n/locales/features/admin-form/zh-sg.ts b/frontend/src/i18n/locales/features/admin-form/zh-sg.ts new file mode 100644 index 0000000000..cc563a6345 --- /dev/null +++ b/frontend/src/i18n/locales/features/admin-form/zh-sg.ts @@ -0,0 +1,5 @@ +import { zhSG as sidebar } from './sidebar' + +export const zhSG = { + sidebar, +} diff --git a/frontend/src/i18n/locales/index.ts b/frontend/src/i18n/locales/index.ts index d075f0d6a6..0bd6e3587b 100644 --- a/frontend/src/i18n/locales/index.ts +++ b/frontend/src/i18n/locales/index.ts @@ -1,9 +1,15 @@ import { ResourceLanguage } from 'i18next' +import { Language } from '~shared/types' + import { enSG } from './en-sg' +import { msSG } from './ms-sg' +import { taSG } from './ta-sg' import { zhSG } from './zh-sg' export const locales = { - 'en-SG': enSG as unknown as ResourceLanguage, - 'zh-SG': zhSG as unknown as ResourceLanguage, + [Language.ENGLISH]: enSG as unknown as ResourceLanguage, + [Language.CHINESE]: zhSG as unknown as ResourceLanguage, + [Language.MALAY]: msSG as unknown as ResourceLanguage, + [Language.TAMIL]: taSG as unknown as ResourceLanguage, } diff --git a/frontend/src/i18n/locales/ms-sg.ts b/frontend/src/i18n/locales/ms-sg.ts new file mode 100644 index 0000000000..657eb09885 --- /dev/null +++ b/frontend/src/i18n/locales/ms-sg.ts @@ -0,0 +1,12 @@ +import { PartialDeep } from 'type-fest' + +import { msSG as adminForm } from './features/admin-form' +import Translation from './types' + +export const msSG: PartialDeep = { + translation: { + features: { + adminForm, + }, + }, +} diff --git a/frontend/src/i18n/locales/ta-sg.ts b/frontend/src/i18n/locales/ta-sg.ts new file mode 100644 index 0000000000..60a8ed46e1 --- /dev/null +++ b/frontend/src/i18n/locales/ta-sg.ts @@ -0,0 +1,12 @@ +import { PartialDeep } from 'type-fest' + +import { taSG as adminForm } from './features/admin-form' +import Translation from './types' + +export const taSG: PartialDeep = { + translation: { + features: { + adminForm, + }, + }, +} diff --git a/frontend/src/i18n/locales/zh-sg.ts b/frontend/src/i18n/locales/zh-sg.ts index 8c8c5708ee..725920b67d 100644 --- a/frontend/src/i18n/locales/zh-sg.ts +++ b/frontend/src/i18n/locales/zh-sg.ts @@ -1,11 +1,13 @@ import { PartialDeep } from 'type-fest' +import { zhSG as adminForm } from './features/admin-form' import { zhSG as login } from './features/login' import Translation from './types' export const zhSG: PartialDeep = { translation: { features: { + adminForm, login, }, }, diff --git a/frontend/src/templates/Field/Email/EmailField.test.tsx b/frontend/src/templates/Field/Email/EmailField.test.tsx index dfdc4db464..2252d04584 100644 --- a/frontend/src/templates/Field/Email/EmailField.test.tsx +++ b/frontend/src/templates/Field/Email/EmailField.test.tsx @@ -2,13 +2,9 @@ import { composeStories } from '@storybook/react' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { Language } from '~shared/types' +import { enSG } from '~/i18n/locales/features/admin-form/sidebar/fields' -import { - INVALID_EMAIL_DOMAIN_ERROR, - INVALID_EMAIL_ERROR, - REQUIRED_ERROR, -} from '~constants/validation' +import { INVALID_EMAIL_ERROR, REQUIRED_ERROR } from '~constants/validation' import * as stories from './EmailField.stories' @@ -149,7 +145,7 @@ describe('email validation', () => { // Assert // Should show error message. - const errorMessage = INVALID_EMAIL_DOMAIN_ERROR[Language.ENGLISH] + const errorMessage = enSG.email.validation.domainDisallowed expect(screen.getByText(errorMessage)).not.toBeNull() }) }) diff --git a/frontend/src/templates/Field/Email/EmailFieldInput.tsx b/frontend/src/templates/Field/Email/EmailFieldInput.tsx index 224674dfc9..5dcce6cd02 100644 --- a/frontend/src/templates/Field/Email/EmailFieldInput.tsx +++ b/frontend/src/templates/Field/Email/EmailFieldInput.tsx @@ -33,16 +33,19 @@ export const EmailFieldInput = ({ handleInputChange, inputProps = {}, }: EmailFieldInputProps): JSX.Element => { - const { i18n } = useTranslation() - const selectedLanguage = i18n.language as Language + const { t } = useTranslation() + const validationErrorMessages = t( + 'features.adminForm.sidebar.fields.email.validation', + { allowObjects: true }, + ) const validationRules = useMemo( () => createEmailValidationRules( schema, disableRequiredValidation, - selectedLanguage, + validationErrorMessages, ), - [schema, disableRequiredValidation, selectedLanguage], + [schema, disableRequiredValidation, validationErrorMessages], ) const { control } = useFormContext() diff --git a/frontend/src/utils/fieldValidation.ts b/frontend/src/utils/fieldValidation.ts index 8b6032d09d..8b29096763 100644 --- a/frontend/src/utils/fieldValidation.ts +++ b/frontend/src/utils/fieldValidation.ts @@ -9,7 +9,6 @@ import simplur from 'simplur' import validator from 'validator' import { DATE_PARSE_FORMAT } from '~shared/constants/dates' -import { Language } from '~shared/types' import { AttachmentFieldBase, BasicField, @@ -42,10 +41,11 @@ import { } from '~shared/utils/phone-num-validation' import { isUenValid } from '~shared/utils/uen-validation' +import { Fields } from '~/i18n/locales/features' + import { INVALID_COUNTRY_REGION_OPTION_ERROR, INVALID_DROPDOWN_OPTION_ERROR, - INVALID_EMAIL_DOMAIN_ERROR, INVALID_EMAIL_ERROR, REQUIRED_ERROR, } from '~constants/validation' @@ -65,6 +65,8 @@ import { } from './date' import { formatNumberToLocaleString } from './stringFormat' +type EmailValidationErrorMessages = Fields['email']['validation'] + // Omit unused props type MinimumFieldValidationProps = Omit< T, @@ -85,7 +87,7 @@ type ValidationRuleFn = ( type ValidationRuleFnEmailAndMobile = ( schema: MinimumFieldValidationPropsEmailAndMobile, disableRequiredValidation?: boolean, - selectedLanguage?: Language, + validationErrorMessages?: EmailValidationErrorMessages, ) => RegisterOptions const requiredSingleAnswerValidationFn = @@ -554,11 +556,17 @@ export const createRadioValidationRules: ValidationRuleFn = ( export const createEmailValidationRules: ValidationRuleFnEmailAndMobile< EmailFieldBase -> = (schema, disableRequiredValidation, selectedLanguage): RegisterOptions => { +> = ( + schema, + disableRequiredValidation, + validationErrorMessages, +): RegisterOptions => { return { validate: { baseValidations: (val?: VerifiableFieldValues) => { - return baseEmailValidationFn({ schema, selectedLanguage })(val?.value) + return baseEmailValidationFn({ schema, validationErrorMessages })( + val?.value, + ) }, ...createBaseVfnFieldValidationRules(schema, disableRequiredValidation) .validate, @@ -573,10 +581,10 @@ export const createEmailValidationRules: ValidationRuleFnEmailAndMobile< export const baseEmailValidationFn = ({ schema, - selectedLanguage = Language.ENGLISH, + validationErrorMessages = { domainDisallowed: 'Domain disallowed' }, }: { schema: MinimumFieldValidationProps - selectedLanguage?: Language + validationErrorMessages?: EmailValidationErrorMessages }) => (inputValue?: string) => { if (!inputValue) return true @@ -591,7 +599,7 @@ export const baseEmailValidationFn = if (allowedDomains.size !== 0) { const domainInValue = trimmedInputValue.split('@')[1].toLowerCase() if (domainInValue && !allowedDomains.has(`@${domainInValue}`)) { - return INVALID_EMAIL_DOMAIN_ERROR[selectedLanguage] + return validationErrorMessages.domainDisallowed } }