diff --git a/shared/helpers/openapi/__snapshots__/generateOpenapi.test.ts.snap b/shared/helpers/openapi/__snapshots__/generateOpenapi.test.ts.snap index 237a04b5e..6c0870907 100644 --- a/shared/helpers/openapi/__snapshots__/generateOpenapi.test.ts.snap +++ b/shared/helpers/openapi/__snapshots__/generateOpenapi.test.ts.snap @@ -63,11 +63,13 @@ exports[`generateOpenApiSchema > should generate proper schema 1`] = ` "applicant_first_name": { "description": "Prenom du candidat", "maxLength": 50, + "minLength": 1, "type": "string", }, "applicant_last_name": { "description": "Nom du candidat", "maxLength": 50, + "minLength": 1, "type": "string", }, "applicant_phone": { diff --git a/shared/helpers/zodHelpers/zodPrimitives.ts b/shared/helpers/zodHelpers/zodPrimitives.ts index b304152d7..2e356e88a 100644 --- a/shared/helpers/zodHelpers/zodPrimitives.ts +++ b/shared/helpers/zodHelpers/zodPrimitives.ts @@ -40,7 +40,7 @@ export const extensions = { .string() .trim() .transform((value) => removeUrlsFromText(value)), /// is it a phone extensions still ?? - telephone: z.string().trim().refine(validatePhone, { message: "Phone number is not valid. Please use international format prefix (example: +33 for France)" }), + telephone: z.string().trim().refine(validatePhone, { message: "Invalid Phone Number: please use international format (+33XXX...) or national format (06XXX...)" }), code_naf: () => z.preprocess( (v: unknown) => (typeof v === "string" ? v.replace(".", "") : v), // parfois, le code naf contient un point diff --git a/shared/i18n/i18n.ts b/shared/i18n/i18n.ts new file mode 100644 index 000000000..7a9f56b86 --- /dev/null +++ b/shared/i18n/i18n.ts @@ -0,0 +1,20 @@ +import i18next from "i18next" +import { z } from "zod" +import { makeZodI18nMap, zodI18nMap } from "zod-i18n-map" +import zodEn from "zod-i18n-map/locales/en/zod.json" +import zodFr from "zod-i18n-map/locales/fr/zod.json" + +// lng and resources key depend on your locale. +i18next.init({ + lng: "en", + resources: { + fr: { zod: zodFr }, + en: { zod: zodEn }, + }, +}) +z.setErrorMap(zodI18nMap) + +export const setZodLanguage = (language: "fr" | "en") => { + i18next.changeLanguage(language) + z.setErrorMap(makeZodI18nMap({ t: i18next.t })) +} diff --git a/shared/models/applications.model.ts b/shared/models/applications.model.ts index 20721f4ce..5d37ce7e0 100644 --- a/shared/models/applications.model.ts +++ b/shared/models/applications.model.ts @@ -1,3 +1,4 @@ +import { ObjectId } from "bson" import { Jsonify } from "type-fest" import { LBA_ITEM_TYPE, LBA_ITEM_TYPE_OLD, allLbaItemType, allLbaItemTypeOLD } from "../constants/lbaitem" @@ -21,18 +22,20 @@ export enum ApplicationScanStatus { export const ZApplication = z .object({ _id: zObjectId, - applicant_email: z.string({ required_error: "⚠ L'adresse e-mail est obligatoire" }).email("⚠ Adresse e-mail invalide").describe("Email du candidat"), + applicant_email: z.string().email().describe("Email du candidat"), applicant_first_name: z - .string({ required_error: "⚠ Le prénom est obligatoire" }) + .string() + .min(1) .max(50) .transform((value) => removeUrlsFromText(value)) .describe("Prenom du candidat"), applicant_last_name: z - .string({ required_error: "⚠ Le nom est obligatoire" }) + .string() + .min(1) .max(50) .transform((value) => removeUrlsFromText(value)) .describe("Nom du candidat"), - applicant_phone: extensions.phone().describe("Téléphone du candidat"), + applicant_phone: extensions.telephone.describe("Téléphone du candidat"), applicant_attachment_name: z .string({ required_error: "⚠ La pièce jointe est obligatoire" }) .regex(/((.*?))(\.)+(docx|pdf)$/i) @@ -187,6 +190,9 @@ export const ZApplicationApiPayload = ZApplicationV2Base.extend({ .string() .transform((recipientId) => { const [collectionName, jobId] = recipientId.split("_") + if (!ObjectId.isValid(jobId)) { + throw new Error(`Invalid job identifier: ${jobId}`) + } if (!["recruteurslba", "jobs_parnters", "recruiters"].includes(collectionName)) { throw new Error(`Invalid collection name: ${collectionName}`) } diff --git a/shared/package.json b/shared/package.json index 532d53260..abd96ea7c 100644 --- a/shared/package.json +++ b/shared/package.json @@ -19,12 +19,14 @@ "@fastify/swagger": "^8.15.0", "bson": "^6.7.0", "dayjs": "^1.11.13", + "i18next": "^24.0.5", "libphonenumber-js": "^1.11.7", "lodash-es": "^4.17.21", "luhn": "^2.4.1", "openapi3-ts": "^4.4.0", "type-fest": "^4.26.0", "zod": "3.23.8", + "zod-i18n-map": "^2.27.0", "zod-mongodb-schema": "^1.0.2" }, "devDependencies": { diff --git a/ui/components/ItemDetail/CandidatureLba/services/getSchema.ts b/ui/components/ItemDetail/CandidatureLba/services/getSchema.ts index 467c6c295..1a97a00db 100644 --- a/ui/components/ItemDetail/CandidatureLba/services/getSchema.ts +++ b/ui/components/ItemDetail/CandidatureLba/services/getSchema.ts @@ -1,4 +1,5 @@ import { IApplicationApiPayloadJSON, ZApplicationApiPayload } from "shared" +import { validatePhone } from "shared/validators/phoneValidator" import { z } from "zod" import { sessionStorageGet } from "@/utils/localStorage" @@ -24,8 +25,5 @@ export const ApplicationFormikSchema = ZApplicationApiPayload.pick({ applicant_email: true, applicant_attachment_name: true, }).extend({ - applicant_phone: z - .string({ required_error: "⚠ Le numéro de téléphone est obligatoire" }) - .min(10, "le téléphone est sur 10 chiffres") - .max(10, "le téléphone est sur 10 chiffres"), // KBA 2024-12-04: based application schema needs to be changed + applicant_phone: z.string().trim().refine(validatePhone, { message: "Téléphone non valide : veuillez utiliser le format international (+33XXX...) ou national (06XXX...)" }), }) diff --git a/ui/pages/_app.tsx b/ui/pages/_app.tsx index 2a46eb254..08335a330 100644 --- a/ui/pages/_app.tsx +++ b/ui/pages/_app.tsx @@ -1,6 +1,7 @@ import { init } from "@socialgouv/matomo-next" import { useRouter } from "next/router" import { useEffect } from "react" +import { setZodLanguage } from "shared/i18n/i18n" import { setIsTrackingEnabled, setTrackingCookies } from "@/common/utils/trackingCookieUtils" @@ -18,6 +19,7 @@ export default function LaBonneAlternance({ Component, pageProps }) { const router = useRouter() useEffect(() => { + setZodLanguage("fr") init(publicConfig.matomo) setIsTrackingEnabled() }, []) diff --git a/yarn.lock b/yarn.lock index 7816f8957..d2552efc4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13156,6 +13156,20 @@ __metadata: languageName: node linkType: hard +"i18next@npm:^24.0.5": + version: 24.0.5 + resolution: "i18next@npm:24.0.5" + dependencies: + "@babel/runtime": ^7.23.2 + peerDependencies: + typescript: ^5 + peerDependenciesMeta: + typescript: + optional: true + checksum: ae5bc37682f14f4ee44450e10598d32edd324edba2d0980339aa232cdfd506174a2e2dd7b9204d2882c28ddc37261c26c6b7b153ff586e54be71e27599d3dbf8 + languageName: node + linkType: hard + "iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.24": version: 0.4.24 resolution: "iconv-lite@npm:0.4.24" @@ -20678,6 +20692,7 @@ __metadata: bson: ^6.7.0 dayjs: ^1.11.13 eslint-plugin-zod: ^1.4.0 + i18next: ^24.0.5 libphonenumber-js: ^1.11.7 lodash-es: ^4.17.21 luhn: ^2.4.1 @@ -20686,6 +20701,7 @@ __metadata: type-fest: ^4.26.0 typescript: ^5.4.5 zod: 3.23.8 + zod-i18n-map: ^2.27.0 zod-mongodb-schema: ^1.0.2 languageName: unknown linkType: soft @@ -23456,6 +23472,16 @@ __metadata: languageName: node linkType: hard +"zod-i18n-map@npm:^2.27.0": + version: 2.27.0 + resolution: "zod-i18n-map@npm:2.27.0" + peerDependencies: + i18next: ">=21.3.0" + zod: ">=3.17.0" + checksum: 9c06867e6f8ee883ccde1b980477563409cf89759a7af345cafd2da5a3d241892f3372e37ddf482a7b7dc40602079292870b64c3c82263141feae341b80da1c1 + languageName: node + linkType: hard + "zod-mongodb-schema@npm:^1.0.2": version: 1.0.2 resolution: "zod-mongodb-schema@npm:1.0.2"