fix: localize Yup validation messages
Deploy banu-front / deploy (push) Successful in 1m50s

This commit is contained in:
sajjadtalkhabi
2026-08-14 21:08:43 +03:30
parent fe08fc46fb
commit fec1da6ec8
8 changed files with 120 additions and 22 deletions
+20 -1
View File
@@ -19,6 +19,7 @@ export const fields = {
seminaryCode: 'کد حوزوی', seminaryCode: 'کد حوزوی',
seminaryServiceCenterCode: 'کد مرکز خدمات حوزه', seminaryServiceCenterCode: 'کد مرکز خدمات حوزه',
childrenCount: 'تعداد فرزند', childrenCount: 'تعداد فرزند',
marriageStatus: 'وضعیت تأهل',
birthDate: 'تاریخ تولد', birthDate: 'تاریخ تولد',
birthday: 'تاریخ تولد', birthday: 'تاریخ تولد',
@@ -32,12 +33,17 @@ export const fields = {
address: 'آدرس', address: 'آدرس',
title: 'عنوان', title: 'عنوان',
subject: 'موضوع',
category: 'نوع خدمت',
note: 'یادداشت',
description: 'توضیحات', description: 'توضیحات',
bio: 'بیوگرافی', bio: 'بیوگرافی',
message: 'پیام', message: 'پیام',
content: 'محتوا', content: 'محتوا',
rejectionReason: 'دلیل رد', rejectionReason: 'دلیل رد',
reason: 'دلیل رد',
adminNote: 'یادداشت مدیر', adminNote: 'یادداشت مدیر',
reviewerNote: 'یادداشت ارزیاب',
activitySummary: 'خلاصهٔ فعالیت', activitySummary: 'خلاصهٔ فعالیت',
avatar: 'تصویر پروفایل', avatar: 'تصویر پروفایل',
@@ -53,12 +59,15 @@ export const fields = {
startDate: 'تاریخ شروع', startDate: 'تاریخ شروع',
endDate: 'تاریخ پایان', endDate: 'تاریخ پایان',
deadline: 'مهلت تحویل',
fromDate: 'از تاریخ', fromDate: 'از تاریخ',
toDate: 'تا تاریخ', toDate: 'تا تاریخ',
capacity: 'ظرفیت', capacity: 'ظرفیت',
minCapacity: 'حداقل ظرفیت', minCapacity: 'حداقل ظرفیت',
maxCapacity: 'حداکثر ظرفیت', maxCapacity: 'حداکثر ظرفیت',
score: 'نمره',
minimumScore: 'حد نصاب قبولی',
order: 'ترتیب', order: 'ترتیب',
priority: 'اولویت', priority: 'اولویت',
@@ -71,22 +80,32 @@ export const fields = {
termId: 'ترم', termId: 'ترم',
courseId: 'دوره', courseId: 'دوره',
sessionId: 'جلسه', sessionId: 'جلسه',
missionaryRequestId: 'درخواست مبلغ',
sessionType: 'نوع جلسه', sessionType: 'نوع جلسه',
type: 'نوع جلسه',
link: 'لینک جلسه',
teacherId: 'مدرس', teacherId: 'مدرس',
studentIds: 'دانش‌آموزان', studentIds: 'دانش‌آموزان',
prerequisiteCourseIds: 'دوره‌های پیش‌نیاز',
contentMediaId: 'فایل محتوای دوره',
coverMediaId: 'تصویر جلد',
imageId: 'تصویر',
educationStatus: 'وضعیت تحصیلی', educationStatus: 'وضعیت تحصیلی',
fieldOfStudy: 'رشتهٔ تحصیلی', fieldOfStudy: 'رشتهٔ تحصیلی',
seminaryLevel: 'مقطع حوزه', seminaryLevel: 'مقطع حوزه',
universityLevel: 'مقطع دانشگاهی', universityLevel: 'مقطع دانشگاهی',
universityName: 'نام حوزه علمیه / دانشگاه', universityName: 'نام حوزه علمیه / دانشگاه',
degree: 'مقطع تحصیلی',
jobTitle: 'عنوان شغل',
organizationName: 'نام سازمان',
hijabApproach: 'رویکرد حجاب', hijabApproach: 'رویکرد حجاب',
prerequisites: 'پیش‌نیازها', prerequisites: 'پیش‌نیازها',
specializedTopics: 'موضوعات تخصصی', specializedTopics: 'موضوعات تخصصی',
propagationPlatforms: 'بسترهای تبلیغ', propagationPlatforms: 'بسترهای تبلیغ',
propagationMethodDescription: 'شرح روش تبلیغ', propagationMethodDescription: 'شرح روش تبلیغ',
propagationExperienceYears: نوات تجربهٔ تبلیغ', propagationExperienceYears: ابقه فعالیت تبلیغی',
relevantCertificates: 'گواهی‌نامه‌های مرتبط', relevantCertificates: 'گواهی‌نامه‌های مرتبط',
onlinePlatformDetails: 'جزئیات بستر آنلاین', onlinePlatformDetails: 'جزئیات بستر آنلاین',
+1 -3
View File
@@ -1,7 +1,5 @@
import { string } from 'yup' import { string } from 'yup'
import { fields } from '@/constants/fields'
export const phoneNumberRule = string() export const phoneNumberRule = string()
.required() .required()
.matches(/^09\d{9}$/, ({ path }) => `${fields[path] || path} نامعتبر است`) .matches(/^09\d{9}$/, 'شماره موبایل باید با ۰۹ شروع شود و ۱۱ رقم باشد')
+14 -3
View File
@@ -3,9 +3,16 @@ import { array, boolean, mixed, number, object, string } from 'yup'
export const courseSchema = object().shape({ export const courseSchema = object().shape({
title: string().required().min(3).max(255), title: string().required().min(3).max(255),
teacherId: mixed().required(), teacherId: mixed().required(),
capacity: number().typeError('ظرفیت باید عدد باشد').required().min(1), capacity: number()
.typeError('ظرفیت باید به‌صورت عدد وارد شود')
.required()
.integer('ظرفیت باید عدد صحیح باشد')
.min(1, 'ظرفیت باید حداقل ۱ نفر باشد'),
prerequisiteCourseIds: array().nullable().default([]), prerequisiteCourseIds: array().nullable().default([]),
contentMediaId: number().nullable().notRequired(), contentMediaId: number()
.typeError('شناسه فایل محتوای دوره باید عدد باشد')
.nullable()
.notRequired(),
description: string().nullable().notRequired(), description: string().nullable().notRequired(),
termId: mixed().required(), termId: mixed().required(),
isActive: boolean().nullable().notRequired(), isActive: boolean().nullable().notRequired(),
@@ -14,7 +21,11 @@ export const courseSchema = object().shape({
export const offeredCourseSchema = object().shape({ export const offeredCourseSchema = object().shape({
termId: string().required(), termId: string().required(),
title: string().required(), title: string().required(),
capacity: string().required(), capacity: number()
.typeError('ظرفیت باید به‌صورت عدد وارد شود')
.required()
.integer('ظرفیت باید عدد صحیح باشد')
.min(1, 'ظرفیت باید حداقل ۱ نفر باشد'),
imageId: string().nullable().notRequired(), imageId: string().nullable().notRequired(),
isActive: boolean().nullable().notRequired(), isActive: boolean().nullable().notRequired(),
}) })
+1 -1
View File
@@ -6,7 +6,7 @@ export const termSchema = object().shape({
isActive: boolean().default(true), isActive: boolean().default(true),
minimumScore: number().typeError('حد نصاب قبولی باید عدد باشد').required(), minimumScore: number().typeError('حد نصاب قبولی باید عدد باشد').required(),
score: number().typeError('کل نمره آزمون باید عدد باشد').required(), score: number().typeError('کل نمره آزمون باید عدد باشد').required(),
coverMediaId: number().nullable().notRequired(), coverMediaId: number().typeError('شناسه تصویر جلد باید عدد باشد').nullable().notRequired(),
}) })
export const addTermStudentSchema = object().shape({ export const addTermStudentSchema = object().shape({
+1 -1
View File
@@ -20,7 +20,7 @@ const baseUserShape = {
provinceId: string().nullable().notRequired(), provinceId: string().nullable().notRequired(),
cityId: string().nullable().notRequired(), cityId: string().nullable().notRequired(),
address: string().nullable().notRequired(), address: string().nullable().notRequired(),
avatarMediaId: number().nullable().notRequired(), avatarMediaId: number().typeError('شناسه تصویر پروفایل باید عدد باشد').nullable().notRequired(),
} }
export const userAddSchema = object().shape({ export const userAddSchema = object().shape({
+11 -5
View File
@@ -25,10 +25,15 @@ export const personalInformationSchema = object().shape({
.matches(/^\d{10}$/, 'کد پستی باید ۱۰ رقم باشد'), .matches(/^\d{10}$/, 'کد پستی باید ۱۰ رقم باشد'),
virtualPhoneNumber: string() virtualPhoneNumber: string()
.required() .required()
.matches(/^\d{10,15}$/, 'شماره تلفن مجازی نامعتبر است'), .matches(/^\d{10,15}$/, 'شماره تلفن مجازی باید فقط شامل ۱۰ تا ۱۵ رقم باشد'),
seminaryCode: requiredString(), seminaryCode: requiredString(),
seminaryServiceCenterCode: 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({ export const educationalInformationSchema = object().shape({
@@ -42,10 +47,11 @@ export const educationalInformationSchema = object().shape({
export const professionalInformationSchema = object().shape({ export const professionalInformationSchema = object().shape({
propagationExperienceYears: number() propagationExperienceYears: number()
.typeError(نوات تجربه تبلیغ باید عدد باشد') .typeError(ابقه فعالیت تبلیغی باید به‌صورت عدد وارد شود')
.required() .required()
.min(0) .integer('سابقه فعالیت تبلیغی باید عدد صحیح باشد')
.max(70), .min(0, 'سابقه فعالیت تبلیغی نمی‌تواند کمتر از صفر باشد')
.max(70, 'سابقه فعالیت تبلیغی نمی‌تواند بیشتر از ۷۰ سال باشد'),
propagationMethodDescription: requiredString(10), propagationMethodDescription: requiredString(10),
specializedTopics: requiredString(10), specializedTopics: requiredString(10),
propagationPlatforms: array() propagationPlatforms: array()
+4 -1
View File
@@ -22,7 +22,10 @@ export const studentProfileSchema = object().shape({
virtualPhoneNumber: string() virtualPhoneNumber: string()
.nullable() .nullable()
.notRequired() .notRequired()
.matches(/^\d{10,15}$/, { message: 'شماره تلفن مجازی نامعتبر است', excludeEmptyString: true }), .matches(/^\d{10,15}$/, {
message: 'شماره تلفن مجازی باید فقط شامل ۱۰ تا ۱۵ رقم باشد',
excludeEmptyString: true,
}),
seminaryCode: string().nullable().notRequired(), seminaryCode: string().nullable().notRequired(),
seminaryServiceCenterCode: string().nullable().notRequired(), seminaryServiceCenterCode: string().nullable().notRequired(),
childrenCount: string() childrenCount: string()
+68 -7
View File
@@ -2,21 +2,82 @@ import { setLocale } from 'yup'
import { fields } from '@/constants/fields' 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({ setLocale({
mixed: { mixed: {
default: ({ path }) => `مقدار فیلد ${label(path)} معتبر نیست`,
required: ({ 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: { string: {
min: ({ path, min }) => `فیلد ${label(path)} باید حداقل ${min} کاراکتر باشد`, min: ({ path, min }) => `فیلد ${label(path)} باید حداقل ${toFaDigits(min)} کاراکتر باشد`,
max: ({ path, max }) => `فیلد ${label(path)} باید حداکثر ${max} کاراکتر باشد`, max: ({ path, max }) => `فیلد ${label(path)} باید حداکثر ${toFaDigits(max)} کاراکتر باشد`,
email: ({ path }) => `فیلد ${label(path)} باید یک ایمیل معتبر باشد`, email: ({ path }) => `فیلد ${label(path)} باید یک ایمیل معتبر باشد`,
url: ({ path }) => `فیلد ${label(path)} باید یک URL معتبر باشد`, url: ({ path }) => `فیلد ${label(path)} باید یک نشانی اینترنتی معتبر باشد`,
length: ({ path, length }) => `فیلد ${label(path)} باید دقیقا ${length} کاراکتر باشد`, 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: { array: {
min: ({ path, min }) => `فیلد ${label(path)} باید حداقل ${min} آیتم داشته باشد`, min: ({ path, min }) => `فیلد ${label(path)} باید حداقل ${toFaDigits(min)} مورد داشته باشد`,
max: ({ path, max }) => `فیلد ${label(path)} باید حداکثر ${max} آیتم داشته باشد`, max: ({ path, max }) => `فیلد ${label(path)} باید حداکثر ${toFaDigits(max)} مورد داشته باشد`,
length: ({ path, length }) =>
`فیلد ${label(path)} باید دقیقاً ${toFaDigits(length)} مورد داشته باشد`,
},
tuple: {
notType: typeError,
}, },
}) })