feat(validation): add arktype form schemas and i18n keys
Introduces field-specific validation messages so toasts name the exact field and rule that failed instead of a catch-all "tous les champs sont requis".
This commit is contained in:
@@ -47,6 +47,20 @@ export default {
|
||||
},
|
||||
validation: {
|
||||
max_char:
|
||||
'no character available | 1 character max | {max} character maximum'
|
||||
'no character available | 1 character max | {max} character maximum',
|
||||
transaction_name_required: 'Transaction name is required.',
|
||||
amount_required: 'Amount is required.',
|
||||
amount_must_be_positive: 'Amount must be greater than 0.',
|
||||
amount_too_large: 'Amount must be less than {max}.',
|
||||
currency_required: 'Currency is required.',
|
||||
date_required: 'Date is required.',
|
||||
pay_by_required: 'Payer is required.',
|
||||
pay_for_required: 'At least one share must be assigned.',
|
||||
user_id_required: 'A username is required.',
|
||||
passwords_mismatch: 'Passwords must match.',
|
||||
account_name_required: 'Account name is required.',
|
||||
account_needs_one_user: 'At least one person must be added to the account.',
|
||||
aliases_must_be_unique: 'Aliases must be unique.',
|
||||
user_ids_must_be_unique: 'Usernames must be unique.'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,20 @@ export default {
|
||||
pseudo: 'identifiant'
|
||||
},
|
||||
validation: {
|
||||
max_char: 'limite atteinte | 1 caractère maximum | {max} caractères maximum'
|
||||
max_char: 'limite atteinte | 1 caractère maximum | {max} caractères maximum',
|
||||
transaction_name_required: 'Le nom de la dépense est requis.',
|
||||
amount_required: 'Le montant est requis.',
|
||||
amount_must_be_positive: 'Le montant doit être supérieur à 0.',
|
||||
amount_too_large: 'Le montant doit être inférieur à {max}.',
|
||||
currency_required: 'La devise est requise.',
|
||||
date_required: 'La date est requise.',
|
||||
pay_by_required: 'Le payeur est requis.',
|
||||
pay_for_required: 'Au moins une part doit être attribuée.',
|
||||
user_id_required: 'Un pseudo est obligatoire.',
|
||||
passwords_mismatch: 'Les mots de passe doivent être identiques.',
|
||||
account_name_required: 'Le nom du compte est requis.',
|
||||
account_needs_one_user: 'Au moins une personne doit être ajoutée au compte.',
|
||||
aliases_must_be_unique: 'Les alias doivent être uniques.',
|
||||
user_ids_must_be_unique: 'Les identifiants doivent être uniques.'
|
||||
}
|
||||
}
|
||||
|
||||
20
src/schemas/account.ts
Normal file
20
src/schemas/account.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { type } from 'arktype'
|
||||
|
||||
export const accountFormSchema = type({
|
||||
name: 'unknown',
|
||||
aliases: 'unknown',
|
||||
userIds: 'unknown'
|
||||
}).narrow((data, ctx) => {
|
||||
if (typeof data.name !== 'string' || data.name.trim().length === 0) {
|
||||
ctx.reject({ message: 'validation.account_name_required', path: ['name'] })
|
||||
}
|
||||
if (!Array.isArray(data.aliases) || data.aliases.length === 0) {
|
||||
ctx.reject({ message: 'validation.account_needs_one_user', path: ['aliases'] })
|
||||
} else if (new Set(data.aliases).size !== data.aliases.length) {
|
||||
ctx.reject({ message: 'validation.aliases_must_be_unique', path: ['aliases'] })
|
||||
}
|
||||
if (Array.isArray(data.userIds) && new Set(data.userIds).size !== data.userIds.length) {
|
||||
ctx.reject({ message: 'validation.user_ids_must_be_unique', path: ['userIds'] })
|
||||
}
|
||||
return !ctx.hasError()
|
||||
})
|
||||
24
src/schemas/transaction.ts
Normal file
24
src/schemas/transaction.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { type } from 'arktype'
|
||||
|
||||
export const MAX_TRANSACTION_AMOUNT = 1_000_000
|
||||
|
||||
export const transactionFormSchema = type({
|
||||
name: 'unknown',
|
||||
amount: 'unknown',
|
||||
payBy: 'unknown'
|
||||
}).narrow((data, ctx) => {
|
||||
if (typeof data.name !== 'string' || data.name.trim().length === 0) {
|
||||
ctx.reject({ message: 'validation.transaction_name_required', path: ['name'] })
|
||||
}
|
||||
if (typeof data.amount !== 'number' || Number.isNaN(data.amount)) {
|
||||
ctx.reject({ message: 'validation.amount_required', path: ['amount'] })
|
||||
} else if (data.amount <= 0) {
|
||||
ctx.reject({ message: 'validation.amount_must_be_positive', path: ['amount'] })
|
||||
} else if (data.amount > MAX_TRANSACTION_AMOUNT) {
|
||||
ctx.reject({ message: 'validation.amount_too_large', path: ['amount'] })
|
||||
}
|
||||
if (typeof data.payBy !== 'string' || data.payBy.length === 0) {
|
||||
ctx.reject({ message: 'validation.pay_by_required', path: ['payBy'] })
|
||||
}
|
||||
return !ctx.hasError()
|
||||
})
|
||||
15
src/schemas/user.ts
Normal file
15
src/schemas/user.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { type } from 'arktype'
|
||||
|
||||
export const signupFormSchema = type({
|
||||
userId: 'unknown',
|
||||
password: 'unknown',
|
||||
confirmPassword: 'unknown'
|
||||
}).narrow((data, ctx) => {
|
||||
if (typeof data.userId !== 'string' || data.userId.trim().length === 0) {
|
||||
ctx.reject({ message: 'validation.user_id_required', path: ['userId'] })
|
||||
}
|
||||
if (data.password !== data.confirmPassword) {
|
||||
ctx.reject({ message: 'validation.passwords_mismatch', path: ['confirmPassword'] })
|
||||
}
|
||||
return !ctx.hasError()
|
||||
})
|
||||
29
src/utils/validate.ts
Normal file
29
src/utils/validate.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { ArkErrors } from 'arktype'
|
||||
import type { Type } from 'arktype'
|
||||
import type { Composer } from 'vue-i18n'
|
||||
import queueNotifService from '@/services/QueueNotifService'
|
||||
|
||||
type TranslateFn = Composer['t']
|
||||
type MessageParams = Record<string, unknown>
|
||||
export type ParamsByPath = Record<string, MessageParams>
|
||||
|
||||
export function validateAndNotify(
|
||||
schema: Type,
|
||||
data: unknown,
|
||||
t: TranslateFn,
|
||||
paramsByPath: ParamsByPath = {}
|
||||
): boolean {
|
||||
const result = schema(data)
|
||||
if (!(result instanceof ArkErrors)) return true
|
||||
|
||||
const seen = new Set<string>()
|
||||
for (const error of result) {
|
||||
const pathKey = error.path.join('.')
|
||||
// Skip duplicate field-level errors at the same path so the user gets one toast per field.
|
||||
if (seen.has(pathKey)) continue
|
||||
seen.add(pathKey)
|
||||
const params = paramsByPath[pathKey] ?? {}
|
||||
queueNotifService.error(t(error.message, params))
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user