From fec1da6ec81af649add2b1fcf0f82ca793143b72 Mon Sep 17 00:00:00 2001 From: sajjadtalkhabi <97734061+sajjadtalkhabi@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:08:43 +0330 Subject: [PATCH] fix: localize Yup validation messages --- src/constants/fields.js | 21 +++++- src/constants/rules/phoneNumberRule.js | 4 +- src/features/admin/courses/schema/index.js | 17 ++++- src/features/admin/terms/schema/index.js | 2 +- src/features/admin/users/schema/index.js | 2 +- src/features/auth/schema/student-register.js | 16 +++-- src/features/student/profile/schema/index.js | 5 +- src/utils/yup-locale.js | 75 ++++++++++++++++++-- 8 files changed, 120 insertions(+), 22 deletions(-) diff --git a/src/constants/fields.js b/src/constants/fields.js index bff53f7..602a9d3 100644 --- a/src/constants/fields.js +++ b/src/constants/fields.js @@ -19,6 +19,7 @@ export const fields = { seminaryCode: 'کد حوزوی', seminaryServiceCenterCode: 'کد مرکز خدمات حوزه', childrenCount: 'تعداد فرزند', + marriageStatus: 'وضعیت تأهل', birthDate: 'تاریخ تولد', birthday: 'تاریخ تولد', @@ -32,12 +33,17 @@ export const fields = { address: 'آدرس', title: 'عنوان', + subject: 'موضوع', + category: 'نوع خدمت', + note: 'یادداشت', description: 'توضیحات', bio: 'بیوگرافی', message: 'پیام', content: 'محتوا', rejectionReason: 'دلیل رد', + reason: 'دلیل رد', adminNote: 'یادداشت مدیر', + reviewerNote: 'یادداشت ارزیاب', activitySummary: 'خلاصهٔ فعالیت', avatar: 'تصویر پروفایل', @@ -53,12 +59,15 @@ export const fields = { startDate: 'تاریخ شروع', endDate: 'تاریخ پایان', + deadline: 'مهلت تحویل', fromDate: 'از تاریخ', toDate: 'تا تاریخ', capacity: 'ظرفیت', minCapacity: 'حداقل ظرفیت', maxCapacity: 'حداکثر ظرفیت', + score: 'نمره', + minimumScore: 'حد نصاب قبولی', order: 'ترتیب', priority: 'اولویت', @@ -71,22 +80,32 @@ export const fields = { termId: 'ترم', courseId: 'دوره', sessionId: 'جلسه', + missionaryRequestId: 'درخواست مبلغ', sessionType: 'نوع جلسه', + type: 'نوع جلسه', + link: 'لینک جلسه', teacherId: 'مدرس', studentIds: 'دانش‌آموزان', + prerequisiteCourseIds: 'دوره‌های پیش‌نیاز', + contentMediaId: 'فایل محتوای دوره', + coverMediaId: 'تصویر جلد', + imageId: 'تصویر', educationStatus: 'وضعیت تحصیلی', fieldOfStudy: 'رشتهٔ تحصیلی', seminaryLevel: 'مقطع حوزه', universityLevel: 'مقطع دانشگاهی', universityName: 'نام حوزه علمیه / دانشگاه', + degree: 'مقطع تحصیلی', + jobTitle: 'عنوان شغل', + organizationName: 'نام سازمان', hijabApproach: 'رویکرد حجاب', prerequisites: 'پیش‌نیازها', specializedTopics: 'موضوعات تخصصی', propagationPlatforms: 'بسترهای تبلیغ', propagationMethodDescription: 'شرح روش تبلیغ', - propagationExperienceYears: 'سنوات تجربهٔ تبلیغ', + propagationExperienceYears: 'سابقه فعالیت تبلیغی', relevantCertificates: 'گواهی‌نامه‌های مرتبط', onlinePlatformDetails: 'جزئیات بستر آنلاین', diff --git a/src/constants/rules/phoneNumberRule.js b/src/constants/rules/phoneNumberRule.js index e967867..25abc10 100644 --- a/src/constants/rules/phoneNumberRule.js +++ b/src/constants/rules/phoneNumberRule.js @@ -1,7 +1,5 @@ import { string } from 'yup' -import { fields } from '@/constants/fields' - export const phoneNumberRule = string() .required() - .matches(/^09\d{9}$/, ({ path }) => `${fields[path] || path} نامعتبر است`) + .matches(/^09\d{9}$/, 'شماره موبایل باید با ۰۹ شروع شود و ۱۱ رقم باشد') diff --git a/src/features/admin/courses/schema/index.js b/src/features/admin/courses/schema/index.js index 52c7161..5d36c0f 100644 --- a/src/features/admin/courses/schema/index.js +++ b/src/features/admin/courses/schema/index.js @@ -3,9 +3,16 @@ import { array, boolean, mixed, number, object, string } from 'yup' export const courseSchema = object().shape({ title: string().required().min(3).max(255), teacherId: mixed().required(), - capacity: number().typeError('ظرفیت باید عدد باشد').required().min(1), + capacity: number() + .typeError('ظرفیت باید به‌صورت عدد وارد شود') + .required() + .integer('ظرفیت باید عدد صحیح باشد') + .min(1, 'ظرفیت باید حداقل ۱ نفر باشد'), prerequisiteCourseIds: array().nullable().default([]), - contentMediaId: number().nullable().notRequired(), + contentMediaId: number() + .typeError('شناسه فایل محتوای دوره باید عدد باشد') + .nullable() + .notRequired(), description: string().nullable().notRequired(), termId: mixed().required(), isActive: boolean().nullable().notRequired(), @@ -14,7 +21,11 @@ export const courseSchema = object().shape({ export const offeredCourseSchema = object().shape({ termId: string().required(), title: string().required(), - capacity: string().required(), + capacity: number() + .typeError('ظرفیت باید به‌صورت عدد وارد شود') + .required() + .integer('ظرفیت باید عدد صحیح باشد') + .min(1, 'ظرفیت باید حداقل ۱ نفر باشد'), imageId: string().nullable().notRequired(), isActive: boolean().nullable().notRequired(), }) diff --git a/src/features/admin/terms/schema/index.js b/src/features/admin/terms/schema/index.js index aa9904e..308191d 100644 --- a/src/features/admin/terms/schema/index.js +++ b/src/features/admin/terms/schema/index.js @@ -6,7 +6,7 @@ export const termSchema = object().shape({ isActive: boolean().default(true), minimumScore: number().typeError('حد نصاب قبولی باید عدد باشد').required(), score: number().typeError('کل نمره آزمون باید عدد باشد').required(), - coverMediaId: number().nullable().notRequired(), + coverMediaId: number().typeError('شناسه تصویر جلد باید عدد باشد').nullable().notRequired(), }) export const addTermStudentSchema = object().shape({ diff --git a/src/features/admin/users/schema/index.js b/src/features/admin/users/schema/index.js index 4cea7e1..59c0834 100644 --- a/src/features/admin/users/schema/index.js +++ b/src/features/admin/users/schema/index.js @@ -20,7 +20,7 @@ const baseUserShape = { provinceId: string().nullable().notRequired(), cityId: string().nullable().notRequired(), address: string().nullable().notRequired(), - avatarMediaId: number().nullable().notRequired(), + avatarMediaId: number().typeError('شناسه تصویر پروفایل باید عدد باشد').nullable().notRequired(), } export const userAddSchema = object().shape({ diff --git a/src/features/auth/schema/student-register.js b/src/features/auth/schema/student-register.js index d02bd12..133987d 100644 --- a/src/features/auth/schema/student-register.js +++ b/src/features/auth/schema/student-register.js @@ -25,10 +25,15 @@ export const personalInformationSchema = object().shape({ .matches(/^\d{10}$/, 'کد پستی باید ۱۰ رقم باشد'), virtualPhoneNumber: string() .required() - .matches(/^\d{10,15}$/, 'شماره تلفن مجازی نامعتبر است'), + .matches(/^\d{10,15}$/, 'شماره تلفن مجازی باید فقط شامل ۱۰ تا ۱۵ رقم باشد'), seminaryCode: requiredString(), seminaryServiceCenterCode: requiredString(), - childrenCount: number().typeError('تعداد فرزند باید عدد باشد').required().min(0).max(20), + childrenCount: number() + .typeError('تعداد فرزند باید به‌صورت عدد وارد شود') + .required() + .integer('تعداد فرزند باید عدد صحیح باشد') + .min(0, 'تعداد فرزند نمی‌تواند کمتر از صفر باشد') + .max(20, 'تعداد فرزند نمی‌تواند بیشتر از ۲۰ باشد'), }) export const educationalInformationSchema = object().shape({ @@ -42,10 +47,11 @@ export const educationalInformationSchema = object().shape({ export const professionalInformationSchema = object().shape({ propagationExperienceYears: number() - .typeError('سنوات تجربه تبلیغ باید عدد باشد') + .typeError('سابقه فعالیت تبلیغی باید به‌صورت عدد وارد شود') .required() - .min(0) - .max(70), + .integer('سابقه فعالیت تبلیغی باید عدد صحیح باشد') + .min(0, 'سابقه فعالیت تبلیغی نمی‌تواند کمتر از صفر باشد') + .max(70, 'سابقه فعالیت تبلیغی نمی‌تواند بیشتر از ۷۰ سال باشد'), propagationMethodDescription: requiredString(10), specializedTopics: requiredString(10), propagationPlatforms: array() diff --git a/src/features/student/profile/schema/index.js b/src/features/student/profile/schema/index.js index c0c3830..c527972 100644 --- a/src/features/student/profile/schema/index.js +++ b/src/features/student/profile/schema/index.js @@ -22,7 +22,10 @@ export const studentProfileSchema = object().shape({ virtualPhoneNumber: string() .nullable() .notRequired() - .matches(/^\d{10,15}$/, { message: 'شماره تلفن مجازی نامعتبر است', excludeEmptyString: true }), + .matches(/^\d{10,15}$/, { + message: 'شماره تلفن مجازی باید فقط شامل ۱۰ تا ۱۵ رقم باشد', + excludeEmptyString: true, + }), seminaryCode: string().nullable().notRequired(), seminaryServiceCenterCode: string().nullable().notRequired(), childrenCount: string() diff --git a/src/utils/yup-locale.js b/src/utils/yup-locale.js index 3b75fa1..2d35536 100644 --- a/src/utils/yup-locale.js +++ b/src/utils/yup-locale.js @@ -2,21 +2,82 @@ import { setLocale } from 'yup' import { fields } from '@/constants/fields' -const label = (path) => fields[path] || path +const FA_DIGITS = '۰۱۲۳۴۵۶۷۸۹' + +const toFaDigits = (value) => String(value ?? '').replace(/\d/g, (digit) => FA_DIGITS[digit]) + +const label = (path) => { + const normalizedPath = String(path ?? '').replace(/\[\d+]/g, '') + const fieldName = normalizedPath.split('.').pop() + return fields[path] || fields[normalizedPath] || fields[fieldName] || path || 'مقدار' +} + +const typeError = ({ path, type }) => { + const field = label(path) + const messages = { + array: `فیلد ${field} باید به‌صورت فهرست باشد`, + boolean: `فیلد ${field} باید مقدار بله یا خیر داشته باشد`, + date: `فیلد ${field} باید یک تاریخ معتبر باشد`, + number: `فیلد ${field} باید عدد باشد`, + object: `ساختار فیلد ${field} معتبر نیست`, + string: `فیلد ${field} باید متن باشد`, + } + return messages[type] || `نوع مقدار فیلد ${field} معتبر نیست` +} setLocale({ mixed: { + default: ({ path }) => `مقدار فیلد ${label(path)} معتبر نیست`, required: ({ path }) => `فیلد ${label(path)} ضروری است`, + defined: ({ path }) => `فیلد ${label(path)} باید مقدار داشته باشد`, + notNull: ({ path }) => `فیلد ${label(path)} نمی‌تواند خالی باشد`, + oneOf: ({ path }) => `مقدار انتخاب‌شده برای فیلد ${label(path)} معتبر نیست`, + notOneOf: ({ path }) => `مقدار انتخاب‌شده برای فیلد ${label(path)} مجاز نیست`, + notType: typeError, }, string: { - min: ({ path, min }) => `فیلد ${label(path)} باید حداقل ${min} کاراکتر باشد`, - max: ({ path, max }) => `فیلد ${label(path)} باید حداکثر ${max} کاراکتر باشد`, + min: ({ path, min }) => `فیلد ${label(path)} باید حداقل ${toFaDigits(min)} کاراکتر باشد`, + max: ({ path, max }) => `فیلد ${label(path)} باید حداکثر ${toFaDigits(max)} کاراکتر باشد`, email: ({ path }) => `فیلد ${label(path)} باید یک ایمیل معتبر باشد`, - url: ({ path }) => `فیلد ${label(path)} باید یک URL معتبر باشد`, - length: ({ path, length }) => `فیلد ${label(path)} باید دقیقا ${length} کاراکتر باشد`, + url: ({ path }) => `فیلد ${label(path)} باید یک نشانی اینترنتی معتبر باشد`, + length: ({ path, length }) => + `فیلد ${label(path)} باید دقیقاً ${toFaDigits(length)} کاراکتر باشد`, + matches: ({ path }) => `فرمت فیلد ${label(path)} صحیح نیست`, + uuid: ({ path }) => `شناسه فیلد ${label(path)} معتبر نیست`, + datetime: ({ path }) => `فیلد ${label(path)} باید یک تاریخ و زمان معتبر باشد`, + datetime_precision: ({ path }) => `دقت زمانی فیلد ${label(path)} معتبر نیست`, + datetime_offset: ({ path }) => `منطقه زمانی فیلد ${label(path)} معتبر نیست`, + trim: ({ path }) => `ابتدا یا انتهای فیلد ${label(path)} نباید فاصله داشته باشد`, + lowercase: ({ path }) => `فیلد ${label(path)} باید با حروف کوچک نوشته شود`, + uppercase: ({ path }) => `فیلد ${label(path)} باید با حروف بزرگ نوشته شود`, + }, + number: { + min: ({ path, min }) => `فیلد ${label(path)} باید حداقل ${toFaDigits(min)} باشد`, + max: ({ path, max }) => `فیلد ${label(path)} باید حداکثر ${toFaDigits(max)} باشد`, + lessThan: ({ path, less }) => `فیلد ${label(path)} باید کمتر از ${toFaDigits(less)} باشد`, + moreThan: ({ path, more }) => `فیلد ${label(path)} باید بیشتر از ${toFaDigits(more)} باشد`, + positive: ({ path }) => `فیلد ${label(path)} باید عددی بزرگ‌تر از صفر باشد`, + negative: ({ path }) => `فیلد ${label(path)} باید عددی کوچک‌تر از صفر باشد`, + integer: ({ path }) => `فیلد ${label(path)} باید عدد صحیح باشد`, + }, + date: { + min: ({ path }) => `تاریخ فیلد ${label(path)} زودتر از محدوده مجاز است`, + max: ({ path }) => `تاریخ فیلد ${label(path)} دیرتر از محدوده مجاز است`, + }, + boolean: { + isValue: ({ path }) => `مقدار فیلد ${label(path)} معتبر نیست`, + }, + object: { + noUnknown: ({ path }) => `فیلد ${label(path)} شامل مقادیر ناشناخته است`, + exact: ({ path }) => `فیلد ${label(path)} شامل مقادیر اضافی است`, }, array: { - min: ({ path, min }) => `فیلد ${label(path)} باید حداقل ${min} آیتم داشته باشد`, - max: ({ path, max }) => `فیلد ${label(path)} باید حداکثر ${max} آیتم داشته باشد`, + min: ({ path, min }) => `فیلد ${label(path)} باید حداقل ${toFaDigits(min)} مورد داشته باشد`, + max: ({ path, max }) => `فیلد ${label(path)} باید حداکثر ${toFaDigits(max)} مورد داشته باشد`, + length: ({ path, length }) => + `فیلد ${label(path)} باید دقیقاً ${toFaDigits(length)} مورد داشته باشد`, + }, + tuple: { + notType: typeError, }, })