diff --git a/.eslintrc.js b/.eslintrc.js index 94648b7cda..674109fd1c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -74,6 +74,18 @@ module.exports = { 'error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, ], + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['*/i18n', '**/lib/i18n'], + message: + 'Do not import the i18n singleton. Use `const { t } = useTranslation()`, or accept `t: TFunction` in helpers outside a component. Only getServerSideProps, where hooks cannot run, and tests are exempt.', + }, + ], + }, + ], 'no-restricted-syntax': [ 'error', { @@ -135,5 +147,25 @@ module.exports = { 'jsx-a11y/no-static-element-interactions': 'off', }, }, + // These two blocks lift every no-restricted-imports pattern, not just the + // i18n one. A second restriction added above will need re-stating here. + { + files: ['pages/**'], + rules: { + 'no-restricted-imports': 'off', + }, + }, + { + files: [ + '*.test.ts', + '*.test.tsx', + '__tests__/**', + 'testUtils.tsx', + '*TestWrapper.tsx', + ], + rules: { + 'no-restricted-imports': 'off', + }, + }, ], }; diff --git a/__tests__/eslintrules/eslintI18nImportRules.test.ts b/__tests__/eslintrules/eslintI18nImportRules.test.ts new file mode 100644 index 0000000000..e1e6ff1ec5 --- /dev/null +++ b/__tests__/eslintrules/eslintI18nImportRules.test.ts @@ -0,0 +1,67 @@ +import eslintConfig from '../../.eslintrc'; +import { importRuleFor, lintImportSnippet } from './restrictedSyntaxHarness'; + +const i18nRule = importRuleFor('i18n'); + +// no-restricted-imports prefixes the configured message with its own sentence, +// unlike no-restricted-syntax which reports the message verbatim. +const restricted = (specifier: string): string => + `'${specifier}' import is restricted from being used by a pattern. ${i18nRule.message}`; + +const lintImport = (code: string): string[] => + lintImportSnippet(code).map((message) => message.message); + +const exemptedPaths = (eslintConfig.overrides ?? []) + .filter((override) => override.rules?.['no-restricted-imports'] === 'off') + .flatMap((override) => override.files); + +describe('i18n singleton no-restricted-imports rule', () => { + it('flags the singleton imported by its src path', () => { + expect(lintImport("import i18n from 'src/lib/i18n';")).toEqual([ + restricted('src/lib/i18n'), + ]); + }); + + it('flags the singleton imported from a parent directory', () => { + expect(lintImport("import i18n from '../i18n';")).toEqual([ + restricted('../i18n'), + ]); + }); + + it('flags the singleton imported from a sibling directory', () => { + expect(lintImport("import i18n from './i18n';")).toEqual([ + restricted('./i18n'), + ]); + }); + + it('flags a named import from the singleton', () => { + expect(lintImport("import { t } from 'src/lib/i18n';")).toEqual([ + restricted('src/lib/i18n'), + ]); + }); + + it('accepts the TFunction type from i18next', () => { + expect(lintImport("import { TFunction } from 'i18next';")).toEqual([]); + }); + + it('accepts useTranslation from react-i18next', () => { + expect( + lintImport("import { useTranslation } from 'react-i18next';"), + ).toEqual([]); + }); + + it('accepts an unrelated src import', () => { + expect(lintImport("import theme from 'src/theme';")).toEqual([]); + }); + + it('exempts only pages and test helpers', () => { + expect(exemptedPaths).toEqual([ + 'pages/**', + '*.test.ts', + '*.test.tsx', + '__tests__/**', + 'testUtils.tsx', + '*TestWrapper.tsx', + ]); + }); +}); diff --git a/__tests__/eslintrules/restrictedSyntaxHarness.ts b/__tests__/eslintrules/restrictedSyntaxHarness.ts index 2775964473..a5c2a740c0 100644 --- a/__tests__/eslintrules/restrictedSyntaxHarness.ts +++ b/__tests__/eslintrules/restrictedSyntaxHarness.ts @@ -8,6 +8,10 @@ interface RestrictedSyntaxOption { message: string; } +interface RestrictedImportsOption { + patterns: { group: string[]; message: string }[]; +} + export interface LintMessage { ruleId: string | null; message: string; @@ -43,18 +47,46 @@ export const ruleFor = (selectorFragment: string): RestrictedSyntaxOption => { return option; }; -export const lintSnippet = (code: string): LintMessage[] => { +// Assert against the real rule +export const restrictedImports = eslintConfig.rules[ + 'no-restricted-imports' +] as ['error', RestrictedImportsOption]; +const [, { patterns }] = restrictedImports; + +export const importRuleFor = ( + groupFragment: string, +): RestrictedImportsOption['patterns'][number] => { + const pattern = patterns.find(({ group }) => + group.some((glob) => glob.includes(groupFragment)), + ); + if (!pattern) { + throw new Error( + `No no-restricted-imports rule has a group containing "${groupFragment}"`, + ); + } + return pattern; +}; + +const lintWith = ( + code: string, + ruleId: string, + ruleConfig: Linter.RuleEntry, +): LintMessage[] => { const messages: LintMessage[] = linter.verify(code, { parser, parserOptions, - rules: { 'no-restricted-syntax': restrictedSyntax }, + rules: { [ruleId]: ruleConfig }, }); const fatal = messages.find((message) => message.fatal); if (fatal) { throw new Error(`Probe failed to parse: ${fatal.message}`); } - return messages.filter( - (message) => message.ruleId === 'no-restricted-syntax', - ); + return messages.filter((message) => message.ruleId === ruleId); }; + +export const lintSnippet = (code: string): LintMessage[] => + lintWith(code, 'no-restricted-syntax', restrictedSyntax); + +export const lintImportSnippet = (code: string): LintMessage[] => + lintWith(code, 'no-restricted-imports', restrictedImports); diff --git a/pages/accountLists/[accountListId]/settings/preferences.page.tsx b/pages/accountLists/[accountListId]/settings/preferences.page.tsx index 974d80faf6..0dabdd6ce2 100644 --- a/pages/accountLists/[accountListId]/settings/preferences.page.tsx +++ b/pages/accountLists/[accountListId]/settings/preferences.page.tsx @@ -60,7 +60,7 @@ const Preferences: React.FC = () => { ? (query.selectedTab as PreferenceAccordion) : null, ); - const countries = getCountries(); + const countries = getCountries(t); const timeZones = useGetTimezones(); const [_, setSetupPosition] = useUserPreference({ diff --git a/src/components/Announcements/Announcements.tsx b/src/components/Announcements/Announcements.tsx index 8558770697..c8b73ad3ec 100644 --- a/src/components/Announcements/Announcements.tsx +++ b/src/components/Announcements/Announcements.tsx @@ -8,6 +8,7 @@ import React, { } from 'react'; import { getApolloContext } from '@apollo/client'; import { DateTime } from 'luxon'; +import { useTranslation } from 'react-i18next'; import { AlertBanner } from 'src/components/Shared/alertBanner/AlertBanner'; import { ActionEnum, @@ -17,7 +18,6 @@ import { import { useOptionalAccountListId } from 'src/hooks/useAccountListId'; import { useContactPartnershipStatuses } from 'src/hooks/useContactPartnershipStatuses'; import { dispatch } from 'src/lib/analytics'; -import i18n from 'src/lib/i18n'; import { DynamicAddAppealModal } from '../Tool/Appeal/Modals/AddAppealModal/DynamicAddAppealModal'; import { DynamicAnnouncementBanner } from './AnnouncementBanner/DynamicAnnouncementBanner'; import { DynamicAnnouncementModal } from './AnnouncementModal/DynamicAnnouncementModal'; @@ -42,6 +42,7 @@ export const Announcements: React.FC = () => { }; const Announcement: React.FC = () => { + const { t } = useTranslation(); const { push } = useRouter(); const accountListId = useOptionalAccountListId(); const [showAppealModal, setShowAppealModal] = useState(false); @@ -127,7 +128,7 @@ const Announcement: React.FC = () => { [accountListId, announcement], ); - const appealName = `${DateTime.local().year} ${i18n.t('End of Year Ask')}`; + const appealName = `${DateTime.local().year} ${t('End of Year Ask')}`; const appealStatuses = useMemo( () => [ { diff --git a/src/components/Contacts/ContactDetails/ContactDetailsTab/Other/ContactDetailsOther.tsx b/src/components/Contacts/ContactDetails/ContactDetailsTab/Other/ContactDetailsOther.tsx index 678adbd340..aea9aebe4a 100644 --- a/src/components/Contacts/ContactDetails/ContactDetailsTab/Other/ContactDetailsOther.tsx +++ b/src/components/Contacts/ContactDetails/ContactDetailsTab/Other/ContactDetailsOther.tsx @@ -2,12 +2,12 @@ import NextLink from 'next/link'; import React from 'react'; import { Box, Link, Typography } from '@mui/material'; import { styled } from '@mui/material/styles'; +import { TFunction } from 'i18next'; import { useTranslation } from 'react-i18next'; import { useApiConstants } from 'src/components/Constants/UseApiConstants'; import { useContactPanel } from 'src/components/Shared/ContactPanelProvider/ContactPanelProvider'; import { PreferredContactMethodEnum } from 'src/graphql/types.generated'; import { formatLanguage } from 'src/lib/data/languages'; -import i18n from 'src/lib/i18n'; import { ContactOtherFragment } from './ContactOther.generated'; const ContactOtherContainer = styled(Box)(({ theme }) => ({ @@ -33,24 +33,27 @@ interface ContactDetailsOtherProp { contact: ContactOtherFragment; } -export const localizedContactMethod = (method?: string | null): string => { +export const localizedContactMethod = ( + method: string | null | undefined, + t: TFunction, +): string => { switch (method) { case PreferredContactMethodEnum.Sms: - return i18n.t('SMS'); + return t('SMS'); case PreferredContactMethodEnum.PhoneCall: - return i18n.t('Phone Call'); + return t('Phone Call'); case PreferredContactMethodEnum.Email: - return i18n.t('Email'); + return t('Email'); case PreferredContactMethodEnum.Facebook: - return i18n.t('Facebook'); + return t('Facebook'); case PreferredContactMethodEnum.Instagram: - return i18n.t('Instagram'); + return t('Instagram'); case PreferredContactMethodEnum.WeChat: - return i18n.t('WeChat'); + return t('WeChat'); case PreferredContactMethodEnum.WhatsApp: - return i18n.t('WhatsApp'); + return t('WhatsApp'); default: - return i18n.t('N/A'); + return t('N/A'); } }; @@ -123,7 +126,7 @@ export const ContactDetailsOther: React.FC = ({ {t('Preferred Contact Method')} - {localizedContactMethod(preferredContactMethod)} + {localizedContactMethod(preferredContactMethod, t)} diff --git a/src/components/Contacts/ContactDetails/ContactDetailsTab/Other/EditContactOtherModal/EditContactOtherModal.tsx b/src/components/Contacts/ContactDetails/ContactDetailsTab/Other/EditContactOtherModal/EditContactOtherModal.tsx index 4c60c5b8b2..9e7c897f9d 100644 --- a/src/components/Contacts/ContactDetails/ContactDetailsTab/Other/EditContactOtherModal/EditContactOtherModal.tsx +++ b/src/components/Contacts/ContactDetails/ContactDetailsTab/Other/EditContactOtherModal/EditContactOtherModal.tsx @@ -374,8 +374,10 @@ export const EditContactOtherModal: React.FC = ({ > {Object.values(PreferredContactMethodEnum).map( (value) => { - const contactMethod = - localizedContactMethod(value); + const contactMethod = localizedContactMethod( + value, + t, + ); return ( ; - const ContactPartnerAccountsContainer = styled(Box)(({ theme }) => ({ margin: theme.spacing(1, 1, 1, 5), })); @@ -72,6 +65,16 @@ export const ContactDetailsPartnerAccounts: React.FC< variables: { accountListId }, }); const { t } = useTranslation(); + + const newPartnerAccountSchema = useMemo( + () => + yup.object({ + accountNumber: yup.string().required(t('Account Number is required')), + }), + [t], + ); + + type Attributes = yup.InferType; const { enqueueSnackbar } = useSnackbar(); const deleteContactDonorAccount = async (id: string) => { diff --git a/src/components/Dashboard/ThisWeek/NewsletterMenu/MenuItems/LogNewsLetter/LogNewsletter.tsx b/src/components/Dashboard/ThisWeek/NewsletterMenu/MenuItems/LogNewsLetter/LogNewsletter.tsx index 4df18d141d..617bacd157 100644 --- a/src/components/Dashboard/ThisWeek/NewsletterMenu/MenuItems/LogNewsLetter/LogNewsletter.tsx +++ b/src/components/Dashboard/ThisWeek/NewsletterMenu/MenuItems/LogNewsLetter/LogNewsletter.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement, useState } from 'react'; +import React, { ReactElement, useMemo, useState } from 'react'; import CloseIcon from '@mui/icons-material/Close'; import { DialogActions, @@ -28,7 +28,6 @@ import { LogTextField, } from 'src/components/Shared/styledComponents/LogStyling'; import { ActivityTypeEnum } from 'src/graphql/types.generated'; -import i18n from 'src/lib/i18n'; import { nullableDateTime } from 'src/lib/yupHelpers'; import { useCreateTasksMutation } from '../../../../../Task/Modal/Form/TaskModal.generated'; import { CloseButton } from '../styledComponents/CloseButton'; @@ -50,23 +49,27 @@ const LogFormControlLabel = styled(FormControlLabel)(({ theme }) => ({ }, })); -const taskSchema = yup.object({ - activityType: yup - .mixed() - .oneOf([...Object.values(ActivityTypeEnum), 'BOTH' as const]) - .defined(), - completedAt: nullableDateTime(), - subject: yup.string().required(i18n.t('Subject is required')), -}); - -type Attributes = yup.InferType; - const LogNewsletter = ({ accountListId, handleClose, }: Props): ReactElement => { const { t } = useTranslation(); + const taskSchema = useMemo( + () => + yup.object({ + activityType: yup + .mixed() + .oneOf([...Object.values(ActivityTypeEnum), 'BOTH' as const]) + .defined(), + completedAt: nullableDateTime(), + subject: yup.string().required(t('Subject is required')), + }), + [t], + ); + + type Attributes = yup.InferType; + const [commentBody, changeCommentBody] = useState(''); const [createTasks, { loading: creating }] = useCreateTasksMutation(); diff --git a/src/components/EditDonationModal/EditDonationModal.tsx b/src/components/EditDonationModal/EditDonationModal.tsx index d827391cf2..8af74b4a72 100644 --- a/src/components/EditDonationModal/EditDonationModal.tsx +++ b/src/components/EditDonationModal/EditDonationModal.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement, useState } from 'react'; +import React, { ReactElement, useMemo, useState } from 'react'; import { CircularProgress, DialogActions, @@ -27,7 +27,6 @@ import Modal from 'src/components/Shared/Modal/Modal'; import { FormFieldsGridContainer } from 'src/components/Task/Modal/Form/Container/FormFieldsGridContainer'; import { useAccountListId } from 'src/hooks/useAccountListId'; import { useFetchAllPages } from 'src/hooks/useFetchAllPages'; -import i18n from 'src/lib/i18n'; import { requiredDateTime } from 'src/lib/yupHelpers'; import { SmallLoadingSpinner } from '../Settings/Organization/LoadingSpinner'; import { CustomDateField } from '../Shared/DateTimePickers/CustomDateField'; @@ -45,30 +44,34 @@ interface EditDonationModalProps { handleClose: () => void; } -const donationSchema = yup.object({ - convertedAmount: yup - .number() - .typeError(i18n.t('Must be a number')) - .required(i18n.t('Amount is required')), - currency: yup.string().required(i18n.t('Currency is required')), - date: requiredDateTime(i18n.t('Date is required')), - donorAccountId: yup.string().required(i18n.t('Partner Account is required')), - designationAccountId: yup - .string() - .required(i18n.t('Designation Account is required')), - appealId: yup.string().optional(), - appealAmount: yup.number(), - memo: yup.string().optional(), -}); - -type Attributes = yup.InferType; - export const EditDonationModal: React.FC = ({ open, donation, handleClose, }) => { const { t } = useTranslation(); + + const donationSchema = useMemo( + () => + yup.object({ + convertedAmount: yup + .number() + .typeError(t('Must be a number')) + .required(t('Amount is required')), + currency: yup.string().required(t('Currency is required')), + date: requiredDateTime(t('Date is required')), + donorAccountId: yup.string().required(t('Partner Account is required')), + designationAccountId: yup + .string() + .required(t('Designation Account is required')), + appealId: yup.string().optional(), + appealAmount: yup.number(), + memo: yup.string().optional(), + }), + [t], + ); + + type Attributes = yup.InferType; const [removeDialogOpen, setRemoveDialogOpen] = useState(false); const accountListId = useAccountListId(); @@ -100,8 +103,8 @@ export const EditDonationModal: React.FC = ({ attributes: { id: donation.id, appealId: fields.appealId, - appealAmount: parseFloat(fields.appealAmount as unknown as string), - amount: parseFloat(fields.convertedAmount as unknown as string), + appealAmount: parseFloat(String(fields.appealAmount)), + amount: parseFloat(String(fields.convertedAmount)), currency: fields.currency, designationAccountId: fields.designationAccountId, donationDate: fields.date.toISODate(), diff --git a/src/components/HrTools/AdditionalSalaryRequest/AboutForm/AboutForm.tsx b/src/components/HrTools/AdditionalSalaryRequest/AboutForm/AboutForm.tsx index e545c40b37..c8a7de7da7 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/AboutForm/AboutForm.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/AboutForm/AboutForm.tsx @@ -20,7 +20,7 @@ export const AboutForm: React.FC = () => { const individualCap = latestRequest?.calculations.currentSalaryCap ?? 0; return ( - + You can use this form to electronically submit additional salary diff --git a/src/components/HrTools/AdditionalSalaryRequest/Shared/AdditionalSalaryRequestContext.test.tsx b/src/components/HrTools/AdditionalSalaryRequest/Shared/AdditionalSalaryRequestContext.test.tsx index cde80e2ea2..2ef02f84bf 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/Shared/AdditionalSalaryRequestContext.test.tsx +++ b/src/components/HrTools/AdditionalSalaryRequest/Shared/AdditionalSalaryRequestContext.test.tsx @@ -7,6 +7,7 @@ import { SnackbarProvider } from 'notistack'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import { PageEnum } from 'src/components/HrTools/Shared/CalculationReports/Shared/sharedTypes'; +import i18n from 'src/lib/i18n'; import theme from 'src/theme'; import { HcmQuery } from '../../Shared/HcmData/Hcm.generated'; import { AdditionalSalaryRequestTestWrapper } from '../AdditionalSalaryRequestTestWrapper'; @@ -54,7 +55,7 @@ const TestComponent: React.FC = () => { return (
-

{getHeader(currentIndex)}

+

{getHeader(currentIndex, i18n.t)}

Drawer: {isDrawerOpen ? 'open' : 'closed'}
diff --git a/src/components/HrTools/AdditionalSalaryRequest/Shared/Helper/getHeader.test.ts b/src/components/HrTools/AdditionalSalaryRequest/Shared/Helper/getHeader.test.ts index b8bdeb4e70..a16082ca9d 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/Shared/Helper/getHeader.test.ts +++ b/src/components/HrTools/AdditionalSalaryRequest/Shared/Helper/getHeader.test.ts @@ -1,13 +1,14 @@ +import i18n from 'src/lib/i18n'; import { getHeader } from './getHeader'; describe('getHeader', () => { it('returns the text About this Form with step AboutForm', () => { - expect(getHeader(0)).toBe('About this Form'); + expect(getHeader(0, i18n.t)).toBe('About this Form'); }); it('returns the text Complete the Form with step CompleteForm', () => { - expect(getHeader(1)).toBe('Complete the Form'); + expect(getHeader(1, i18n.t)).toBe('Complete the Form'); }); it('returns the text Receipt with step Receipt', () => { - expect(getHeader(2)).toBe('Receipt'); + expect(getHeader(2, i18n.t)).toBe('Receipt'); }); }); diff --git a/src/components/HrTools/AdditionalSalaryRequest/Shared/Helper/getHeader.ts b/src/components/HrTools/AdditionalSalaryRequest/Shared/Helper/getHeader.ts index eae83cbecb..38e4e63281 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/Shared/Helper/getHeader.ts +++ b/src/components/HrTools/AdditionalSalaryRequest/Shared/Helper/getHeader.ts @@ -1,13 +1,13 @@ -import i18n from 'src/lib/i18n'; +import { TFunction } from 'i18next'; -export const getHeader = (step: number): string => { +export const getHeader = (step: number, t: TFunction): string => { switch (step) { case 0: - return i18n.t('About this Form'); + return t('About this Form'); case 1: - return i18n.t('Complete the Form'); + return t('Complete the Form'); case 2: - return i18n.t('Receipt'); + return t('Receipt'); default: return ''; } diff --git a/src/components/HrTools/AdditionalSalaryRequest/Shared/useAdditionalSalaryRequestForm.ts b/src/components/HrTools/AdditionalSalaryRequest/Shared/useAdditionalSalaryRequestForm.ts index d704ade127..0f54ac7b4e 100644 --- a/src/components/HrTools/AdditionalSalaryRequest/Shared/useAdditionalSalaryRequestForm.ts +++ b/src/components/HrTools/AdditionalSalaryRequest/Shared/useAdditionalSalaryRequestForm.ts @@ -8,7 +8,6 @@ import { ProgressiveApprovalTierReasonEnum, } from 'src/graphql/types.generated'; import { useLocale } from 'src/hooks/useLocale'; -import i18n from 'src/lib/i18n'; import { currencyFormat } from 'src/lib/intlFormat'; import { amount, phoneNumber } from 'src/lib/yupHelpers'; import { CompleteFormValues } from '../AdditionalSalaryRequest'; @@ -133,9 +132,7 @@ export const useAdditionalSalaryRequestForm = ( createCurrencyValidation(field.label, getMaxForField(field)), ]), ), - phoneNumber: phoneNumber(i18n.t).required( - i18n.t('Phone Number is required.'), - ), + phoneNumber: phoneNumber(t).required(t('Phone Number is required.')), emailAddress: yup .string() .required(t('Email address is required')) diff --git a/src/components/HrTools/MinisterHousingAllowance/RequestPage/RequestPage.tsx b/src/components/HrTools/MinisterHousingAllowance/RequestPage/RequestPage.tsx index e467b6e6a6..afe6886927 100644 --- a/src/components/HrTools/MinisterHousingAllowance/RequestPage/RequestPage.tsx +++ b/src/components/HrTools/MinisterHousingAllowance/RequestPage/RequestPage.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { Container, Stack } from '@mui/material'; import { Formik } from 'formik'; import { useTranslation } from 'react-i18next'; @@ -5,7 +6,6 @@ import * as yup from 'yup'; import Loading from 'src/components/Loading/Loading'; import { MhaRentOrOwnEnum, MhaStatusEnum } from 'src/graphql/types.generated'; import { useAccountListId } from 'src/hooks/useAccountListId'; -import i18n from 'src/lib/i18n'; import theme from 'src/theme'; import { PanelLayout } from '../../Shared/CalculationReports/PanelLayout/PanelLayout'; import { useIconPanelItems } from '../../Shared/CalculationReports/PanelLayout/useIconPanelItems'; @@ -30,15 +30,19 @@ export interface FormValues { rentOrOwn: MhaRentOrOwnEnum | undefined; } -const validationSchema = yup.object({ - rentOrOwn: yup - .string() - .required(i18n.t('Please select one of the options above to continue.')), -}); - export const RequestPage: React.FC = () => { const { t } = useTranslation(); + const validationSchema = useMemo( + () => + yup.object({ + rentOrOwn: yup + .string() + .required(t('Please select one of the options above to continue.')), + }), + [t], + ); + const { requestId, steps, diff --git a/src/components/HrTools/MinisterHousingAllowance/Steps/StepThree/Calculation.tsx b/src/components/HrTools/MinisterHousingAllowance/Steps/StepThree/Calculation.tsx index 50a6db06ab..5da90c13b6 100644 --- a/src/components/HrTools/MinisterHousingAllowance/Steps/StepThree/Calculation.tsx +++ b/src/components/HrTools/MinisterHousingAllowance/Steps/StepThree/Calculation.tsx @@ -15,7 +15,7 @@ import { import { Formik } from 'formik'; import { DateTime } from 'luxon'; import { useSnackbar } from 'notistack'; -import { Trans, useTranslation } from 'react-i18next'; +import { TFunction, Trans, useTranslation } from 'react-i18next'; import * as yup from 'yup'; import { DirectionButtons } from 'src/components/HrTools/Shared/CalculationReports/DirectionButtons/DirectionButtons'; import { PageEnum } from 'src/components/HrTools/Shared/CalculationReports/Shared/sharedTypes'; @@ -26,7 +26,6 @@ import { } from 'src/components/Reports/styledComponents'; import { MhaRentOrOwnEnum } from 'src/graphql/types.generated'; import { useLocale } from 'src/hooks/useLocale'; -import i18n from 'src/lib/i18n'; import { dateFormatShort } from 'src/lib/intlFormat'; import { phoneNumber } from 'src/lib/yupHelpers'; import { useSubmitMinistryHousingAllowanceRequestMutation } from '../../MinisterHousingAllowance.generated'; @@ -61,23 +60,24 @@ export interface CalculationFormValues { iUnderstandMhaPolicy?: boolean; } -const getValidationSchema = (rentOrOwn?: MhaRentOrOwnEnum) => { +const getValidationSchema = ( + rentOrOwn: MhaRentOrOwnEnum | undefined, + t: TFunction, +) => { const baseSchema = { mortgageOrRentPayment: yup.number().nullable(), furnitureCostsTwo: yup.number().nullable(), repairCosts: yup.number().nullable(), avgUtilityTwo: yup.number().nullable(), unexpectedExpenses: yup.number().nullable(), - phoneNumber: phoneNumber(i18n.t).required( - i18n.t('Phone Number is required.'), - ), + phoneNumber: phoneNumber(t).required(t('Phone Number is required.')), emailAddress: yup .string() - .email(i18n.t('Invalid email address.')) - .required(i18n.t('Email is required.')), + .email(t('Invalid email address.')) + .required(t('Email is required.')), iUnderstandMhaPolicy: yup .boolean() - .oneOf([true], i18n.t('This box must be checked to continue.')), + .oneOf([true], t('This box must be checked to continue.')), }; // extra fields for OWN @@ -195,7 +195,7 @@ export const Calculation: React.FC = ({ }) : t('approval soon'); - const schema = getValidationSchema(rentOrOwn); + const schema = getValidationSchema(rentOrOwn, t); if (loading) { return ; diff --git a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx index 983eafc781..5c7291037d 100644 --- a/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx +++ b/src/components/HrTools/SavingsFundTransfer/TransferModal/TransferModal.tsx @@ -28,7 +28,6 @@ import { } from 'src/components/Shared/Modal/ActionButtons/ActionButtons'; import Modal from 'src/components/Shared/Modal/Modal'; import { useLocale } from 'src/hooks/useLocale'; -import i18n from 'src/lib/i18n'; import { currencyFormat, dateFormat, @@ -87,22 +86,22 @@ const buildDefaultNote = ( }).trim(); }; -const pastDateMessage = (schedule: ScheduleEnum): string => +const pastDateMessage = (schedule: ScheduleEnum, t: TFunction): string => schedule === ScheduleEnum.OneTime - ? i18n.t('Transfer date cannot be in the past') - : i18n.t('Recurring transfers must start at least one day in the future'); + ? t('Transfer date cannot be in the past') + : t('Recurring transfers must start at least one day in the future'); -const transferSchema = (locale: string) => +const transferSchema = (locale: string, t: TFunction) => yup.object({ - transferFrom: yup.string().required(i18n.t('From account is required')), - transferTo: yup.string().required(i18n.t('To account is required')), + transferFrom: yup.string().required(t('From account is required')), + transferTo: yup.string().required(t('To account is required')), schedule: yup .mixed() .oneOf(Object.values(ScheduleEnum)) - .required(i18n.t('Schedule is required')), + .required(t('Schedule is required')), transferDate: yup .mixed() - .required(i18n.t('Transfer date is required')) + .required(t('Transfer date is required')) .test('start-date', function (value) { if (!value) { return false; @@ -122,7 +121,7 @@ const transferSchema = (locale: string) => } return this.createError({ - message: i18n.t('Transfer date cannot be earlier than {{date}}', { + message: t('Transfer date cannot be earlier than {{date}}', { date: dateFormat(baseline, locale), }), }); @@ -132,7 +131,7 @@ const transferSchema = (locale: string) => return true; } - return this.createError({ message: pastDateMessage(schedule) }); + return this.createError({ message: pastDateMessage(schedule, t) }); }), endDate: yup .mixed() @@ -142,7 +141,7 @@ const transferSchema = (locale: string) => then: (schema) => schema.test( 'end>start', - i18n.t('End date must be at least one day after the transfer date'), + t('End date must be at least one day after the transfer date'), function (end) { const start = this.parent.transferDate as DateTime | null; if (!end || !start) { @@ -155,11 +154,11 @@ const transferSchema = (locale: string) => }), amount: yup .number() - .required(i18n.t('Amount is required')) - .min(0.01, i18n.t('Amount must be at least $0.01')), + .required(t('Amount is required')) + .min(0.01, t('Amount must be at least $0.01')), note: yup.string().when('schedule', { is: ScheduleEnum.OneTime, - then: (schema) => schema.trim().required(i18n.t('Note is required')), + then: (schema) => schema.trim().required(t('Note is required')), }), }); interface TransferModalProps { @@ -289,7 +288,7 @@ export const TransferModal: React.FC = ({ isEditing: Boolean(data.transfer.id), originalStart: data.transfer.transferDate ?? null, }} - validationSchema={transferSchema(locale)} + validationSchema={transferSchema(locale, t)} onSubmit={handleSubmit} > {({ diff --git a/src/components/Layouts/Primary/TopBar/Items/AddMenu/Items/AddDonation/AddDonation.tsx b/src/components/Layouts/Primary/TopBar/Items/AddMenu/Items/AddDonation/AddDonation.tsx index 6e37938b1d..90cf137496 100644 --- a/src/components/Layouts/Primary/TopBar/Items/AddMenu/Items/AddDonation/AddDonation.tsx +++ b/src/components/Layouts/Primary/TopBar/Items/AddMenu/Items/AddDonation/AddDonation.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement } from 'react'; +import React, { ReactElement, useMemo } from 'react'; import { Box, CircularProgress, @@ -27,7 +27,6 @@ import { } from 'src/components/Shared/Modal/ActionButtons/ActionButtons'; import { FormTextField } from 'src/components/Shared/styledComponents/FormTextField'; import { LogFormLabel } from 'src/components/Shared/styledComponents/LogStyling'; -import i18n from 'src/lib/i18n'; import { requiredDateTime } from 'src/lib/yupHelpers'; import { useAddDonationMutation, @@ -39,55 +38,58 @@ interface AddDonationProps { handleClose: () => void; } -const donationSchema = yup.object({ - amount: yup - .number() - .typeError('Amount must be a valid number') - .required(i18n.t('Amount is required')) - .test( - 'Is amount in valid currency format?', - 'Amount must be in valid currency format', - (amount) => /\$?[0-9][0-9.,]*/.test(amount as unknown as string), - ) - .test( - 'Is positive?', - 'Must use a positive number for amount', - (value) => parseFloat(value as unknown as string) > 0, - ), - appealAmount: yup - .number() - .typeError('Appeal amount must be a valid number') - .nullable() - .test( - 'Is appeal amount in valid currency format?', - 'Appeal amount must be in valid currency format', - (amount) => - !amount || /\$?[0-9][0-9.,]*/.test(amount as unknown as string), - ) - .test( - 'Is positive?', - 'Must use a positive number for appeal amount', - (value) => !value || parseFloat(value as unknown as string) > 0, - ), - appealId: yup.string().nullable(), - currency: yup.string().required(i18n.t('Currency is required')), - designationAccountId: yup - .string() - .required(i18n.t('Designation Account is required')), - donationDate: requiredDateTime(i18n.t('Date is required')), - donorAccountId: yup.string().required(i18n.t('Partner Account is required')), - memo: yup.string().nullable(), - motivation: yup.string().nullable(), - paymentMethod: yup.string().nullable(), -}); - -type Attributes = yup.InferType; - export const AddDonation = ({ accountListId, handleClose, }: AddDonationProps): ReactElement => { const { t } = useTranslation(); + + const donationSchema = useMemo( + () => + yup.object({ + amount: yup + .number() + .typeError(t('Amount must be a valid number')) + .required(t('Amount is required')) + .test( + 'Is amount in valid currency format?', + t('Amount must be in valid currency format'), + (amount) => /\$?[0-9][0-9.,]*/.test(String(amount)), + ) + .test( + 'Is positive?', + t('Must use a positive number for amount'), + (value) => parseFloat(String(value)) > 0, + ), + appealAmount: yup + .number() + .typeError(t('Appeal amount must be a valid number')) + .nullable() + .test( + 'Is appeal amount in valid currency format?', + t('Appeal amount must be in valid currency format'), + (amount) => !amount || /\$?[0-9][0-9.,]*/.test(String(amount)), + ) + .test( + 'Is positive?', + t('Must use a positive number for appeal amount'), + (value) => !value || parseFloat(String(value)) > 0, + ), + appealId: yup.string().nullable(), + currency: yup.string().required(t('Currency is required')), + designationAccountId: yup + .string() + .required(t('Designation Account is required')), + donationDate: requiredDateTime(t('Date is required')), + donorAccountId: yup.string().required(t('Partner Account is required')), + memo: yup.string().nullable(), + motivation: yup.string().nullable(), + paymentMethod: yup.string().nullable(), + }), + [t], + ); + + type Attributes = yup.InferType; const { enqueueSnackbar } = useSnackbar(); const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down('sm'), @@ -134,10 +136,7 @@ export const AddDonation = ({ }; const onSubmit = async (attributes: Attributes) => { - const amount = (attributes.amount as unknown as string).replace( - /[^\d.-]/g, - '', - ); + const amount = String(attributes.amount).replace(/[^\d.-]/g, ''); const { data } = await addDonation({ variables: { @@ -146,7 +145,7 @@ export const AddDonation = ({ ...attributes, amount: parseFloat(amount), appealAmount: attributes.appealAmount - ? parseFloat(attributes.appealAmount as unknown as string) + ? parseFloat(String(attributes.appealAmount)) : null, donationDate: attributes.donationDate.toISODate() ?? '', }, diff --git a/src/components/Layouts/Primary/TopBar/Items/AddMenu/Items/CreateContact/CreateContact.tsx b/src/components/Layouts/Primary/TopBar/Items/AddMenu/Items/CreateContact/CreateContact.tsx index 9158c00511..7fab354c9f 100644 --- a/src/components/Layouts/Primary/TopBar/Items/AddMenu/Items/CreateContact/CreateContact.tsx +++ b/src/components/Layouts/Primary/TopBar/Items/AddMenu/Items/CreateContact/CreateContact.tsx @@ -1,5 +1,5 @@ import { useRouter } from 'next/router'; -import React, { ReactElement } from 'react'; +import React, { ReactElement, useMemo } from 'react'; import { DialogActions, DialogContent, Grid } from '@mui/material'; import { Formik } from 'formik'; import { useSnackbar } from 'notistack'; @@ -21,7 +21,6 @@ import { PersonCreateInput, StatusEnum, } from 'src/graphql/types.generated'; -import i18n from 'src/lib/i18n'; import { useCreateContactMutation } from './CreateContact.generated'; interface Props { @@ -34,16 +33,20 @@ interface Person { lastName: string; } -const contactSchema: yup.ObjectSchema> = - yup.object({ - name: yup.string().required(i18n.t('Name is required')), - }); - const CreateContact = ({ accountListId, handleClose, }: Props): ReactElement => { const { t } = useTranslation(); + + const contactSchema: yup.ObjectSchema> = + useMemo( + () => + yup.object({ + name: yup.string().required(t('Name is required')), + }), + [t], + ); const { enqueueSnackbar } = useSnackbar(); const { push } = useRouter(); diff --git a/src/components/Reports/Shared/SettingsDialog/SettingsDialog.test.tsx b/src/components/Reports/Shared/SettingsDialog/SettingsDialog.test.tsx index 69b90065cb..66e901a9e5 100644 --- a/src/components/Reports/Shared/SettingsDialog/SettingsDialog.test.tsx +++ b/src/components/Reports/Shared/SettingsDialog/SettingsDialog.test.tsx @@ -7,6 +7,7 @@ import { DateTime } from 'luxon'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import { StaffExpenseCategoryEnum } from 'src/graphql/types.generated'; +import i18n from 'src/lib/i18n'; import { ReportsStaffExpensesQuery } from '../../StaffExpenseReport/GetStaffExpense.generated'; import { DateRange } from '../../StaffExpenseReport/Helpers/StaffReportEnum'; import { @@ -279,7 +280,7 @@ describe('SettingsDialog', () => { it('rejects a start date after the viewed month when no end date is set', async () => { await expect( - getValidationSchema(DateTime.fromISO('2019-06-01')).validateAt( + getValidationSchema(DateTime.fromISO('2019-06-01'), i18n.t).validateAt( 'startDate', { startDate: DateTime.fromISO('2019-09-01'), @@ -293,7 +294,7 @@ describe('SettingsDialog', () => { it('accepts a start date within the viewed month when no end date is set', async () => { await expect( - getValidationSchema(DateTime.fromISO('2019-06-01')).validateAt( + getValidationSchema(DateTime.fromISO('2019-06-01'), i18n.t).validateAt( 'startDate', { startDate: DateTime.fromISO('2019-06-30'), diff --git a/src/components/Reports/Shared/SettingsDialog/SettingsDialog.tsx b/src/components/Reports/Shared/SettingsDialog/SettingsDialog.tsx index 46f92d0399..e3b5e52201 100644 --- a/src/components/Reports/Shared/SettingsDialog/SettingsDialog.tsx +++ b/src/components/Reports/Shared/SettingsDialog/SettingsDialog.tsx @@ -17,11 +17,10 @@ import { } from '@mui/material'; import { Form, Formik } from 'formik'; import { DateTime } from 'luxon'; -import { useTranslation } from 'react-i18next'; +import { TFunction, useTranslation } from 'react-i18next'; import * as yup from 'yup'; import { CustomDateField } from 'src/components/Shared/DateTimePickers/CustomDateField'; import { Fund, StaffExpenseCategoryEnum } from 'src/graphql/types.generated'; -import i18n from 'src/lib/i18n'; import { useReportsStaffExpensesQuery } from '../../StaffExpenseReport/GetStaffExpense.generated'; import { DateRange } from '../../StaffExpenseReport/Helpers/StaffReportEnum'; import { getAvailableCategories } from '../../StaffExpenseReport/Helpers/filterTransactions'; @@ -54,7 +53,7 @@ export interface Filters { categories: string[] | null; } -export const getValidationSchema = (currentTime: DateTime) => +export const getValidationSchema = (currentTime: DateTime, t: TFunction) => yup.object({ selectedDateRange: yup.mixed().nullable(), startDate: yup @@ -62,7 +61,7 @@ export const getValidationSchema = (currentTime: DateTime) => .nullable() .test( 'start-date-validation', - i18n.t('Start date must be earlier than or equal to end date'), + t('Start date must be earlier than or equal to end date'), function (value) { const { endDate } = this.parent; if (!value || !endDate) { @@ -74,7 +73,7 @@ export const getValidationSchema = (currentTime: DateTime) => ) .test( 'start-date-not-future-without-end', - i18n.t( + t( 'Select an end date when the start date is later than the month being viewed', ), function (value) { @@ -91,7 +90,7 @@ export const getValidationSchema = (currentTime: DateTime) => .nullable() .test( 'end-date-validation', - i18n.t('End date must be later than or equal to start date'), + t('End date must be later than or equal to start date'), function (value) { const { startDate } = this.parent; if (!value || !startDate) { @@ -171,8 +170,8 @@ export const SettingsDialog: React.FC = ({ ); const validationSchema = useMemo( - () => getValidationSchema(currentTime), - [currentTime], + () => getValidationSchema(currentTime, t), + [currentTime, t], ); const handleClose = () => { diff --git a/src/components/Settings/Accounts/MergeForm/MergeForm.tsx b/src/components/Settings/Accounts/MergeForm/MergeForm.tsx index 9f1cba052d..1652ba6480 100644 --- a/src/components/Settings/Accounts/MergeForm/MergeForm.tsx +++ b/src/components/Settings/Accounts/MergeForm/MergeForm.tsx @@ -1,4 +1,4 @@ -import { ReactElement } from 'react'; +import { ReactElement, useMemo } from 'react'; import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; import { Alert, @@ -23,7 +23,6 @@ import { SubmitButton } from 'src/components/Shared/Modal/ActionButtons/ActionBu import { PaddedBox } from 'src/components/Shared/styledComponents/PaddedBox'; import { useAccountListId } from 'src/hooks/useAccountListId'; import { getAppName } from 'src/lib/getAppName'; -import i18n from 'src/lib/i18n'; import theme from 'src/theme'; import { useAccountListQuery, @@ -45,20 +44,24 @@ type FormikSchema = { accept: boolean; }; -const formikSchema: yup.ObjectSchema = yup.object({ - selectedAccountId: yup.string().required(i18n.t('Account is required')), - accept: yup - .boolean() - .oneOf([true], i18n.t('You must accept before proceeding')) - .required(), -}); - type MergeFormProps = { isSpouse: boolean; }; export const MergeForm: React.FC = ({ isSpouse }) => { const { t } = useTranslation(); + + const formikSchema: yup.ObjectSchema = useMemo( + () => + yup.object({ + selectedAccountId: yup.string().required(t('Account is required')), + accept: yup + .boolean() + .oneOf([true], t('You must accept before proceeding')) + .required(), + }), + [t], + ); const { enqueueSnackbar } = useSnackbar(); const accountListId = useAccountListId() || ''; const { data } = useGetAccountListsForMergingQuery(); diff --git a/src/components/Settings/integrations/Google/Modals/EditGoogleIntegrationForm.tsx b/src/components/Settings/integrations/Google/Modals/EditGoogleIntegrationForm.tsx index aa45d763b7..fc5d1d3a92 100644 --- a/src/components/Settings/integrations/Google/Modals/EditGoogleIntegrationForm.tsx +++ b/src/components/Settings/integrations/Google/Modals/EditGoogleIntegrationForm.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement } from 'react'; +import React, { ReactElement, useMemo } from 'react'; import { Box, Checkbox, @@ -26,7 +26,6 @@ import { } from 'src/graphql/types.generated'; import { useAccountListId } from 'src/hooks/useAccountListId'; import { getAppName } from 'src/lib/getAppName'; -import i18n from 'src/lib/i18n'; import { GoogleAccountAttributesSlimmed } from '../GoogleAccordion'; import { GoogleAccountIntegrationsDocument, @@ -62,31 +61,6 @@ const StyledFormControlLabel = styled(FormControlLabel)(() => ({ margin: '0 0 0 -11px', })); -const integrationSchema = yup.object({ - id: yup.string().required(), - calendarId: yup.string().required(i18n.t('Calendar is required')), - calendarIntegrations: yup - .array() - .of( - yup - .mixed() - .oneOf(Object.values(ActivityTypeEnum)) - .required(), - ) - .required(), - calendars: yup - .array() - .of( - yup - .object({ - id: yup.string().required(), - name: yup.string().required(), - }) - .nullable(), - ) - .required(), -}); - export const EditGoogleIntegrationForm: React.FC< EditGoogleIntegrationFormProps > = ({ @@ -105,6 +79,35 @@ export const EditGoogleIntegrationForm: React.FC< const activities = useApiConstants()?.activities; + const integrationSchema = useMemo( + () => + yup.object({ + id: yup.string().required(), + calendarId: yup.string().required(t('Calendar is required')), + calendarIntegrations: yup + .array() + .of( + yup + .mixed() + .oneOf(Object.values(ActivityTypeEnum)) + .required(), + ) + .required(), + calendars: yup + .array() + .of( + yup + .object({ + id: yup.string().required(), + name: yup.string().required(), + }) + .nullable(), + ) + .required(), + }), + [t], + ); + const onSubmit = async ( attributes: yup.InferType, ) => { diff --git a/src/components/Settings/integrations/Mailchimp/MailchimpAccordion.tsx b/src/components/Settings/integrations/Mailchimp/MailchimpAccordion.tsx index 50aa324eda..8b5e336736 100644 --- a/src/components/Settings/integrations/Mailchimp/MailchimpAccordion.tsx +++ b/src/components/Settings/integrations/Mailchimp/MailchimpAccordion.tsx @@ -26,7 +26,6 @@ import { SubmitButton } from 'src/components/Shared/Modal/ActionButtons/ActionBu import { MailchimpAccount } from 'src/graphql/types.generated'; import { useAccountListId } from 'src/hooks/useAccountListId'; import { getAppName } from 'src/lib/getAppName'; -import i18n from 'src/lib/i18n'; import { AccordionProps, StyledList, @@ -43,13 +42,6 @@ import { } from './MailchimpAccount.generated'; import { DeleteMailchimpAccountModal } from './Modals/DeleteMailchimpModal'; -const mailchimpSchema: yup.ObjectSchema< - Pick -> = yup.object({ - autoLogCampaigns: yup.boolean().required(), - primaryListId: yup.string().required(i18n.t('A list is required')), -}); - const StyledFormControlLabel = styled(FormControlLabel)(() => ({ flex: '0 1 50%', margin: '0 0 0 -11px', @@ -65,6 +57,16 @@ export const MailchimpAccordion: React.FC = ({ disabled, }) => { const { t } = useTranslation(); + const mailchimpSchema: yup.ObjectSchema< + Pick + > = useMemo( + () => + yup.object({ + autoLogCampaigns: yup.boolean().required(), + primaryListId: yup.string().required(t('A list is required')), + }), + [t], + ); const [showSettings, setShowSettings] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const { enqueueSnackbar } = useSnackbar(); diff --git a/src/components/Shared/Filters/NullState/NullState.tsx b/src/components/Shared/Filters/NullState/NullState.tsx index cfb97e73fc..08c4eedd0c 100644 --- a/src/components/Shared/Filters/NullState/NullState.tsx +++ b/src/components/Shared/Filters/NullState/NullState.tsx @@ -11,7 +11,6 @@ import { preloadCreateContact } from 'src/components/Layouts/Primary/TopBar/Item import { useUrlFilters } from 'src/components/Shared/UrlFiltersProvider/UrlFiltersProvider'; import { TaskModalEnum } from 'src/components/Task/Modal/TaskModal'; import useTaskModal from 'src/hooks/useTaskModal'; -import i18n from 'src/lib/i18n'; import theme from 'src/theme'; import { NullStateBox } from './NullStateBox'; @@ -73,17 +72,19 @@ interface Props { const NullState: React.FC = ({ page, totalCount, - title = i18n.t('You have {{count}} total {{page}}s', { - count: totalCount, - page, - }), - paragraph = i18n.t( - 'Unfortunately none of them match your current search or filters.', - ), + title: titleProp, + paragraph: paragraphProp, }: Props) => { const { t } = useTranslation(); const { searchTerm, setActiveFilters, isFiltered } = useUrlFilters(); + const title = + titleProp ?? + t('You have {{count}} total {{page}}s', { count: totalCount, page }); + const paragraph = + paragraphProp ?? + t('Unfortunately none of them match your current search or filters.'); + return ( ().required(), - activityType: yup.mixed().required(), - subject: yup.string().required(i18n.t('Task Name is required')), - contactIds: yup.array().of(yup.string().required()).default([]), - completedAt: nullableDateTime(), - userId: yup.string().nullable(), - tagList: yup.array().of(yup.string().required()).default([]), - displayResult: yup.mixed().nullable(), - result: yup.mixed().nullable(), - changeContactStatus: yup.boolean(), - nextAction: yup.mixed().nullable(), - // These field schemas should ideally be string().defined(), but Formik thinks the form is invalid - // when those fields fields are blank for some reason, and we need to allow blank values - location: yup.string(), - comment: yup.string(), -}); -type Attributes = yup.InferType; - interface Props { accountListId: string; onClose: () => void; @@ -109,6 +89,31 @@ const TaskModalLogForm = ({ }: Props): ReactElement => { const session = useSession(); const { t } = useTranslation(); + + const taskSchema = useMemo( + () => + yup.object({ + taskPhase: yup.mixed().required(), + activityType: yup.mixed().required(), + subject: yup.string().required(t('Task Name is required')), + contactIds: yup.array().of(yup.string().required()).default([]), + completedAt: nullableDateTime(), + userId: yup.string().nullable(), + tagList: yup.array().of(yup.string().required()).default([]), + displayResult: yup.mixed().nullable(), + result: yup.mixed().nullable(), + changeContactStatus: yup.boolean(), + nextAction: yup.mixed().nullable(), + // These field schemas should ideally be string().defined(), but Formik thinks the form is invalid + // when those fields fields are blank for some reason, and we need to allow blank values + location: yup.string(), + comment: yup.string(), + }), + [t], + ); + + type Attributes = yup.InferType; + const [showMore, setShowMore] = useState(false); const [resultSelected, setResultSelected] = useState< DisplayResultEnum | ResultEnum | null diff --git a/src/components/Task/Modal/Form/TaskModalForm.tsx b/src/components/Task/Modal/Form/TaskModalForm.tsx index d46812275b..a392832403 100644 --- a/src/components/Task/Modal/Form/TaskModalForm.tsx +++ b/src/components/Task/Modal/Form/TaskModalForm.tsx @@ -48,7 +48,6 @@ import { getLocalizedNotificationTimeUnit, getLocalizedNotificationType, } from 'src/lib/functions/getLocalizedNotificationStrings'; -import i18n from 'src/lib/i18n'; import { getValueFromIdValue } from 'src/lib/phases/getValueFromIdValue'; import { inPersonActivityTypes } from 'src/lib/phases/taskActivityTypes'; import { nullableDateTime } from 'src/lib/yupHelpers'; @@ -120,29 +119,6 @@ const getTaskDetails = ( }; }; -const taskSchema = yup.object({ - taskPhase: yup.mixed().required(), - activityType: yup.mixed().required(), - subject: yup.string().required(i18n.t('Task Name is required')), - startAt: nullableDateTime(), - completedAt: nullableDateTime(), - displayResult: yup.mixed().nullable(), - result: yup.mixed().nullable(), - changeContactStatus: yup.boolean(), - nextAction: yup.mixed().nullable(), - tagList: yup.array().of(yup.string().required()).default([]), - contactIds: yup.array().of(yup.string().required()).default([]), - userId: yup.string().nullable(), - notificationTimeBefore: yup.number().nullable(), - notificationType: yup.mixed().nullable(), - notificationTimeUnit: yup.mixed().nullable(), - // These field schemas should ideally be string().defined(), but Formik thinks the form is invalid - // when those fields fields are blank for some reason, and we need to allow blank values - location: yup.string(), - comment: yup.string(), -}); -type Attributes = yup.InferType; - export interface TaskModalFormProps { accountListId: string; task?: GetTaskForTaskModalQuery['task'] | null; @@ -165,6 +141,35 @@ const TaskModalForm = ({ const session = useSession(); const { t } = useTranslation(); + + const taskSchema = useMemo( + () => + yup.object({ + taskPhase: yup.mixed().required(), + activityType: yup.mixed().required(), + subject: yup.string().required(t('Task Name is required')), + startAt: nullableDateTime(), + completedAt: nullableDateTime(), + displayResult: yup.mixed().nullable(), + result: yup.mixed().nullable(), + changeContactStatus: yup.boolean(), + nextAction: yup.mixed().nullable(), + tagList: yup.array().of(yup.string().required()).default([]), + contactIds: yup.array().of(yup.string().required()).default([]), + userId: yup.string().nullable(), + notificationTimeBefore: yup.number().nullable(), + notificationType: yup.mixed().nullable(), + notificationTimeUnit: yup.mixed().nullable(), + // These field schemas should ideally be string().defined(), but Formik thinks the form is invalid + // when those fields fields are blank for some reason, and we need to allow blank values + location: yup.string(), + comment: yup.string(), + }), + [t], + ); + + type Attributes = yup.InferType; + const { openTaskModal } = useTaskModal(); const [removeDialogOpen, handleRemoveDialog] = useState(false); const [resultSelected, setResultSelected] = useState< diff --git a/src/components/Tool/Appeal/Flow/ContactFlow.tsx b/src/components/Tool/Appeal/Flow/ContactFlow.tsx index d4938ee756..b3ba86ee05 100644 --- a/src/components/Tool/Appeal/Flow/ContactFlow.tsx +++ b/src/components/Tool/Appeal/Flow/ContactFlow.tsx @@ -1,11 +1,11 @@ -import React, { useRef, useState } from 'react'; +import React, { useMemo, useRef, useState } from 'react'; import { Box } from '@mui/material'; +import { TFunction } from 'i18next'; import { useSnackbar } from 'notistack'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { useTranslation } from 'react-i18next'; import { PledgeStatusEnum } from 'src/graphql/types.generated'; -import i18n from 'src/lib/i18n'; import theme from 'src/theme'; import { AppealHeaderInfo } from '../AppealDetails/AppealHeaderInfo/AppealHeaderInfo'; import { AppealQuery } from '../AppealDetails/AppealsMainPanel/AppealInfo.generated'; @@ -43,34 +43,34 @@ export const colorMap = { 'color-received': theme.palette.progressBarOrange.main, }; -const flowOptions: ContactFlowOption[] = [ +const getFlowOptions = (t: TFunction): ContactFlowOption[] => [ { id: crypto.randomUUID(), - name: i18n.t('Excluded'), + name: t('Excluded'), status: AppealStatusEnum.Excluded, color: colorMap['color-danger'], }, { id: crypto.randomUUID(), - name: i18n.t('Asked'), + name: t('Asked'), status: AppealStatusEnum.Asked, color: colorMap['color-text'], }, { id: crypto.randomUUID(), - name: i18n.t('Committed'), + name: t('Committed'), status: AppealStatusEnum.NotReceived, color: colorMap['color-committed'], }, { id: crypto.randomUUID(), - name: i18n.t('Received'), + name: t('Received'), status: AppealStatusEnum.ReceivedNotProcessed, color: colorMap['color-received'], }, { id: crypto.randomUUID(), - name: i18n.t('Given'), + name: t('Given'), status: AppealStatusEnum.Processed, color: colorMap['color-given'], }, @@ -82,6 +82,7 @@ export const ContactFlow: React.FC = ({ appealInfoLoading, }: ContactFlowProps) => { const { t } = useTranslation(); + const flowOptions = useMemo(() => getFlowOptions(t), [t]); const { enqueueSnackbar } = useSnackbar(); const [addExcludedContactModalOpen, setAddExcludedContactModalOpen] = useState(false); diff --git a/src/components/Tool/Appeal/InitialPage/AddAppealForm/AddAppealForm.test.tsx b/src/components/Tool/Appeal/InitialPage/AddAppealForm/AddAppealForm.test.tsx index 330b293649..988b177def 100644 --- a/src/components/Tool/Appeal/InitialPage/AddAppealForm/AddAppealForm.test.tsx +++ b/src/components/Tool/Appeal/InitialPage/AddAppealForm/AddAppealForm.test.tsx @@ -8,13 +8,14 @@ import { SnackbarProvider } from 'notistack'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import { AppealsWrapper } from 'pages/accountLists/[accountListId]/tools/appeals/AppealsWrapper'; +import i18n from 'src/lib/i18n'; import theme from 'src/theme'; import AddAppealForm, { AddAppealFormProps, buildExclusionFilter, buildInclusionFilter, calculateGoal, - contactExclusions, + getContactExclusions, } from './AddAppealForm'; import { ContactTagsQuery } from './AddAppealForm.generated'; import { contactTagsMock } from './AddAppealFormMocks'; @@ -76,6 +77,8 @@ const Components = ({ ); +const contactExclusions = getContactExclusions(i18n.t); + describe('AddAppealForm', () => { beforeEach(() => { mutationSpy.mockClear(); diff --git a/src/components/Tool/Appeal/InitialPage/AddAppealForm/AddAppealForm.tsx b/src/components/Tool/Appeal/InitialPage/AddAppealForm/AddAppealForm.tsx index 0269f18e3f..3f3a3a84af 100644 --- a/src/components/Tool/Appeal/InitialPage/AddAppealForm/AddAppealForm.tsx +++ b/src/components/Tool/Appeal/InitialPage/AddAppealForm/AddAppealForm.tsx @@ -1,5 +1,5 @@ import { useRouter } from 'next/router'; -import React, { ReactElement } from 'react'; +import React, { ReactElement, useMemo } from 'react'; import { mdiClose, mdiEqual, mdiPlus } from '@mdi/js'; import Icon from '@mdi/react'; import { @@ -16,6 +16,7 @@ import { Typography, } from '@mui/material'; import { Field, Form, Formik, FormikProps, FormikValues } from 'formik'; +import { TFunction } from 'i18next'; import { isEqual } from 'lodash'; import { DateTime } from 'luxon'; import { useSnackbar } from 'notistack'; @@ -28,7 +29,6 @@ import { FilterOption, } from 'src/graphql/types.generated'; import { useContactPartnershipStatuses } from 'src/hooks/useContactPartnershipStatuses'; -import i18n from 'src/lib/i18n'; import removeObjectNulls from 'src/lib/removeObjectNulls'; import { useContactTagsQuery } from './AddAppealForm.generated'; import { useCreateAppealMutation } from './CreateAppeal.generated'; @@ -45,25 +45,25 @@ export type ContactExclusion = { name: string; value: ExclusionEnum; }; -export const contactExclusions: ContactExclusion[] = [ +export const getContactExclusions = (t: TFunction): ContactExclusion[] => [ { - name: i18n.t('May have given a special gift in the last 3 months'), + name: t('May have given a special gift in the last 3 months'), value: ExclusionEnum.SpecialGift, }, { - name: i18n.t('May have joined my team in the last 3 months'), + name: t('May have joined my team in the last 3 months'), value: ExclusionEnum.JoinedTeam, }, { - name: i18n.t('May have increased their giving in the last 3 months'), + name: t('May have increased their giving in the last 3 months'), value: ExclusionEnum.IncreasedGiving, }, { - name: i18n.t('May have missed a gift in the last 30-90 days'), + name: t('May have missed a gift in the last 30-90 days'), value: ExclusionEnum.MissedGift, }, { - name: i18n.t('Have "Send Appeals" set to No'), + name: t('Have "Send Appeals" set to No'), value: ExclusionEnum.DoNotAskAppeals, }, ]; @@ -149,63 +149,64 @@ export const buildExclusionFilter = ( const isPositiveInteger = (value: number | undefined) => typeof value === 'number' && value >= 0 && value === Math.floor(value); -const appealFormSchema = yup.object({ - name: yup.string().required('Please enter a name'), - initialGoal: yup - .number() - .typeError(i18n.t('Initial Goal must be a valid number')) - .required(i18n.t('Initial Goal is required')) - .test( - i18n.t('Is positive?'), - i18n.t('Must use a positive whole number for Initial Goal'), - isPositiveInteger, - ), - letterCost: yup - .number() - .typeError(i18n.t('Letter Cost must be a valid number')) - .required(i18n.t('Letter Cost is required')) - .test( - i18n.t('Is positive?'), - i18n.t('Must use a positive whole number for Letter Cost'), - isPositiveInteger, - ), - adminPercentage: yup - .number() - .typeError(i18n.t('Admin Cost must be a valid number')) - .required(i18n.t('Admin Cost is required')) - .test( - i18n.t('Is positive?'), - i18n.t('Must use a positive whole number for Admin Cost'), - isPositiveInteger, +const getAppealFormSchema = (t: TFunction) => + yup.object({ + name: yup.string().required(t('Please enter a name')), + initialGoal: yup + .number() + .typeError(t('Initial Goal must be a valid number')) + .required(t('Initial Goal is required')) + .test( + t('Is positive?'), + t('Must use a positive whole number for Initial Goal'), + isPositiveInteger, + ), + letterCost: yup + .number() + .typeError(t('Letter Cost must be a valid number')) + .required(t('Letter Cost is required')) + .test( + t('Is positive?'), + t('Must use a positive whole number for Letter Cost'), + isPositiveInteger, + ), + adminPercentage: yup + .number() + .typeError(t('Admin Cost must be a valid number')) + .required(t('Admin Cost is required')) + .test( + t('Is positive?'), + t('Must use a positive whole number for Admin Cost'), + isPositiveInteger, + ), + goal: yup + .number() + .typeError(t('Goal must be a valid number')) + .required(t('Goal is required')) + .test( + t('Is positive?'), + t('Must use a positive number for Goal'), + (value) => parseFloat(String(value)) >= 0, + ), + statuses: yup.array().of( + yup + .object({ + name: yup.string(), + value: yup.string(), + }) + .required(), ), - goal: yup - .number() - .typeError(i18n.t('Goal must be a valid number')) - .required(i18n.t('Goal is required')) - .test( - i18n.t('Is positive?'), - i18n.t('Must use a positive number for Goal'), - (value) => parseFloat(value as unknown as string) >= 0, + tags: yup.array().of(yup.string().required()), + exclusions: yup.array().of( + yup + .object({ + name: yup.string(), + value: yup.string(), + }) + .required(), ), - statuses: yup.array().of( - yup - .object({ - name: yup.string(), - value: yup.string(), - }) - .required(), - ), - tags: yup.array().of(yup.string().required()), - exclusions: yup.array().of( - yup - .object({ - name: yup.string(), - value: yup.string(), - }) - .required(), - ), -}); -type Attributes = yup.InferType; + }); +type Attributes = yup.InferType>; type FormikRefType = React.RefObject< FormikProps<{ @@ -266,6 +267,8 @@ const AddAppealForm: React.FC = ({ }) => { const { classes } = useStyles(); const { t } = useTranslation(); + const contactExclusions = useMemo(() => getContactExclusions(t), [t]); + const appealFormSchema = useMemo(() => getAppealFormSchema(t), [t]); const { push } = useRouter(); const { enqueueSnackbar } = useSnackbar(); const { contactStatuses } = useContactPartnershipStatuses(); diff --git a/src/components/Tool/Appeal/Modals/AddAppealModal/AddAppealModal.tsx b/src/components/Tool/Appeal/Modals/AddAppealModal/AddAppealModal.tsx index fddced8cc5..8023a35afe 100644 --- a/src/components/Tool/Appeal/Modals/AddAppealModal/AddAppealModal.tsx +++ b/src/components/Tool/Appeal/Modals/AddAppealModal/AddAppealModal.tsx @@ -12,7 +12,7 @@ import { FilterOption } from 'src/graphql/types.generated'; import { useAccountListId } from 'src/hooks/useAccountListId'; import AddAppealForm, { ContactExclusion, - contactExclusions, + getContactExclusions, } from '../../InitialPage/AddAppealForm/AddAppealForm'; interface AddAppealModalProps { @@ -61,8 +61,8 @@ export const AddAppealModal: React.FC = ({ if (!isEndOfYearAsk) { return appealExcludes; } - return contactExclusions; - }, [appealExcludes, isEndOfYearAsk]); + return getContactExclusions(t); + }, [appealExcludes, isEndOfYearAsk, t]); const handleSubmit = async () => { if (formRef.current) { diff --git a/src/components/Tool/Appeal/Modals/EditAppealHeaderInfoModal/EditAppealHeaderInfoModal.tsx b/src/components/Tool/Appeal/Modals/EditAppealHeaderInfoModal/EditAppealHeaderInfoModal.tsx index 757620bc40..f7c7f00985 100644 --- a/src/components/Tool/Appeal/Modals/EditAppealHeaderInfoModal/EditAppealHeaderInfoModal.tsx +++ b/src/components/Tool/Appeal/Modals/EditAppealHeaderInfoModal/EditAppealHeaderInfoModal.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement } from 'react'; +import React, { ReactElement, useMemo } from 'react'; import { DialogActions, DialogContent, @@ -18,7 +18,6 @@ import { } from 'src/components/Shared/Modal/ActionButtons/ActionButtons'; import Modal from 'src/components/Shared/Modal/Modal'; import { useAccountListId } from 'src/hooks/useAccountListId'; -import i18n from 'src/lib/i18n'; import { useUpdateAppealMutation } from './EditAppeal.generated'; interface EditAppealHeaderInfoModalProps { @@ -31,23 +30,27 @@ export type EditAppealFormikSchema = { amount: number; }; -const EditAppealSchema: yup.ObjectSchema = yup.object({ - name: yup.string().required(i18n.t('Please enter a name')), - amount: yup - .number() - .required(i18n.t('Please enter a goal')) - .typeError(i18n.t('Appeal amount must be a valid number')) - .test( - i18n.t('Is positive?'), - i18n.t('Must use a positive number for appeal amount'), - (value) => !value || parseFloat(value as unknown as string) > 0, - ), -}); - export const EditAppealHeaderInfoModal: React.FC< EditAppealHeaderInfoModalProps > = ({ appealInfo, handleClose }) => { const { t } = useTranslation(); + + const editAppealSchema: yup.ObjectSchema = useMemo( + () => + yup.object({ + name: yup.string().required(t('Please enter a name')), + amount: yup + .number() + .required(t('Please enter a goal')) + .typeError(t('Appeal amount must be a valid number')) + .test( + t('Is positive?'), + t('Must use a positive number for appeal amount'), + (value) => !value || parseFloat(String(value)) > 0, + ), + }), + [t], + ); const accountListId = useAccountListId(); const { enqueueSnackbar } = useSnackbar(); const [UpdateAppeal] = useUpdateAppealMutation(); @@ -98,7 +101,7 @@ export const EditAppealHeaderInfoModal: React.FC< name: appealInfo.name, amount: appealInfo.amount ?? 0, }} - validationSchema={EditAppealSchema} + validationSchema={editAppealSchema} validateOnMount onSubmit={onSubmit} > diff --git a/src/components/Tool/Appeal/Modals/PledgeModal/PledgeModal.tsx b/src/components/Tool/Appeal/Modals/PledgeModal/PledgeModal.tsx index e12d661210..624d25f50f 100644 --- a/src/components/Tool/Appeal/Modals/PledgeModal/PledgeModal.tsx +++ b/src/components/Tool/Appeal/Modals/PledgeModal/PledgeModal.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement } from 'react'; +import React, { ReactElement, useMemo } from 'react'; import { Alert, DialogActions, @@ -28,7 +28,6 @@ import Modal from 'src/components/Shared/Modal/Modal'; import { FormTextField } from 'src/components/Shared/styledComponents/FormTextField'; import { LogFormLabel } from 'src/components/Shared/styledComponents/LogStyling'; import { PledgeStatusEnum } from 'src/graphql/types.generated'; -import i18n from 'src/lib/i18n'; import { requiredDateTime } from 'src/lib/yupHelpers'; import { AppealStatusEnum, @@ -52,28 +51,6 @@ interface PledgeModalProps { selectedAppealStatus?: AppealStatusEnum | null; } -const CreatePledgeSchema = yup.object({ - amount: yup - .number() - .typeError(i18n.t('Amount must be a valid number')) - .required(i18n.t('Amount is required')) - .test( - i18n.t('Is amount in valid currency format?'), - i18n.t('Amount must be in valid currency format'), - (amount) => /\$?[0-9][0-9.,]*/.test(amount as unknown as string), - ) - .test( - i18n.t('Is positive?'), - i18n.t('Must use a positive number for amount'), - (value) => parseFloat(value as unknown as string) > 0, - ), - amountCurrency: yup.string().required(i18n.t('Currency is required')), - expectedDate: requiredDateTime(i18n.t('Expected Date is required')), - status: yup.string().required(i18n.t('Status is required')), -}); - -type Attributes = yup.InferType; - export const PledgeModal: React.FC = ({ contact, pledge, @@ -95,6 +72,32 @@ export const PledgeModal: React.FC = ({ const isNewPledge = pledge === undefined; + const createPledgeSchema = useMemo( + () => + yup.object({ + amount: yup + .number() + .typeError(t('Amount must be a valid number')) + .required(t('Amount is required')) + .test( + t('Is amount in valid currency format?'), + t('Amount must be in valid currency format'), + (amount) => /\$?[0-9][0-9.,]*/.test(String(amount)), + ) + .test( + t('Is positive?'), + t('Must use a positive number for amount'), + (value) => parseFloat(String(value)) > 0, + ), + amountCurrency: yup.string().required(t('Currency is required')), + expectedDate: requiredDateTime(t('Expected Date is required')), + status: yup.string().required(t('Status is required')), + }), + [t], + ); + + type Attributes = yup.InferType; + const onSubmit = async (attributes: Attributes) => { const amount = parseFloat( attributes.amount.toString().replace(/[^\d.-]/g, ''), @@ -203,7 +206,7 @@ export const PledgeModal: React.FC = ({ > diff --git a/src/components/Tool/FixCommitmentInfo/InputOptions/Frequencies.ts b/src/components/Tool/FixCommitmentInfo/InputOptions/Frequencies.ts deleted file mode 100644 index 06e8cd66a8..0000000000 --- a/src/components/Tool/FixCommitmentInfo/InputOptions/Frequencies.ts +++ /dev/null @@ -1,13 +0,0 @@ -import i18n from 'src/lib/i18n'; - -export const frequencies: { [key: string]: string } = { - WEEKLY: i18n.t('Weekly'), - EVERY_2_WEEKS: i18n.t('Every 2 Weeks'), - MONTHLY: i18n.t('Monthly'), - EVERY_2_MONTHS: i18n.t('Every 2 Months'), - QUARTERLY: i18n.t('Quarterly'), - EVERY_4_MONTHS: i18n.t('Every 4 Months'), - EVERY_6_MONTHS: i18n.t('Every 6 Months'), - ANNUALLY: i18n.t('Annually'), - EVERY_2_YEARS: i18n.t('Every 2 Years'), -}; diff --git a/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx b/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx index eaf063b86d..33f85de221 100644 --- a/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx +++ b/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { Box, FormControl, @@ -16,7 +17,6 @@ import { useTranslation } from 'react-i18next'; import { makeStyles } from 'tss-react/mui'; import * as yup from 'yup'; import { AddIcon } from 'src/components/Contacts/ContactDetails/ContactDetailsTab/StyledComponents'; -import i18n from 'src/lib/i18n'; import { useEmailAddressesMutation } from './AddEmailAddress.generated'; import { GetInvalidEmailAddressesDocument, @@ -65,23 +65,27 @@ interface EmailValidationFormProps { accountListId: string; } -const validationSchema = yup.object({ - email: yup - .string() - .email(i18n.t('Invalid Email Address Format')) - .required(i18n.t('Please enter a valid email address')), - isPrimary: yup.bool().default(false), - updatedAt: yup.string(), - source: yup.string(), - personId: yup.string(), - isValid: yup.bool().default(false), -}); - const EmailValidationForm = ({ personId, accountListId, }: EmailValidationFormProps) => { const { t } = useTranslation(); + + const validationSchema = useMemo( + () => + yup.object({ + email: yup + .string() + .email(t('Invalid Email Address Format')) + .required(t('Please enter a valid email address')), + isPrimary: yup.bool().default(false), + updatedAt: yup.string(), + source: yup.string(), + personId: yup.string(), + isValid: yup.bool().default(false), + }), + [t], + ); const [emailAddressesMutation] = useEmailAddressesMutation(); const { enqueueSnackbar } = useSnackbar(); const { classes } = useStyles(); diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx index 394cc66d7d..69108b6844 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx @@ -32,7 +32,6 @@ import { Confirmation } from 'src/components/Shared/Modal/Confirmation/Confirmat import { useUpdateEmailAddressesMutation } from 'src/components/Tool/FixEmailAddresses/FixEmailAddresses.generated'; import { useLocale } from 'src/hooks/useLocale'; import { getAppName } from 'src/lib/getAppName'; -import i18n from 'src/lib/i18n'; import { dateFormatShort } from 'src/lib/intlFormat'; import { isEditableSource, sourceToStr } from 'src/lib/sourceHelper'; import theme from 'src/theme'; @@ -115,12 +114,6 @@ interface EmailToDelete { email: Email; } -const validationSchema = yup.object({ - newEmail: yup - .string() - .email(i18n.t('Invalid Email Address Format')) - .required(i18n.t('Please enter a valid email address')), -}); export interface FixEmailAddressPersonProps { person: PersonInvalidEmailFragment; dataState: { [key: string]: PersonEmailAddresses }; @@ -147,6 +140,18 @@ export const FixEmailAddressPerson: React.FC = ({ }) => { const appName = getAppName(); const { t } = useTranslation(); + + const validationSchema = useMemo( + () => + yup.object({ + newEmail: yup + .string() + .email(t('Invalid Email Address Format')) + .required(t('Please enter a valid email address')), + }), + [t], + ); + const locale = useLocale(); const { classes } = useStyles(); const { enqueueSnackbar } = useSnackbar(); diff --git a/src/components/Tool/Home/ToolsHome.test.tsx b/src/components/Tool/Home/ToolsHome.test.tsx index 0a2fb55ab8..5f3acd4378 100644 --- a/src/components/Tool/Home/ToolsHome.test.tsx +++ b/src/components/Tool/Home/ToolsHome.test.tsx @@ -4,9 +4,10 @@ import { render, waitFor } from '@testing-library/react'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import { GetToolNotificationsQuery } from 'src/components/Layouts/Primary/TopBar/Items/NavMenu/GetToolNotifcations.generated'; +import i18n from 'src/lib/i18n'; import theme from '../../../theme'; import ToolsHome from './ToolsHome'; -import { ToolsListHome } from './ToolsListHome'; +import { getToolsListHome } from './ToolsListHome'; const accountListId = 'account-list-1'; @@ -50,7 +51,7 @@ describe('ToolHome', () => { const { getByTestId, queryByText } = render(); expect(getByTestId('Home')).toBeInTheDocument(); - ToolsListHome.forEach((tool) => { + getToolsListHome(i18n.t).forEach((tool) => { expect(queryByText(tool.tool)).toBeInTheDocument(); expect(queryByText(tool.desc)).toBeInTheDocument(); }); diff --git a/src/components/Tool/Home/ToolsHome.tsx b/src/components/Tool/Home/ToolsHome.tsx index 03c02e9e06..78e700956f 100644 --- a/src/components/Tool/Home/ToolsHome.tsx +++ b/src/components/Tool/Home/ToolsHome.tsx @@ -1,13 +1,14 @@ -import React, { ReactElement } from 'react'; +import React, { ReactElement, useMemo } from 'react'; import { Box, Grid, Theme } from '@mui/material'; import { motion } from 'framer-motion'; +import { useTranslation } from 'react-i18next'; import { makeStyles } from 'tss-react/mui'; import { useGetToolNotificationsQuery } from 'src/components/Layouts/Primary/TopBar/Items/NavMenu/GetToolNotifcations.generated'; import { ToolName } from 'src/components/Layouts/Primary/TopBar/Items/NavMenu/NavMenu'; import { useAccountListId } from '../../../hooks/useAccountListId'; import { ToolsGridContainer } from '../styledComponents'; import Tool from './Tool'; -import { ToolsListHome } from './ToolsListHome'; +import { getToolsListHome } from './ToolsListHome'; const useStyles = makeStyles()((theme: Theme) => ({ toolIcon: { @@ -40,7 +41,9 @@ interface ToolHomeProps { } const ToolsHome: React.FC = ({ onSetupTour }): ReactElement => { + const { t } = useTranslation(); const { classes } = useStyles(); + const toolsListHome = useMemo(() => getToolsListHome(t), [t]); const accountListId = useAccountListId(); const { data, loading } = useGetToolNotificationsQuery({ @@ -66,7 +69,7 @@ const ToolsHome: React.FC = ({ onSetupTour }): ReactElement => { > - {ToolsListHome.map((tool) => { + {toolsListHome.map((tool) => { const needsAttention = (!onSetupTour && toolDataTotalCount && diff --git a/src/components/Tool/Home/ToolsListHome.ts b/src/components/Tool/Home/ToolsListHome.ts index 7e73683230..aee3c5965b 100644 --- a/src/components/Tool/Home/ToolsListHome.ts +++ b/src/components/Tool/Home/ToolsListHome.ts @@ -11,7 +11,7 @@ import { mdiTable, mdiTrophy, } from '@mdi/js'; -import i18n from 'src/lib/i18n'; +import { TFunction } from 'i18next'; export interface ToolItem { tool: string; @@ -21,84 +21,82 @@ export interface ToolItem { url: string; } -export const ToolsListHome: ToolItem[] = [ +export const getToolsListHome = (t: TFunction): ToolItem[] => [ { - tool: i18n.t('Appeals'), - desc: i18n.t( - 'Set goals, create asks, and track progress for one-time needs', - ), + tool: t('Appeals'), + desc: t('Set goals, create asks, and track progress for one-time needs'), icon: mdiTrophy, id: 'appeals', url: 'appeals', }, { - tool: i18n.t('Import from Google'), - desc: i18n.t('Import your contact information from your Google account'), + tool: t('Import from Google'), + desc: t('Import your contact information from your Google account'), icon: mdiGoogle, id: 'import/google', url: 'import/google', }, { - tool: i18n.t('Import from CSV'), - desc: i18n.t('Import contacts you have saved in a CSV file'), + tool: t('Import from CSV'), + desc: t('Import contacts you have saved in a CSV file'), icon: mdiTable, id: 'import/csv', url: 'import/csv', }, { - tool: i18n.t('Import from TntConnect'), - desc: i18n.t('Import your contacts from your TntConnect database'), + tool: t('Import from TntConnect'), + desc: t('Import your contacts from your TntConnect database'), icon: mdiCloudUpload, id: 'import/tnt', url: 'import/tnt', }, { - tool: i18n.t('Fix Commitment Info'), - desc: i18n.t('Set the correct contacts commitment info for each contact'), + tool: t('Fix Commitment Info'), + desc: t('Set the correct contacts commitment info for each contact'), icon: mdiCurrencyUsd, id: 'fixCommitmentInfo', url: 'fix/commitmentInfo', }, { - tool: i18n.t('Fix Email Addresses'), - desc: i18n.t('Set the correct primary email address for each person'), + tool: t('Fix Email Addresses'), + desc: t('Set the correct primary email address for each person'), icon: mdiEmail, id: 'fixEmailAddresses', url: 'fix/emailAddresses', }, { - tool: i18n.t('Fix Mailing Addresses'), - desc: i18n.t('Set the correct primary mailing address for each contact'), + tool: t('Fix Mailing Addresses'), + desc: t('Set the correct primary mailing address for each contact'), icon: mdiMap, id: 'fixMailingAddresses', url: 'fix/mailingAddresses', }, { - tool: i18n.t('Fix Phone Numbers'), - desc: i18n.t('Set the correct primary phone number for each person'), + tool: t('Fix Phone Numbers'), + desc: t('Set the correct primary phone number for each person'), icon: mdiPhone, id: 'fixPhoneNumbers', url: 'fix/phoneNumbers', }, { - tool: i18n.t('Fix Send Newsletter'), - desc: i18n.t('Set the correct newsletter state for each contact'), + tool: t('Fix Send Newsletter'), + desc: t('Set the correct newsletter state for each contact'), icon: mdiNewspaperVariantOutline, id: 'fixSendNewsletter', url: 'fix/sendNewsletter', }, { - tool: i18n.t('Merge Contacts'), - desc: i18n.t('Review and merge duplicate contacts'), + tool: t('Merge Contacts'), + desc: t('Review and merge duplicate contacts'), icon: mdiHome, id: 'mergeContacts', url: 'merge/contacts', }, { - tool: i18n.t('Merge People'), - desc: i18n.t('Review and merge duplicate people'), + tool: t('Merge People'), + desc: t('Review and merge duplicate people'), icon: mdiAccountGroup, id: 'mergePeople', url: 'merge/people', diff --git a/src/components/Tool/NoData.tsx b/src/components/Tool/NoData.tsx index c26e9ca293..8db5a1b8fe 100644 --- a/src/components/Tool/NoData.tsx +++ b/src/components/Tool/NoData.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement } from 'react'; +import React, { ReactElement, useMemo } from 'react'; import { mdiAccountGroup, mdiCurrencyUsd, @@ -11,7 +11,8 @@ import { } from '@mdi/js'; import Icon from '@mdi/react'; import { Typography } from '@mui/material'; -import i18n from 'src/lib/i18n'; +import { TFunction } from 'i18next'; +import { useTranslation } from 'react-i18next'; import { NullStateBox } from '../Shared/Filters/NullState/NullStateBox'; interface Props { @@ -25,66 +26,67 @@ interface ToolText { icon: string; } -const textMap: { [key: string]: ToolText } = { +const getTextMap = (t: TFunction): { [key: string]: ToolText } => ({ fixCommitmentInfo: { - primaryText: i18n.t('No contacts with commitment info need attention'), - secondaryText: i18n.t( + primaryText: t('No contacts with commitment info need attention'), + secondaryText: t( 'Contacts with possibly incorrect commitment info will appear here.', ), icon: mdiCurrencyUsd, }, fixMailingAddresses: { - primaryText: i18n.t('No contacts with mailing addresses need attention'), - secondaryText: i18n.t( + primaryText: t('No contacts with mailing addresses need attention'), + secondaryText: t( 'Contacts with new addresses or multiple primary mailing addresses will appear here.', ), icon: mdiMap, }, fixSendNewsletter: { - primaryText: i18n.t( + primaryText: t( 'No contacts with an empty newsletter status need attention', ), - secondaryText: i18n.t( + secondaryText: t( 'Contacts that appear here have an empty newsletter status and partner status set to financial, special, or pray.', ), icon: mdiNewspaperVariantOutline, }, mergeContacts: { - primaryText: i18n.t('No duplicate contacts need attention'), - secondaryText: i18n.t( + primaryText: t('No duplicate contacts need attention'), + secondaryText: t( 'People with similar names and partner account numbers will appear here.', ), icon: mdiHome, }, fixEmailAddresses: { - primaryText: i18n.t('No people with email addresses need attention'), - secondaryText: i18n.t( + primaryText: t('No people with email addresses need attention'), + secondaryText: t( 'People with new email addresses or multiple primary email addresses will appear here.', ), icon: mdiEmailOutline, }, fixPhoneNumbers: { - primaryText: i18n.t('No people with phone numbers need attention'), - secondaryText: i18n.t( + primaryText: t('No people with phone numbers need attention'), + secondaryText: t( 'People with new phone numbers or multiple primary phone numbers will appear here.', ), icon: mdiPhone, }, mergePeople: { - primaryText: i18n.t('No duplicate people need attention'), - secondaryText: i18n.t('People with similar names will appear here.'), + primaryText: t('No duplicate people need attention'), + secondaryText: t('People with similar names will appear here.'), icon: mdiAccountGroup, }, googleImport: { - primaryText: i18n.t("You haven't connected a Google account yet"), - secondaryText: i18n.t( - 'Add a Google account then try to import from Google.', - ), + primaryText: t("You haven't connected a Google account yet"), + secondaryText: t('Add a Google account then try to import from Google.'), icon: mdiGoogle, }, -}; +}); const NoData: React.FC = ({ tool, button }: Props) => { + const { t } = useTranslation(); + const textMap = useMemo(() => getTextMap(t), [t]); + return ( diff --git a/src/components/User/Preferences/UserPreferenceProvider.tsx b/src/components/User/Preferences/UserPreferenceProvider.tsx index 4d7925fe93..464239f5cc 100644 --- a/src/components/User/Preferences/UserPreferenceProvider.tsx +++ b/src/components/User/Preferences/UserPreferenceProvider.tsx @@ -1,5 +1,5 @@ import React, { createContext, useContext, useEffect, useState } from 'react'; -import i18next from 'src/lib/i18n'; +import { useTranslation } from 'react-i18next'; import { useGetUserQuery } from '../GetUser.generated'; export type UserPreferenceType = { @@ -18,15 +18,16 @@ interface Props { children?: React.ReactNode; } export const UserPreferenceProvider: React.FC = ({ children }) => { + const { i18n } = useTranslation(); const { data } = useGetUserQuery(); const [locale, setLocale] = useState('en-US'); useEffect(() => { if (data) { - i18next.changeLanguage(data.user.preferences?.language ?? 'en'); + i18n.changeLanguage(data.user.preferences?.language ?? 'en'); setLocale(data.user.preferences?.locale ?? 'en-US'); } - }, [data]); + }, [data, i18n]); return ( > => { + const { t } = useTranslation(); const timezones = [ { key: 'American Samoa', - value: i18n.t('(GMT-11:00) American Samoa'), + value: t('(GMT-11:00) American Samoa'), }, { key: 'International Date Line West', - value: i18n.t('(GMT-11:00) International Date Line West'), + value: t('(GMT-11:00) International Date Line West'), }, { key: 'Midway Island', - value: i18n.t('(GMT-11:00) Midway Island'), + value: t('(GMT-11:00) Midway Island'), }, { key: 'Samoa', - value: i18n.t('(GMT-11:00) Samoa'), + value: t('(GMT-11:00) Samoa'), }, { key: 'Hawaii', - value: i18n.t('(GMT-10:00) Hawaii'), + value: t('(GMT-10:00) Hawaii'), }, { key: 'Alaska', - value: i18n.t('(GMT-09:00) Alaska'), + value: t('(GMT-09:00) Alaska'), }, { key: 'Pacific Time (US & Canada)', - value: i18n.t('(GMT-08:00) Pacific Time (US & Canada)'), + value: t('(GMT-08:00) Pacific Time (US & Canada)'), }, { key: 'Tijuana', - value: i18n.t('(GMT-08:00) Tijuana'), + value: t('(GMT-08:00) Tijuana'), }, { key: 'Arizona', - value: i18n.t('(GMT-07:00) Arizona'), + value: t('(GMT-07:00) Arizona'), }, { key: 'Chihuahua', - value: i18n.t('(GMT-07:00) Chihuahua'), + value: t('(GMT-07:00) Chihuahua'), }, { key: 'Mazatlan', - value: i18n.t('(GMT-07:00) Mazatlan'), + value: t('(GMT-07:00) Mazatlan'), }, { key: 'Mountain Time (US & Canada)', - value: i18n.t('(GMT-07:00) Mountain Time (US & Canada)'), + value: t('(GMT-07:00) Mountain Time (US & Canada)'), }, { key: 'Central America', - value: i18n.t('(GMT-06:00) Central America'), + value: t('(GMT-06:00) Central America'), }, { key: 'Central Time (US & Canada)', - value: i18n.t('(GMT-06:00) Central Time (US & Canada)'), + value: t('(GMT-06:00) Central Time (US & Canada)'), }, { key: 'Guadalajara', - value: i18n.t('(GMT-06:00) Guadalajara'), + value: t('(GMT-06:00) Guadalajara'), }, { key: 'Mexico City', - value: i18n.t('(GMT-06:00) Mexico City'), + value: t('(GMT-06:00) Mexico City'), }, { key: 'Monterrey', - value: i18n.t('(GMT-06:00) Monterrey'), + value: t('(GMT-06:00) Monterrey'), }, { key: 'Saskatchewan', - value: i18n.t('(GMT-06:00) Saskatchewan'), + value: t('(GMT-06:00) Saskatchewan'), }, { key: 'Bogota', - value: i18n.t('(GMT-05:00) Bogota'), + value: t('(GMT-05:00) Bogota'), }, { key: 'Eastern Time (US & Canada)', - value: i18n.t('(GMT-05:00) Eastern Time (US & Canada)'), + value: t('(GMT-05:00) Eastern Time (US & Canada)'), }, { key: 'Indiana (East)', - value: i18n.t('(GMT-05:00) Indiana (East)'), + value: t('(GMT-05:00) Indiana (East)'), }, { key: 'Lima', - value: i18n.t('(GMT-05:00) Lima'), + value: t('(GMT-05:00) Lima'), }, { key: 'Quito', - value: i18n.t('(GMT-05:00) Quito'), + value: t('(GMT-05:00) Quito'), }, { key: 'Atlantic Time (Canada)', - value: i18n.t('(GMT-04:00) Atlantic Time (Canada)'), + value: t('(GMT-04:00) Atlantic Time (Canada)'), }, { key: 'Caracas', - value: i18n.t('(GMT-04:00) Caracas'), + value: t('(GMT-04:00) Caracas'), }, { key: 'Georgetown', - value: i18n.t('(GMT-04:00) Georgetown'), + value: t('(GMT-04:00) Georgetown'), }, { key: 'La Paz', - value: i18n.t('(GMT-04:00) La Paz'), + value: t('(GMT-04:00) La Paz'), }, { key: 'Santiago', - value: i18n.t('(GMT-04:00) Santiago'), + value: t('(GMT-04:00) Santiago'), }, { key: 'Newfoundland', - value: i18n.t('(GMT-03:30) Newfoundland'), + value: t('(GMT-03:30) Newfoundland'), }, { key: 'Brasilia', - value: i18n.t('(GMT-03:00) Brasilia'), + value: t('(GMT-03:00) Brasilia'), }, { key: 'Buenos Aires', - value: i18n.t('(GMT-03:00) Buenos Aires'), + value: t('(GMT-03:00) Buenos Aires'), }, { key: 'Greenland', - value: i18n.t('(GMT-03:00) Greenland'), + value: t('(GMT-03:00) Greenland'), }, { key: 'Montevideo', - value: i18n.t('(GMT-03:00) Montevideo'), + value: t('(GMT-03:00) Montevideo'), }, { key: 'Mid-Atlantic', - value: i18n.t('(GMT-02:00) Mid-Atlantic'), + value: t('(GMT-02:00) Mid-Atlantic'), }, { key: 'Azores', - value: i18n.t('(GMT-01:00) Azores'), + value: t('(GMT-01:00) Azores'), }, { key: 'Cape Verde Is.', - value: i18n.t('(GMT-01:00) Cape Verde Is.'), + value: t('(GMT-01:00) Cape Verde Is.'), }, { key: 'Casablanca', - value: i18n.t('(GMT+00:00) Casablanca'), + value: t('(GMT+00:00) Casablanca'), }, { key: 'Dublin', - value: i18n.t('(GMT+00:00) Dublin'), + value: t('(GMT+00:00) Dublin'), }, { key: 'Edinburgh', - value: i18n.t('(GMT+00:00) Edinburgh'), + value: t('(GMT+00:00) Edinburgh'), }, { key: 'Lisbon', - value: i18n.t('(GMT+00:00) Lisbon'), + value: t('(GMT+00:00) Lisbon'), }, { key: 'London', - value: i18n.t('(GMT+00:00) London'), + value: t('(GMT+00:00) London'), }, { key: 'Monrovia', - value: i18n.t('(GMT+00:00) Monrovia'), + value: t('(GMT+00:00) Monrovia'), }, { key: 'UTC', - value: i18n.t('(GMT+00:00) UTC'), + value: t('(GMT+00:00) UTC'), }, { key: 'Amsterdam', - value: i18n.t('(GMT+01:00) Amsterdam'), + value: t('(GMT+01:00) Amsterdam'), }, { key: 'Belgrade', - value: i18n.t('(GMT+01:00) Belgrade'), + value: t('(GMT+01:00) Belgrade'), }, { key: 'Berlin', - value: i18n.t('(GMT+01:00) Berlin'), + value: t('(GMT+01:00) Berlin'), }, { key: 'Bern', - value: i18n.t('(GMT+01:00) Bern'), + value: t('(GMT+01:00) Bern'), }, { key: 'Bratislava', - value: i18n.t('(GMT+01:00) Bratislava'), + value: t('(GMT+01:00) Bratislava'), }, { key: 'Brussels', - value: i18n.t('(GMT+01:00) Brussels'), + value: t('(GMT+01:00) Brussels'), }, { key: 'Budapest', - value: i18n.t('(GMT+01:00) Budapest'), + value: t('(GMT+01:00) Budapest'), }, { key: 'Copenhagen', - value: i18n.t('(GMT+01:00) Copenhagen'), + value: t('(GMT+01:00) Copenhagen'), }, { key: 'Ljubljana', - value: i18n.t('(GMT+01:00) Ljubljana'), + value: t('(GMT+01:00) Ljubljana'), }, { key: 'Madrid', - value: i18n.t('(GMT+01:00) Madrid'), + value: t('(GMT+01:00) Madrid'), }, { key: 'Paris', - value: i18n.t('(GMT+01:00) Paris'), + value: t('(GMT+01:00) Paris'), }, { key: 'Prague', - value: i18n.t('(GMT+01:00) Prague'), + value: t('(GMT+01:00) Prague'), }, { key: 'Rome', - value: i18n.t('(GMT+01:00) Rome'), + value: t('(GMT+01:00) Rome'), }, { key: 'Sarajevo', - value: i18n.t('(GMT+01:00) Sarajevo'), + value: t('(GMT+01:00) Sarajevo'), }, { key: 'Skopje', - value: i18n.t('(GMT+01:00) Skopje'), + value: t('(GMT+01:00) Skopje'), }, { key: 'Stockholm', - value: i18n.t('(GMT+01:00) Stockholm'), + value: t('(GMT+01:00) Stockholm'), }, { key: 'Vienna', - value: i18n.t('(GMT+01:00) Vienna'), + value: t('(GMT+01:00) Vienna'), }, { key: 'Warsaw', - value: i18n.t('(GMT+01:00) Warsaw'), + value: t('(GMT+01:00) Warsaw'), }, { key: 'West Central Africa', - value: i18n.t('(GMT+01:00) West Central Africa'), + value: t('(GMT+01:00) West Central Africa'), }, { key: 'Zagreb', - value: i18n.t('(GMT+01:00) Zagreb'), + value: t('(GMT+01:00) Zagreb'), }, { key: 'Athens', - value: i18n.t('(GMT+02:00) Athens'), + value: t('(GMT+02:00) Athens'), }, { key: 'Bucharest', - value: i18n.t('(GMT+02:00) Bucharest'), + value: t('(GMT+02:00) Bucharest'), }, { key: 'Cairo', - value: i18n.t('(GMT+02:00) Cairo'), + value: t('(GMT+02:00) Cairo'), }, { key: 'Harare', - value: i18n.t('(GMT+02:00) Harare'), + value: t('(GMT+02:00) Harare'), }, { key: 'Helsinki', - value: i18n.t('(GMT+02:00) Helsinki'), + value: t('(GMT+02:00) Helsinki'), }, { key: 'Istanbul', - value: i18n.t('(GMT+02:00) Istanbul'), + value: t('(GMT+02:00) Istanbul'), }, { key: 'Jerusalem', - value: i18n.t('(GMT+02:00) Jerusalem'), + value: t('(GMT+02:00) Jerusalem'), }, { key: 'Kaliningrad', - value: i18n.t('(GMT+02:00) Kaliningrad'), + value: t('(GMT+02:00) Kaliningrad'), }, { key: 'Kyiv', - value: i18n.t('(GMT+02:00) Kyiv'), + value: t('(GMT+02:00) Kyiv'), }, { key: 'Pretoria', - value: i18n.t('(GMT+02:00) Pretoria'), + value: t('(GMT+02:00) Pretoria'), }, { key: 'Riga', - value: i18n.t('(GMT+02:00) Riga'), + value: t('(GMT+02:00) Riga'), }, { key: 'Sofia', - value: i18n.t('(GMT+02:00) Sofia'), + value: t('(GMT+02:00) Sofia'), }, { key: 'Tallinn', - value: i18n.t('(GMT+02:00) Tallinn'), + value: t('(GMT+02:00) Tallinn'), }, { key: 'Vilnius', - value: i18n.t('(GMT+02:00) Vilnius'), + value: t('(GMT+02:00) Vilnius'), }, { key: 'Baghdad', - value: i18n.t('(GMT+03:00) Baghdad'), + value: t('(GMT+03:00) Baghdad'), }, { key: 'Kuwait', - value: i18n.t('(GMT+03:00) Kuwait'), + value: t('(GMT+03:00) Kuwait'), }, { key: 'Minsk', - value: i18n.t('(GMT+03:00) Minsk'), + value: t('(GMT+03:00) Minsk'), }, { key: 'Moscow', - value: i18n.t('(GMT+03:00) Moscow'), + value: t('(GMT+03:00) Moscow'), }, { key: 'Nairobi', - value: i18n.t('(GMT+03:00) Nairobi'), + value: t('(GMT+03:00) Nairobi'), }, { key: 'Riyadh', - value: i18n.t('(GMT+03:00) Riyadh'), + value: t('(GMT+03:00) Riyadh'), }, { key: 'St. Petersburg', - value: i18n.t('(GMT+03:00) St. Petersburg'), + value: t('(GMT+03:00) St. Petersburg'), }, { key: 'Volgograd', - value: i18n.t('(GMT+03:00) Volgograd'), + value: t('(GMT+03:00) Volgograd'), }, { key: 'Tehran', - value: i18n.t('(GMT+03:30) Tehran'), + value: t('(GMT+03:30) Tehran'), }, { key: 'Abu Dhabi', - value: i18n.t('(GMT+04:00) Abu Dhabi'), + value: t('(GMT+04:00) Abu Dhabi'), }, { key: 'Baku', - value: i18n.t('(GMT+04:00) Baku'), + value: t('(GMT+04:00) Baku'), }, { key: 'Muscat', - value: i18n.t('(GMT+04:00) Muscat'), + value: t('(GMT+04:00) Muscat'), }, { key: 'Samara', - value: i18n.t('(GMT+04:00) Samara'), + value: t('(GMT+04:00) Samara'), }, { key: 'Tbilisi', - value: i18n.t('(GMT+04:00) Tbilisi'), + value: t('(GMT+04:00) Tbilisi'), }, { key: 'Yerevan', - value: i18n.t('(GMT+04:00) Yerevan'), + value: t('(GMT+04:00) Yerevan'), }, { key: 'Kabul', - value: i18n.t('(GMT+04:30) Kabul'), + value: t('(GMT+04:30) Kabul'), }, { key: 'Ekaterinburg', - value: i18n.t('(GMT+05:00) Ekaterinburg'), + value: t('(GMT+05:00) Ekaterinburg'), }, { key: 'Islamabad', - value: i18n.t('(GMT+05:00) Islamabad'), + value: t('(GMT+05:00) Islamabad'), }, { key: 'Karachi', - value: i18n.t('(GMT+05:00) Karachi'), + value: t('(GMT+05:00) Karachi'), }, { key: 'Tashkent', - value: i18n.t('(GMT+05:00) Tashkent'), + value: t('(GMT+05:00) Tashkent'), }, { key: 'Chennai', - value: i18n.t('(GMT+05:30) Chennai'), + value: t('(GMT+05:30) Chennai'), }, { key: 'Kolkata', - value: i18n.t('(GMT+05:30) Kolkata'), + value: t('(GMT+05:30) Kolkata'), }, { key: 'Mumbai', - value: i18n.t('(GMT+05:30) Mumbai'), + value: t('(GMT+05:30) Mumbai'), }, { key: 'New Delhi', - value: i18n.t('(GMT+05:30) New Delhi'), + value: t('(GMT+05:30) New Delhi'), }, { key: 'Sri Jayawardenepura', - value: i18n.t('(GMT+05:30) Sri Jayawardenepura'), + value: t('(GMT+05:30) Sri Jayawardenepura'), }, { key: 'Kathmandu', - value: i18n.t('(GMT+05:45) Kathmandu'), + value: t('(GMT+05:45) Kathmandu'), }, { key: 'Almaty', - value: i18n.t('(GMT+06:00) Almaty'), + value: t('(GMT+06:00) Almaty'), }, { key: 'Astana', - value: i18n.t('(GMT+06:00) Astana'), + value: t('(GMT+06:00) Astana'), }, { key: 'Dhaka', - value: i18n.t('(GMT+06:00) Dhaka'), + value: t('(GMT+06:00) Dhaka'), }, { key: 'Urumqi', - value: i18n.t('(GMT+06:00) Urumqi'), + value: t('(GMT+06:00) Urumqi'), }, { key: 'Rangoon', - value: i18n.t('(GMT+06:30) Rangoon'), + value: t('(GMT+06:30) Rangoon'), }, { key: 'Bangkok', - value: i18n.t('(GMT+07:00) Bangkok'), + value: t('(GMT+07:00) Bangkok'), }, { key: 'Hanoi', - value: i18n.t('(GMT+07:00) Hanoi'), + value: t('(GMT+07:00) Hanoi'), }, { key: 'Jakarta', - value: i18n.t('(GMT+07:00) Jakarta'), + value: t('(GMT+07:00) Jakarta'), }, { key: 'Krasnoyarsk', - value: i18n.t('(GMT+07:00) Krasnoyarsk'), + value: t('(GMT+07:00) Krasnoyarsk'), }, { key: 'Novosibirsk', - value: i18n.t('(GMT+07:00) Novosibirsk'), + value: t('(GMT+07:00) Novosibirsk'), }, { key: 'Beijing', - value: i18n.t('(GMT+08:00) Beijing'), + value: t('(GMT+08:00) Beijing'), }, { key: 'Chongqing', - value: i18n.t('(GMT+08:00) Chongqing'), + value: t('(GMT+08:00) Chongqing'), }, { key: 'Hong Kong', - value: i18n.t('(GMT+08:00) Hong Kong'), + value: t('(GMT+08:00) Hong Kong'), }, { key: 'Irkutsk', - value: i18n.t('(GMT+08:00) Irkutsk'), + value: t('(GMT+08:00) Irkutsk'), }, { key: 'Kuala Lumpur', - value: i18n.t('(GMT+08:00) Kuala Lumpur'), + value: t('(GMT+08:00) Kuala Lumpur'), }, { key: 'Perth', - value: i18n.t('(GMT+08:00) Perth'), + value: t('(GMT+08:00) Perth'), }, { key: 'Singapore', - value: i18n.t('(GMT+08:00) Singapore'), + value: t('(GMT+08:00) Singapore'), }, { key: 'Taipei', - value: i18n.t('(GMT+08:00) Taipei'), + value: t('(GMT+08:00) Taipei'), }, { key: 'Ulaanbaatar', - value: i18n.t('(GMT+08:00) Ulaanbaatar'), + value: t('(GMT+08:00) Ulaanbaatar'), }, { key: 'Osaka', - value: i18n.t('(GMT+09:00) Osaka'), + value: t('(GMT+09:00) Osaka'), }, { key: 'Sapporo', - value: i18n.t('(GMT+09:00) Sapporo'), + value: t('(GMT+09:00) Sapporo'), }, { key: 'Seoul', - value: i18n.t('(GMT+09:00) Seoul'), + value: t('(GMT+09:00) Seoul'), }, { key: 'Tokyo', - value: i18n.t('(GMT+09:00) Tokyo'), + value: t('(GMT+09:00) Tokyo'), }, { key: 'Yakutsk', - value: i18n.t('(GMT+09:00) Yakutsk'), + value: t('(GMT+09:00) Yakutsk'), }, { key: 'Adelaide', - value: i18n.t('(GMT+09:30) Adelaide'), + value: t('(GMT+09:30) Adelaide'), }, { key: 'Darwin', - value: i18n.t('(GMT+09:30) Darwin'), + value: t('(GMT+09:30) Darwin'), }, { key: 'Brisbane', - value: i18n.t('(GMT+10:00) Brisbane'), + value: t('(GMT+10:00) Brisbane'), }, { key: 'Canberra', - value: i18n.t('(GMT+10:00) Canberra'), + value: t('(GMT+10:00) Canberra'), }, { key: 'Guam', - value: i18n.t('(GMT+10:00) Guam'), + value: t('(GMT+10:00) Guam'), }, { key: 'Hobart', - value: i18n.t('(GMT+10:00) Hobart'), + value: t('(GMT+10:00) Hobart'), }, { key: 'Melbourne', - value: i18n.t('(GMT+10:00) Melbourne'), + value: t('(GMT+10:00) Melbourne'), }, { key: 'Port Moresby', - value: i18n.t('(GMT+10:00) Port Moresby'), + value: t('(GMT+10:00) Port Moresby'), }, { key: 'Sydney', - value: i18n.t('(GMT+10:00) Sydney'), + value: t('(GMT+10:00) Sydney'), }, { key: 'Vladivostok', - value: i18n.t('(GMT+10:00) Vladivostok'), + value: t('(GMT+10:00) Vladivostok'), }, { key: 'Magadan', - value: i18n.t('(GMT+11:00) Magadan'), + value: t('(GMT+11:00) Magadan'), }, { key: 'New Caledonia', - value: i18n.t('(GMT+11:00) New Caledonia'), + value: t('(GMT+11:00) New Caledonia'), }, { key: 'Solomon Is.', - value: i18n.t('(GMT+11:00) Solomon Is.'), + value: t('(GMT+11:00) Solomon Is.'), }, { key: 'Srednekolymsk', - value: i18n.t('(GMT+11:00) Srednekolymsk'), + value: t('(GMT+11:00) Srednekolymsk'), }, { key: 'Auckland', - value: i18n.t('(GMT+12:00) Auckland'), + value: t('(GMT+12:00) Auckland'), }, { key: 'Fiji', - value: i18n.t('(GMT+12:00) Fiji'), + value: t('(GMT+12:00) Fiji'), }, { key: 'Kamchatka', - value: i18n.t('(GMT+12:00) Kamchatka'), + value: t('(GMT+12:00) Kamchatka'), }, { key: 'Marshall Is.', - value: i18n.t('(GMT+12:00) Marshall Is.'), + value: t('(GMT+12:00) Marshall Is.'), }, { key: 'Wellington', - value: i18n.t('(GMT+12:00) Wellington'), + value: t('(GMT+12:00) Wellington'), }, { key: 'Chatham Is.', - value: i18n.t('(GMT+12:45) Chatham Is.'), + value: t('(GMT+12:45) Chatham Is.'), }, { key: "Nuku'alofa", - value: i18n.t("(GMT+13:00) Nuku'alofa"), + value: t("(GMT+13:00) Nuku'alofa"), }, { key: 'Tokelau Is.', - value: i18n.t('(GMT+13:00) Tokelau Is.'), + value: t('(GMT+13:00) Tokelau Is.'), }, ]; return timezones; diff --git a/src/lib/data/countries.ts b/src/lib/data/countries.ts index 745e74c745..cd3cb064d3 100644 --- a/src/lib/data/countries.ts +++ b/src/lib/data/countries.ts @@ -1,251 +1,253 @@ -import i18n from '../i18n'; +import { TFunction } from 'i18next'; -export const getCountries = (): { name: string; code: string }[] => { +export const getCountries = ( + t: TFunction, +): { name: string; code: string }[] => { const countries = [ - { name: i18n.t('None'), code: 'None' }, - { name: i18n.t('Afghanistan'), code: 'AF' }, - { name: i18n.t('Åland Islands'), code: 'AX' }, - { name: i18n.t('Albania'), code: 'AL' }, - { name: i18n.t('Algeria'), code: 'DZ' }, - { name: i18n.t('American Samoa'), code: 'AS' }, - { name: i18n.t('AndorrA'), code: 'AD' }, - { name: i18n.t('Angola'), code: 'AO' }, - { name: i18n.t('Anguilla'), code: 'AI' }, - { name: i18n.t('Antarctica'), code: 'AQ' }, - { name: i18n.t('Antigua and Barbuda'), code: 'AG' }, - { name: i18n.t('Argentina'), code: 'AR' }, - { name: i18n.t('Armenia'), code: 'AM' }, - { name: i18n.t('Aruba'), code: 'AW' }, - { name: i18n.t('Australia'), code: 'AU' }, - { name: i18n.t('Austria'), code: 'AT' }, - { name: i18n.t('Azerbaijan'), code: 'AZ' }, - { name: i18n.t('Bahamas'), code: 'BS' }, - { name: i18n.t('Bahrain'), code: 'BH' }, - { name: i18n.t('Bangladesh'), code: 'BD' }, - { name: i18n.t('Barbados'), code: 'BB' }, - { name: i18n.t('Belarus'), code: 'BY' }, - { name: i18n.t('Belgium'), code: 'BE' }, - { name: i18n.t('Belize'), code: 'BZ' }, - { name: i18n.t('Benin'), code: 'BJ' }, - { name: i18n.t('Bermuda'), code: 'BM' }, - { name: i18n.t('Bhutan'), code: 'BT' }, - { name: i18n.t('Bolivia'), code: 'BO' }, - { name: i18n.t('Bosnia and Herzegovina'), code: 'BA' }, - { name: i18n.t('Botswana'), code: 'BW' }, - { name: i18n.t('Bouvet Island'), code: 'BV' }, - { name: i18n.t('Brazil'), code: 'BR' }, - { name: i18n.t('British Indian Ocean Territory'), code: 'IO' }, - { name: i18n.t('Brunei Darussalam'), code: 'BN' }, - { name: i18n.t('Bulgaria'), code: 'BG' }, - { name: i18n.t('Burkina Faso'), code: 'BF' }, - { name: i18n.t('Burundi'), code: 'BI' }, - { name: i18n.t('Cambodia'), code: 'KH' }, - { name: i18n.t('Cameroon'), code: 'CM' }, - { name: i18n.t('Canada'), code: 'CA' }, - { name: i18n.t('Cape Verde'), code: 'CV' }, - { name: i18n.t('Cayman Islands'), code: 'KY' }, - { name: i18n.t('Central African Republic'), code: 'CF' }, - { name: i18n.t('Chad'), code: 'TD' }, - { name: i18n.t('Chile'), code: 'CL' }, - { name: i18n.t('China'), code: 'CN' }, - { name: i18n.t('Christmas Island'), code: 'CX' }, - { name: i18n.t('Cocos (Keeling) Islands'), code: 'CC' }, - { name: i18n.t('Colombia'), code: 'CO' }, - { name: i18n.t('Comoros'), code: 'KM' }, - { name: i18n.t('Congo'), code: 'CG' }, - { name: i18n.t('Congo, The Democratic Republic of the'), code: 'CD' }, - { name: i18n.t('Cook Islands'), code: 'CK' }, - { name: i18n.t('Costa Rica'), code: 'CR' }, - { name: i18n.t('Croatia'), code: 'HR' }, - { name: i18n.t('Cuba'), code: 'CU' }, - { name: i18n.t('Cyprus'), code: 'CY' }, - { name: i18n.t('Czech Republic'), code: 'CZ' }, - { name: i18n.t('Denmark'), code: 'DK' }, - { name: i18n.t('Djibouti'), code: 'DJ' }, - { name: i18n.t('Dominica'), code: 'DM' }, - { name: i18n.t('Dominican Republic'), code: 'DO' }, - { name: i18n.t('Ecuador'), code: 'EC' }, - { name: i18n.t('Egypt'), code: 'EG' }, - { name: i18n.t('El Salvador'), code: 'SV' }, - { name: i18n.t('Equatorial Guinea'), code: 'GQ' }, - { name: i18n.t('Eritrea'), code: 'ER' }, - { name: i18n.t('Estonia'), code: 'EE' }, - { name: i18n.t('Ethiopia'), code: 'ET' }, - { name: i18n.t('Falkland Islands (Malvinas)'), code: 'FK' }, - { name: i18n.t('Faroe Islands'), code: 'FO' }, - { name: i18n.t('Fiji'), code: 'FJ' }, - { name: i18n.t('Finland'), code: 'FI' }, - { name: i18n.t('France'), code: 'FR' }, - { name: i18n.t('French Guiana'), code: 'GF' }, - { name: i18n.t('French Polynesia'), code: 'PF' }, - { name: i18n.t('French Southern Territories'), code: 'TF' }, - { name: i18n.t('Gabon'), code: 'GA' }, - { name: i18n.t('Gambia'), code: 'GM' }, - { name: i18n.t('Georgia'), code: 'GE' }, - { name: i18n.t('Germany'), code: 'DE' }, - { name: i18n.t('Ghana'), code: 'GH' }, - { name: i18n.t('Gibraltar'), code: 'GI' }, - { name: i18n.t('Greece'), code: 'GR' }, - { name: i18n.t('Greenland'), code: 'GL' }, - { name: i18n.t('Grenada'), code: 'GD' }, - { name: i18n.t('Guadeloupe'), code: 'GP' }, - { name: i18n.t('Guam'), code: 'GU' }, - { name: i18n.t('Guatemala'), code: 'GT' }, - { name: i18n.t('Guernsey'), code: 'GG' }, - { name: i18n.t('Guinea'), code: 'GN' }, - { name: i18n.t('Guinea-Bissau'), code: 'GW' }, - { name: i18n.t('Guyana'), code: 'GY' }, - { name: i18n.t('Haiti'), code: 'HT' }, - { name: i18n.t('Heard Island and Mcdonald Islands'), code: 'HM' }, - { name: i18n.t('Holy See (Vatican City State)'), code: 'VA' }, - { name: i18n.t('Honduras'), code: 'HN' }, - { name: i18n.t('Hong Kong'), code: 'HK' }, - { name: i18n.t('Hungary'), code: 'HU' }, - { name: i18n.t('Iceland'), code: 'IS' }, - { name: i18n.t('India'), code: 'IN' }, - { name: i18n.t('Indonesia'), code: 'ID' }, - { name: i18n.t('Iran, Islamic Republic Of'), code: 'IR' }, - { name: i18n.t('Iraq'), code: 'IQ' }, - { name: i18n.t('Ireland'), code: 'IE' }, - { name: i18n.t('Isle of Man'), code: 'IM' }, - { name: i18n.t('Israel'), code: 'IL' }, - { name: i18n.t('Italy'), code: 'IT' }, - { name: i18n.t('Jamaica'), code: 'JM' }, - { name: i18n.t('Japan'), code: 'JP' }, - { name: i18n.t('Jersey'), code: 'JE' }, - { name: i18n.t('Jordan'), code: 'JO' }, - { name: i18n.t('Kazakhstan'), code: 'KZ' }, - { name: i18n.t('Kenya'), code: 'KE' }, - { name: i18n.t('Kiribati'), code: 'KI' }, - { name: i18n.t('Korea, Republic of'), code: 'KR' }, - { name: i18n.t('Kuwait'), code: 'KW' }, - { name: i18n.t('Kyrgyzstan'), code: 'KG' }, - { name: i18n.t('Latvia'), code: 'LV' }, - { name: i18n.t('Lebanon'), code: 'LB' }, - { name: i18n.t('Lesotho'), code: 'LS' }, - { name: i18n.t('Liberia'), code: 'LR' }, - { name: i18n.t('Libyan Arab Jamahiriya'), code: 'LY' }, - { name: i18n.t('Liechtenstein'), code: 'LI' }, - { name: i18n.t('Lithuania'), code: 'LT' }, - { name: i18n.t('Luxembourg'), code: 'LU' }, - { name: i18n.t('Macao'), code: 'MO' }, - { name: i18n.t('Macedonia, The Former Yugoslav Republic of'), code: 'MK' }, - { name: i18n.t('Madagascar'), code: 'MG' }, - { name: i18n.t('Malawi'), code: 'MW' }, - { name: i18n.t('Malaysia'), code: 'MY' }, - { name: i18n.t('Maldives'), code: 'MV' }, - { name: i18n.t('Mali'), code: 'ML' }, - { name: i18n.t('Malta'), code: 'MT' }, - { name: i18n.t('Marshall Islands'), code: 'MH' }, - { name: i18n.t('Martinique'), code: 'MQ' }, - { name: i18n.t('Mauritania'), code: 'MR' }, - { name: i18n.t('Mauritius'), code: 'MU' }, - { name: i18n.t('Mayotte'), code: 'YT' }, - { name: i18n.t('Mexico'), code: 'MX' }, - { name: i18n.t('Micronesia, Federated States of'), code: 'FM' }, - { name: i18n.t('Moldova, Republic of'), code: 'MD' }, - { name: i18n.t('Monaco'), code: 'MC' }, - { name: i18n.t('Mongolia'), code: 'MN' }, - { name: i18n.t('Montserrat'), code: 'MS' }, - { name: i18n.t('Morocco'), code: 'MA' }, - { name: i18n.t('Mozambique'), code: 'MZ' }, - { name: i18n.t('Myanmar'), code: 'MM' }, - { name: i18n.t('Namibia'), code: 'NA' }, - { name: i18n.t('Nauru'), code: 'NR' }, - { name: i18n.t('Nepal'), code: 'NP' }, - { name: i18n.t('Netherlands'), code: 'NL' }, - { name: i18n.t('Netherlands Antilles'), code: 'AN' }, - { name: i18n.t('New Caledonia'), code: 'NC' }, - { name: i18n.t('New Zealand'), code: 'NZ' }, - { name: i18n.t('Nicaragua'), code: 'NI' }, - { name: i18n.t('Niger'), code: 'NE' }, - { name: i18n.t('Nigeria'), code: 'NG' }, - { name: i18n.t('Niue'), code: 'NU' }, - { name: i18n.t('Norfolk Island'), code: 'NF' }, - { name: i18n.t('Northern Mariana Islands'), code: 'MP' }, - { name: i18n.t('Norway'), code: 'NO' }, - { name: i18n.t('Oman'), code: 'OM' }, - { name: i18n.t('Pakistan'), code: 'PK' }, - { name: i18n.t('Palau'), code: 'PW' }, - { name: i18n.t('Palestinian Territory, Occupied'), code: 'PS' }, - { name: i18n.t('Panama'), code: 'PA' }, - { name: i18n.t('Papua New Guinea'), code: 'PG' }, - { name: i18n.t('Paraguay'), code: 'PY' }, - { name: i18n.t('Peru'), code: 'PE' }, - { name: i18n.t('Philippines'), code: 'PH' }, - { name: i18n.t('Pitcairn'), code: 'PN' }, - { name: i18n.t('Poland'), code: 'PL' }, - { name: i18n.t('Portugal'), code: 'PT' }, - { name: i18n.t('Puerto Rico'), code: 'PR' }, - { name: i18n.t('Qatar'), code: 'QA' }, - { name: i18n.t('Reunion'), code: 'RE' }, - { name: i18n.t('Romania'), code: 'RO' }, - { name: i18n.t('Russian Federation'), code: 'RU' }, - { name: i18n.t('RWANDA'), code: 'RW' }, - { name: i18n.t('Saint Helena'), code: 'SH' }, - { name: i18n.t('Saint Kitts and Nevis'), code: 'KN' }, - { name: i18n.t('Saint Lucia'), code: 'LC' }, - { name: i18n.t('Saint Pierre and Miquelon'), code: 'PM' }, - { name: i18n.t('Saint Vincent and the Grenadines'), code: 'VC' }, - { name: i18n.t('Samoa'), code: 'WS' }, - { name: i18n.t('San Marino'), code: 'SM' }, - { name: i18n.t('Sao Tome and Principe'), code: 'ST' }, - { name: i18n.t('Saudi Arabia'), code: 'SA' }, - { name: i18n.t('Senegal'), code: 'SN' }, - { name: i18n.t('Serbia and Montenegro'), code: 'CS' }, - { name: i18n.t('Seychelles'), code: 'SC' }, - { name: i18n.t('Sierra Leone'), code: 'SL' }, - { name: i18n.t('Singapore'), code: 'SG' }, - { name: i18n.t('Slovakia'), code: 'SK' }, - { name: i18n.t('Slovenia'), code: 'SI' }, - { name: i18n.t('Solomon Islands'), code: 'SB' }, - { name: i18n.t('Somalia'), code: 'SO' }, - { name: i18n.t('South Africa'), code: 'ZA' }, + { name: t('None'), code: 'None' }, + { name: t('Afghanistan'), code: 'AF' }, + { name: t('Åland Islands'), code: 'AX' }, + { name: t('Albania'), code: 'AL' }, + { name: t('Algeria'), code: 'DZ' }, + { name: t('American Samoa'), code: 'AS' }, + { name: t('AndorrA'), code: 'AD' }, + { name: t('Angola'), code: 'AO' }, + { name: t('Anguilla'), code: 'AI' }, + { name: t('Antarctica'), code: 'AQ' }, + { name: t('Antigua and Barbuda'), code: 'AG' }, + { name: t('Argentina'), code: 'AR' }, + { name: t('Armenia'), code: 'AM' }, + { name: t('Aruba'), code: 'AW' }, + { name: t('Australia'), code: 'AU' }, + { name: t('Austria'), code: 'AT' }, + { name: t('Azerbaijan'), code: 'AZ' }, + { name: t('Bahamas'), code: 'BS' }, + { name: t('Bahrain'), code: 'BH' }, + { name: t('Bangladesh'), code: 'BD' }, + { name: t('Barbados'), code: 'BB' }, + { name: t('Belarus'), code: 'BY' }, + { name: t('Belgium'), code: 'BE' }, + { name: t('Belize'), code: 'BZ' }, + { name: t('Benin'), code: 'BJ' }, + { name: t('Bermuda'), code: 'BM' }, + { name: t('Bhutan'), code: 'BT' }, + { name: t('Bolivia'), code: 'BO' }, + { name: t('Bosnia and Herzegovina'), code: 'BA' }, + { name: t('Botswana'), code: 'BW' }, + { name: t('Bouvet Island'), code: 'BV' }, + { name: t('Brazil'), code: 'BR' }, + { name: t('British Indian Ocean Territory'), code: 'IO' }, + { name: t('Brunei Darussalam'), code: 'BN' }, + { name: t('Bulgaria'), code: 'BG' }, + { name: t('Burkina Faso'), code: 'BF' }, + { name: t('Burundi'), code: 'BI' }, + { name: t('Cambodia'), code: 'KH' }, + { name: t('Cameroon'), code: 'CM' }, + { name: t('Canada'), code: 'CA' }, + { name: t('Cape Verde'), code: 'CV' }, + { name: t('Cayman Islands'), code: 'KY' }, + { name: t('Central African Republic'), code: 'CF' }, + { name: t('Chad'), code: 'TD' }, + { name: t('Chile'), code: 'CL' }, + { name: t('China'), code: 'CN' }, + { name: t('Christmas Island'), code: 'CX' }, + { name: t('Cocos (Keeling) Islands'), code: 'CC' }, + { name: t('Colombia'), code: 'CO' }, + { name: t('Comoros'), code: 'KM' }, + { name: t('Congo'), code: 'CG' }, + { name: t('Congo, The Democratic Republic of the'), code: 'CD' }, + { name: t('Cook Islands'), code: 'CK' }, + { name: t('Costa Rica'), code: 'CR' }, + { name: t('Croatia'), code: 'HR' }, + { name: t('Cuba'), code: 'CU' }, + { name: t('Cyprus'), code: 'CY' }, + { name: t('Czech Republic'), code: 'CZ' }, + { name: t('Denmark'), code: 'DK' }, + { name: t('Djibouti'), code: 'DJ' }, + { name: t('Dominica'), code: 'DM' }, + { name: t('Dominican Republic'), code: 'DO' }, + { name: t('Ecuador'), code: 'EC' }, + { name: t('Egypt'), code: 'EG' }, + { name: t('El Salvador'), code: 'SV' }, + { name: t('Equatorial Guinea'), code: 'GQ' }, + { name: t('Eritrea'), code: 'ER' }, + { name: t('Estonia'), code: 'EE' }, + { name: t('Ethiopia'), code: 'ET' }, + { name: t('Falkland Islands (Malvinas)'), code: 'FK' }, + { name: t('Faroe Islands'), code: 'FO' }, + { name: t('Fiji'), code: 'FJ' }, + { name: t('Finland'), code: 'FI' }, + { name: t('France'), code: 'FR' }, + { name: t('French Guiana'), code: 'GF' }, + { name: t('French Polynesia'), code: 'PF' }, + { name: t('French Southern Territories'), code: 'TF' }, + { name: t('Gabon'), code: 'GA' }, + { name: t('Gambia'), code: 'GM' }, + { name: t('Georgia'), code: 'GE' }, + { name: t('Germany'), code: 'DE' }, + { name: t('Ghana'), code: 'GH' }, + { name: t('Gibraltar'), code: 'GI' }, + { name: t('Greece'), code: 'GR' }, + { name: t('Greenland'), code: 'GL' }, + { name: t('Grenada'), code: 'GD' }, + { name: t('Guadeloupe'), code: 'GP' }, + { name: t('Guam'), code: 'GU' }, + { name: t('Guatemala'), code: 'GT' }, + { name: t('Guernsey'), code: 'GG' }, + { name: t('Guinea'), code: 'GN' }, + { name: t('Guinea-Bissau'), code: 'GW' }, + { name: t('Guyana'), code: 'GY' }, + { name: t('Haiti'), code: 'HT' }, + { name: t('Heard Island and Mcdonald Islands'), code: 'HM' }, + { name: t('Holy See (Vatican City State)'), code: 'VA' }, + { name: t('Honduras'), code: 'HN' }, + { name: t('Hong Kong'), code: 'HK' }, + { name: t('Hungary'), code: 'HU' }, + { name: t('Iceland'), code: 'IS' }, + { name: t('India'), code: 'IN' }, + { name: t('Indonesia'), code: 'ID' }, + { name: t('Iran, Islamic Republic Of'), code: 'IR' }, + { name: t('Iraq'), code: 'IQ' }, + { name: t('Ireland'), code: 'IE' }, + { name: t('Isle of Man'), code: 'IM' }, + { name: t('Israel'), code: 'IL' }, + { name: t('Italy'), code: 'IT' }, + { name: t('Jamaica'), code: 'JM' }, + { name: t('Japan'), code: 'JP' }, + { name: t('Jersey'), code: 'JE' }, + { name: t('Jordan'), code: 'JO' }, + { name: t('Kazakhstan'), code: 'KZ' }, + { name: t('Kenya'), code: 'KE' }, + { name: t('Kiribati'), code: 'KI' }, + { name: t('Korea, Republic of'), code: 'KR' }, + { name: t('Kuwait'), code: 'KW' }, + { name: t('Kyrgyzstan'), code: 'KG' }, + { name: t('Latvia'), code: 'LV' }, + { name: t('Lebanon'), code: 'LB' }, + { name: t('Lesotho'), code: 'LS' }, + { name: t('Liberia'), code: 'LR' }, + { name: t('Libyan Arab Jamahiriya'), code: 'LY' }, + { name: t('Liechtenstein'), code: 'LI' }, + { name: t('Lithuania'), code: 'LT' }, + { name: t('Luxembourg'), code: 'LU' }, + { name: t('Macao'), code: 'MO' }, + { name: t('Macedonia, The Former Yugoslav Republic of'), code: 'MK' }, + { name: t('Madagascar'), code: 'MG' }, + { name: t('Malawi'), code: 'MW' }, + { name: t('Malaysia'), code: 'MY' }, + { name: t('Maldives'), code: 'MV' }, + { name: t('Mali'), code: 'ML' }, + { name: t('Malta'), code: 'MT' }, + { name: t('Marshall Islands'), code: 'MH' }, + { name: t('Martinique'), code: 'MQ' }, + { name: t('Mauritania'), code: 'MR' }, + { name: t('Mauritius'), code: 'MU' }, + { name: t('Mayotte'), code: 'YT' }, + { name: t('Mexico'), code: 'MX' }, + { name: t('Micronesia, Federated States of'), code: 'FM' }, + { name: t('Moldova, Republic of'), code: 'MD' }, + { name: t('Monaco'), code: 'MC' }, + { name: t('Mongolia'), code: 'MN' }, + { name: t('Montserrat'), code: 'MS' }, + { name: t('Morocco'), code: 'MA' }, + { name: t('Mozambique'), code: 'MZ' }, + { name: t('Myanmar'), code: 'MM' }, + { name: t('Namibia'), code: 'NA' }, + { name: t('Nauru'), code: 'NR' }, + { name: t('Nepal'), code: 'NP' }, + { name: t('Netherlands'), code: 'NL' }, + { name: t('Netherlands Antilles'), code: 'AN' }, + { name: t('New Caledonia'), code: 'NC' }, + { name: t('New Zealand'), code: 'NZ' }, + { name: t('Nicaragua'), code: 'NI' }, + { name: t('Niger'), code: 'NE' }, + { name: t('Nigeria'), code: 'NG' }, + { name: t('Niue'), code: 'NU' }, + { name: t('Norfolk Island'), code: 'NF' }, + { name: t('Northern Mariana Islands'), code: 'MP' }, + { name: t('Norway'), code: 'NO' }, + { name: t('Oman'), code: 'OM' }, + { name: t('Pakistan'), code: 'PK' }, + { name: t('Palau'), code: 'PW' }, + { name: t('Palestinian Territory, Occupied'), code: 'PS' }, + { name: t('Panama'), code: 'PA' }, + { name: t('Papua New Guinea'), code: 'PG' }, + { name: t('Paraguay'), code: 'PY' }, + { name: t('Peru'), code: 'PE' }, + { name: t('Philippines'), code: 'PH' }, + { name: t('Pitcairn'), code: 'PN' }, + { name: t('Poland'), code: 'PL' }, + { name: t('Portugal'), code: 'PT' }, + { name: t('Puerto Rico'), code: 'PR' }, + { name: t('Qatar'), code: 'QA' }, + { name: t('Reunion'), code: 'RE' }, + { name: t('Romania'), code: 'RO' }, + { name: t('Russian Federation'), code: 'RU' }, + { name: t('RWANDA'), code: 'RW' }, + { name: t('Saint Helena'), code: 'SH' }, + { name: t('Saint Kitts and Nevis'), code: 'KN' }, + { name: t('Saint Lucia'), code: 'LC' }, + { name: t('Saint Pierre and Miquelon'), code: 'PM' }, + { name: t('Saint Vincent and the Grenadines'), code: 'VC' }, + { name: t('Samoa'), code: 'WS' }, + { name: t('San Marino'), code: 'SM' }, + { name: t('Sao Tome and Principe'), code: 'ST' }, + { name: t('Saudi Arabia'), code: 'SA' }, + { name: t('Senegal'), code: 'SN' }, + { name: t('Serbia and Montenegro'), code: 'CS' }, + { name: t('Seychelles'), code: 'SC' }, + { name: t('Sierra Leone'), code: 'SL' }, + { name: t('Singapore'), code: 'SG' }, + { name: t('Slovakia'), code: 'SK' }, + { name: t('Slovenia'), code: 'SI' }, + { name: t('Solomon Islands'), code: 'SB' }, + { name: t('Somalia'), code: 'SO' }, + { name: t('South Africa'), code: 'ZA' }, { - name: i18n.t('South Georgia and the South Sandwich Islands'), + name: t('South Georgia and the South Sandwich Islands'), code: 'GS', }, - { name: i18n.t('Spain'), code: 'ES' }, - { name: i18n.t('Sri Lanka'), code: 'LK' }, - { name: i18n.t('Sudan'), code: 'SD' }, - { name: i18n.t('Suriname'), code: 'SR' }, - { name: i18n.t('Svalbard and Jan Mayen'), code: 'SJ' }, - { name: i18n.t('Swaziland'), code: 'SZ' }, - { name: i18n.t('Sweden'), code: 'SE' }, - { name: i18n.t('Switzerland'), code: 'CH' }, - { name: i18n.t('Syrian Arab Republic'), code: 'SY' }, - { name: i18n.t('Taiwan, Province of China'), code: 'TW' }, - { name: i18n.t('Tajikistan'), code: 'TJ' }, - { name: i18n.t('Tanzania, United Republic of'), code: 'TZ' }, - { name: i18n.t('Thailand'), code: 'TH' }, - { name: i18n.t('Timor-Leste'), code: 'TL' }, - { name: i18n.t('Togo'), code: 'TG' }, - { name: i18n.t('Tokelau'), code: 'TK' }, - { name: i18n.t('Tonga'), code: 'TO' }, - { name: i18n.t('Trinidad and Tobago'), code: 'TT' }, - { name: i18n.t('Tunisia'), code: 'TN' }, - { name: i18n.t('Turkey'), code: 'TR' }, - { name: i18n.t('Turkmenistan'), code: 'TM' }, - { name: i18n.t('Turks and Caicos Islands'), code: 'TC' }, - { name: i18n.t('Tuvalu'), code: 'TV' }, - { name: i18n.t('Uganda'), code: 'UG' }, - { name: i18n.t('Ukraine'), code: 'UA' }, - { name: i18n.t('United Arab Emirates'), code: 'AE' }, - { name: i18n.t('United Kingdom'), code: 'GB' }, - { name: i18n.t('United States'), code: 'US' }, - { name: i18n.t('United States Minor Outlying Islands'), code: 'UM' }, - { name: i18n.t('Uruguay'), code: 'UY' }, - { name: i18n.t('Uzbekistan'), code: 'UZ' }, - { name: i18n.t('Vanuatu'), code: 'VU' }, - { name: i18n.t('Venezuela'), code: 'VE' }, - { name: i18n.t('Viet Nam'), code: 'VN' }, - { name: i18n.t('Virgin Islands, British'), code: 'VG' }, - { name: i18n.t('Virgin Islands, U.S.'), code: 'VI' }, - { name: i18n.t('Wallis and Futuna'), code: 'WF' }, - { name: i18n.t('Western Sahara'), code: 'EH' }, - { name: i18n.t('Yemen'), code: 'YE' }, - { name: i18n.t('Zambia'), code: 'ZM' }, - { name: i18n.t('Zimbabw'), code: 'ZN' }, + { name: t('Spain'), code: 'ES' }, + { name: t('Sri Lanka'), code: 'LK' }, + { name: t('Sudan'), code: 'SD' }, + { name: t('Suriname'), code: 'SR' }, + { name: t('Svalbard and Jan Mayen'), code: 'SJ' }, + { name: t('Swaziland'), code: 'SZ' }, + { name: t('Sweden'), code: 'SE' }, + { name: t('Switzerland'), code: 'CH' }, + { name: t('Syrian Arab Republic'), code: 'SY' }, + { name: t('Taiwan, Province of China'), code: 'TW' }, + { name: t('Tajikistan'), code: 'TJ' }, + { name: t('Tanzania, United Republic of'), code: 'TZ' }, + { name: t('Thailand'), code: 'TH' }, + { name: t('Timor-Leste'), code: 'TL' }, + { name: t('Togo'), code: 'TG' }, + { name: t('Tokelau'), code: 'TK' }, + { name: t('Tonga'), code: 'TO' }, + { name: t('Trinidad and Tobago'), code: 'TT' }, + { name: t('Tunisia'), code: 'TN' }, + { name: t('Turkey'), code: 'TR' }, + { name: t('Turkmenistan'), code: 'TM' }, + { name: t('Turks and Caicos Islands'), code: 'TC' }, + { name: t('Tuvalu'), code: 'TV' }, + { name: t('Uganda'), code: 'UG' }, + { name: t('Ukraine'), code: 'UA' }, + { name: t('United Arab Emirates'), code: 'AE' }, + { name: t('United Kingdom'), code: 'GB' }, + { name: t('United States'), code: 'US' }, + { name: t('United States Minor Outlying Islands'), code: 'UM' }, + { name: t('Uruguay'), code: 'UY' }, + { name: t('Uzbekistan'), code: 'UZ' }, + { name: t('Vanuatu'), code: 'VU' }, + { name: t('Venezuela'), code: 'VE' }, + { name: t('Viet Nam'), code: 'VN' }, + { name: t('Virgin Islands, British'), code: 'VG' }, + { name: t('Virgin Islands, U.S.'), code: 'VI' }, + { name: t('Wallis and Futuna'), code: 'WF' }, + { name: t('Western Sahara'), code: 'EH' }, + { name: t('Yemen'), code: 'YE' }, + { name: t('Zambia'), code: 'ZM' }, + { name: t('Zimbabw'), code: 'ZN' }, ]; return countries; };