From fa6440a4c4fc4329323af39a7a70021efc43c126 Mon Sep 17 00:00:00 2001 From: Andreas Kienle Date: Tue, 29 Jul 2025 11:01:45 +0200 Subject: [PATCH] refactor: i18n --- cypress/support/commands.ts | 2 +- i18n.ts | 22 ----- .../Dialogs/CreateProjectDialog.cy.tsx | 8 +- .../Dialogs/CreateProjectDialogContainer.tsx | 7 +- .../CreateWorkspaceDialogContainer.tsx | 7 +- ...eateManagedControlPlaneWizardContainer.tsx | 3 +- src/lib/api/validations/schemas.ts | 93 ++++++++++--------- src/main.tsx | 2 +- src/utils/i18n/i18n.ts | 19 ++++ tsconfig.json | 2 +- 10 files changed, 86 insertions(+), 79 deletions(-) delete mode 100644 i18n.ts create mode 100644 src/utils/i18n/i18n.ts diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 35938801..4ae0de28 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -1,2 +1,2 @@ import '@ui5/webcomponents-cypress-commands'; -import "../../i18n"; \ No newline at end of file +import "../../src/utils/i18n/i18n"; \ No newline at end of file diff --git a/i18n.ts b/i18n.ts deleted file mode 100644 index 090278ab..00000000 --- a/i18n.ts +++ /dev/null @@ -1,22 +0,0 @@ -import i18n from 'i18next'; -import { initReactI18next } from 'react-i18next'; - -import translationEN from './public/locales/en.json' - -const resources = { - en: { - translation: translationEN - } -}; - -i18n - .use(initReactI18next) - .init({ - resources, - lng: "en", - fallbackLng: 'en', - debug: true, - }); - - -export default i18n; \ No newline at end of file diff --git a/src/components/Dialogs/CreateProjectDialog.cy.tsx b/src/components/Dialogs/CreateProjectDialog.cy.tsx index 279e516e..4bd70c99 100644 --- a/src/components/Dialogs/CreateProjectDialog.cy.tsx +++ b/src/components/Dialogs/CreateProjectDialog.cy.tsx @@ -1,21 +1,25 @@ -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useMemo } from 'react'; import { CreateProjectWorkspaceDialog, OnCreatePayload } from './CreateProjectWorkspaceDialog'; import { MemberRoles } from '../../lib/api/types/shared/members'; import { ErrorDialogHandle } from '../Shared/ErrorMessageBox'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import { validationSchemaProjectWorkspace } from '../../lib/api/validations/schemas.ts'; +import { createProjectWorkspaceSchema } from '../../lib/api/validations/schemas.ts'; import { CreateDialogProps } from './CreateWorkspaceDialogContainer.tsx'; +import { useTranslation } from 'react-i18next'; export const CreateProjectWorkspaceDialogWrapper: React.FC<{ // eslint-disable-next-line @typescript-eslint/no-explicit-any spyFormBody?: (data: any) => object; }> = ({ spyFormBody }) => { const [isOpen, setIsOpen] = useState(true); + const { t } = useTranslation(); const errorDialogRef = useRef(null); + const validationSchemaProjectWorkspace = useMemo(() => createProjectWorkspaceSchema(t), [t]); + const { register, handleSubmit, diff --git a/src/components/Dialogs/CreateProjectDialogContainer.tsx b/src/components/Dialogs/CreateProjectDialogContainer.tsx index 4094ec03..11eb95f4 100644 --- a/src/components/Dialogs/CreateProjectDialogContainer.tsx +++ b/src/components/Dialogs/CreateProjectDialogContainer.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useApiResourceMutation } from '../../lib/api/useApiResource'; import { ErrorDialogHandle } from '../Shared/ErrorMessageBox.tsx'; import { APIError } from '../../lib/api/error'; @@ -12,7 +12,7 @@ import { useTranslation } from 'react-i18next'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { CreateProject, CreateProjectResource, CreateProjectType } from '../../lib/api/types/crate/createProject.ts'; -import { validationSchemaProjectWorkspace } from '../../lib/api/validations/schemas.ts'; +import { createProjectWorkspaceSchema } from '../../lib/api/validations/schemas.ts'; import { CreateDialogProps } from './CreateWorkspaceDialogContainer.tsx'; export function CreateProjectDialogContainer({ @@ -22,6 +22,8 @@ export function CreateProjectDialogContainer({ isOpen: boolean; setIsOpen: (isOpen: boolean) => void; }) { + const { t } = useTranslation(); + const validationSchemaProjectWorkspace = useMemo(() => createProjectWorkspaceSchema(t), [t]); const { watch, register, @@ -39,7 +41,6 @@ export function CreateProjectDialogContainer({ members: [], }, }); - const { t } = useTranslation(); const { user } = useAuthOnboarding(); const username = user?.email; diff --git a/src/components/Dialogs/CreateWorkspaceDialogContainer.tsx b/src/components/Dialogs/CreateWorkspaceDialogContainer.tsx index 6306343e..bca3b319 100644 --- a/src/components/Dialogs/CreateWorkspaceDialogContainer.tsx +++ b/src/components/Dialogs/CreateWorkspaceDialogContainer.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useApiResourceMutation, useRevalidateApiResource } from '../../lib/api/useApiResource'; import { ErrorDialogHandle } from '../Shared/ErrorMessageBox.tsx'; import { APIError } from '../../lib/api/error'; @@ -16,7 +16,7 @@ import { Member, MemberRoles } from '../../lib/api/types/shared/members.ts'; import { useTranslation } from 'react-i18next'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; -import { validationSchemaProjectWorkspace } from '../../lib/api/validations/schemas.ts'; +import { createProjectWorkspaceSchema } from '../../lib/api/validations/schemas.ts'; import { ComponentsListItem } from '../../lib/api/types/crate/createManagedControlPlane.ts'; export type CreateDialogProps = { @@ -37,6 +37,8 @@ export function CreateWorkspaceDialogContainer({ setIsOpen: (isOpen: boolean) => void; project?: string; }) { + const { t } = useTranslation(); + const validationSchemaProjectWorkspace = useMemo(() => createProjectWorkspaceSchema(t), [t]); const { register, handleSubmit, @@ -54,7 +56,6 @@ export function CreateWorkspaceDialogContainer({ chargingTargetType: '', }, }); - const { t } = useTranslation(); const { user } = useAuthOnboarding(); const username = user?.email; diff --git a/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx b/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx index adcfa51b..17707c24 100644 --- a/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx +++ b/src/components/Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx @@ -23,7 +23,7 @@ import { useTranslation } from 'react-i18next'; import { useAuthOnboarding } from '../../../spaces/onboarding/auth/AuthContextOnboarding.tsx'; import { ErrorDialog, ErrorDialogHandle } from '../../Shared/ErrorMessageBox.tsx'; import { CreateDialogProps } from '../../Dialogs/CreateWorkspaceDialogContainer.tsx'; -import { validationSchemaCreateManagedControlPlane } from '../../../lib/api/validations/schemas.ts'; +import { createManagedControlPlaneSchema } from '../../../lib/api/validations/schemas.ts'; import { Member, MemberRoles } from '../../../lib/api/types/shared/members.ts'; import { useApiResourceMutation } from '../../../lib/api/useApiResource.ts'; import { @@ -62,6 +62,7 @@ export const CreateManagedControlPlaneWizardContainer: FC(null); const [selectedStep, setSelectedStep] = useState('metadata'); + const validationSchemaCreateManagedControlPlane = useMemo(() => createManagedControlPlaneSchema(t), [t]); const { register, diff --git a/src/lib/api/validations/schemas.ts b/src/lib/api/validations/schemas.ts index 8d47b64f..754a20c2 100644 --- a/src/lib/api/validations/schemas.ts +++ b/src/lib/api/validations/schemas.ts @@ -1,56 +1,59 @@ import { z } from 'zod'; import { Member } from '../types/shared/members.ts'; -import i18n from '../../../../i18n.ts'; +import { TFunction } from 'i18next'; import { btpChargingTargetRegex, managedControlPlaneNameRegex, projectWorkspaceNameRegex } from './regex.ts'; -const { t } = i18n; - const member = z.custom(); // Shared superRefine helper for charging target validation -function validateChargingTarget( - data: T, - ctx: z.RefinementCtx, +function createValidateChargingTarget( + t: TFunction, ) { - if (data.chargingTargetType && data.chargingTarget && !btpChargingTargetRegex.test(data.chargingTarget ?? '')) { - ctx.addIssue({ - path: ['chargingTarget'], - code: z.ZodIssueCode.custom, - message: t('validationErrors.notValidChargingTargetFormat'), - }); - } else if (data.chargingTargetType && !data.chargingTarget) { - ctx.addIssue({ - path: ['chargingTarget'], - code: z.ZodIssueCode.custom, - message: t('validationErrors.required'), - }); - } + return (data: T, ctx: z.RefinementCtx) => { + if (data.chargingTargetType && data.chargingTarget && !btpChargingTargetRegex.test(data.chargingTarget ?? '')) { + ctx.addIssue({ + path: ['chargingTarget'], + code: z.ZodIssueCode.custom, + message: t('validationErrors.notValidChargingTargetFormat'), + }); + } else if (data.chargingTargetType && !data.chargingTarget) { + ctx.addIssue({ + path: ['chargingTarget'], + code: z.ZodIssueCode.custom, + message: t('validationErrors.required'), + }); + } + }; } -export const validationSchemaProjectWorkspace = z - .object({ - name: z - .string() - .min(1, t('validationErrors.required')) - .regex(projectWorkspaceNameRegex, t('validationErrors.properFormatting')) - .max(25, t('validationErrors.maxChars', { maxLength: 25 })), - displayName: z.string().optional(), - chargingTarget: z.string().optional(), - chargingTargetType: z.string().optional(), - members: z.array(member).refine((members) => members?.length > 0), - }) - .superRefine(validateChargingTarget); +export function createProjectWorkspaceSchema(t: TFunction) { + return z + .object({ + name: z + .string() + .min(1, t('validationErrors.required')) + .regex(projectWorkspaceNameRegex, t('validationErrors.properFormatting')) + .max(25, t('validationErrors.maxChars', { maxLength: 25 })), + displayName: z.string().optional(), + chargingTarget: z.string().optional(), + chargingTargetType: z.string().optional(), + members: z.array(member).refine((members) => members?.length > 0), + }) + .superRefine(createValidateChargingTarget(t)); +} -export const validationSchemaCreateManagedControlPlane = z - .object({ - name: z - .string() - .min(1, t('validationErrors.required')) - .regex(managedControlPlaneNameRegex, t('validationErrors.properFormattingLowercase')) - .max(36, t('validationErrors.maxChars', { maxLength: 36 })), - displayName: z.string().optional(), - chargingTarget: z.string().optional(), - chargingTargetType: z.string().optional(), - members: z.array(member), - }) - .superRefine(validateChargingTarget); +export function createManagedControlPlaneSchema(t: TFunction) { + return z + .object({ + name: z + .string() + .min(1, t('validationErrors.required')) + .regex(managedControlPlaneNameRegex, t('validationErrors.properFormattingLowercase')) + .max(36, t('validationErrors.maxChars', { maxLength: 36 })), + displayName: z.string().optional(), + chargingTarget: z.string().optional(), + chargingTargetType: z.string().optional(), + members: z.array(member), + }) + .superRefine(createValidateChargingTarget(t)); +} diff --git a/src/main.tsx b/src/main.tsx index 5ad8a947..bd5a26d9 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -8,7 +8,7 @@ import { CopyButtonProvider } from './context/CopyButtonContext.tsx'; import { FrontendConfigProvider } from './context/FrontendConfigContext.tsx'; import '@ui5/webcomponents-react/dist/Assets'; //used for loading themes import { ThemeManager } from './components/ThemeManager.tsx'; -import '.././i18n.ts'; +import './utils/i18n/i18n.ts'; import './utils/i18n/timeAgo'; import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; import { ApolloClientProvider } from './spaces/onboarding/services/ApolloClientProvider/ApolloClientProvider.tsx'; diff --git a/src/utils/i18n/i18n.ts b/src/utils/i18n/i18n.ts new file mode 100644 index 00000000..55532412 --- /dev/null +++ b/src/utils/i18n/i18n.ts @@ -0,0 +1,19 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +import translationEN from '../../../public/locales/en.json'; + +const resources = { + en: { + translation: translationEN, + }, +}; + +i18n.use(initReactI18next).init({ + resources, + lng: 'en', + fallbackLng: 'en', + debug: process.env.NODE_ENV === 'development', +}); + +export default i18n; diff --git a/tsconfig.json b/tsconfig.json index b022f0df..0eb03a0c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,7 @@ "noFallthroughCasesInSwitch": true, "types": ["node", "cypress"] }, - "include": ["src", "cypress.d.ts", "server.js", "i18n.ts", "server/**/*"], + "include": ["src", "cypress.d.ts", "server.js", "server/**/*"], "references": [ { "path": "./tsconfig.node.json"