|string> $errors * @return array> */ public static function normalize(array $errors, string $locale): array { $out = []; foreach ($errors as $field => $messages) { $fieldKey = (string) $field; $normalized = []; foreach ((array) $messages as $message) { $normalized[] = self::present($fieldKey, (string) $message, $locale); } $out[$fieldKey] = $normalized; } return $out; } /** * @param array> $normalized */ public static function summary(array $normalized, int $maxParts = 3): ?string { $parts = []; foreach ($normalized as $messages) { foreach ($messages as $message) { $parts[] = $message; if (count($parts) >= $maxParts) { return implode(';', $parts).'…'; } } } return $parts === [] ? null : implode(';', $parts); } private static function present(string $field, string $message, string $locale): string { $trimmed = trim($message); if ($trimmed === '') { return $message; } if (preg_match('/\p{Han}/u', $trimmed) === 1) { return $trimmed; } $exact = self::exactMessage($trimmed, $locale); if ($exact !== null) { return $exact; } if (preg_match( '/^(exceeds_actor|exceeds_parent_ceiling|exceeds_delegation_ceiling|permission_exceeds_actor|permission_catalog_incomplete)\s*:\s*(.+)$/u', $trimmed, $matches, ) === 1) { $detail = trim(preg_replace('/\s*\(run:.*$/i', '', $matches[2]) ?? $matches[2]); $line = trans('validation.business.'.$matches[1], ['detail' => $detail], $locale); if ($line !== 'validation.business.'.$matches[1]) { return $line; } } $attribute = self::attributeLabel($field, $locale); $businessKey = 'validation.business.'.$trimmed; $businessLine = trans($businessKey, ['attribute' => $attribute], $locale); if ($businessLine !== $businessKey) { return $businessLine; } $customLine = self::customRuleLine($field, $trimmed, $attribute, $locale); if ($customLine !== null) { return $customLine; } $ruleLine = self::standardRuleLine($trimmed, $attribute, $locale); if ($ruleLine !== null) { return $ruleLine; } $humanized = self::humanizeLaravelEnglish($field, $trimmed, $locale, $attribute); if ($humanized !== null) { return $humanized; } return $trimmed; } private static function exactMessage(string $message, string $locale): ?string { $map = trans('validation.exact', [], $locale); if (! is_array($map)) { return null; } return $map[$message] ?? null; } private static function customRuleLine( string $field, string $rule, string $attribute, string $locale, ): ?string { $candidates = array_values(array_unique([ 'validation.custom.'.$field.'.'.$rule, 'validation.custom.'.self::flatFieldName($field).'.'.$rule, ])); foreach ($candidates as $key) { $line = trans($key, ['attribute' => $attribute], $locale); if ($line !== $key) { return $line; } } return null; } private static function standardRuleLine(string $rule, string $attribute, string $locale): ?string { if (! preg_match('/^[a-z0-9_.]+$/i', $rule)) { return null; } $key = 'validation.'.$rule; $line = trans($key, ['attribute' => $attribute], $locale); return $line !== $key ? $line : null; } private static function humanizeLaravelEnglish( string $field, string $message, string $locale, string $attribute, ): ?string { if (preg_match('/^The (.+?) field (.+)$/i', $message, $matches) !== 1) { if (preg_match('/^The (.+?) has already been taken\.?$/i', $message, $taken) === 1) { $attribute = self::attributeLabelFromEnglish($taken[1], $field, $locale); return trans('validation.unique', ['attribute' => $attribute], $locale); } if (preg_match('/^The selected (.+?) is invalid\.?$/i', $message, $selected) === 1) { $attribute = self::attributeLabelFromEnglish($selected[1], $field, $locale); return trans('validation.exists', ['attribute' => $attribute], $locale); } return null; } $englishName = $matches[1]; $tail = $matches[2]; $attribute = self::attributeLabelFromEnglish($englishName, $field, $locale); $tailMap = [ 'is required' => 'validation.required', 'must be a valid email address' => 'validation.email', 'must be a valid UUID' => 'validation.uuid', 'must be an integer' => 'validation.integer', 'must be a string' => 'validation.string', 'must be a number' => 'validation.numeric', 'must be a valid JSON string' => 'validation.json', 'must be true or false' => 'validation.boolean', 'must be an array' => 'validation.array', 'format is invalid' => null, 'is not allowed' => 'validation.prohibited', 'must be present' => 'validation.present', ]; foreach ($tailMap as $suffix => $ruleKey) { if (stripos($tail, $suffix) === false) { continue; } if ($ruleKey === null) { return self::customRuleLine($field, 'regex', $attribute, $locale) ?? trans('validation.regex', ['attribute' => $attribute], $locale); } $line = trans($ruleKey, ['attribute' => $attribute], $locale); return $line !== $ruleKey ? $line : null; } if (preg_match('/must be at least (\d+) characters/i', $tail, $min)) { $customKey = 'validation.custom.'.self::flatFieldName($field).'.min'; $customLine = trans($customKey, ['attribute' => $attribute, 'min' => $min[1]], $locale); if ($customLine !== $customKey) { return $customLine; } return trans('validation.min.string', ['attribute' => $attribute, 'min' => $min[1]], $locale); } if (preg_match('/must not be greater than (\d+) characters/i', $tail, $max)) { return trans('validation.max.string', ['attribute' => $attribute, 'max' => $max[1]], $locale); } if (preg_match('/must contain (\d+) items/i', $tail, $size)) { return trans('validation.size.array', ['attribute' => $attribute, 'size' => $size[1]], $locale); } if (preg_match('/must have at least (\d+) items/i', $tail, $minItems)) { return trans('validation.min.array', ['attribute' => $attribute, 'min' => $minItems[1]], $locale); } if (preg_match('/must not have more than (\d+) items/i', $tail, $maxItems)) { return trans('validation.max.array', ['attribute' => $attribute, 'max' => $maxItems[1]], $locale); } if (preg_match('/must be between ([\d.]+) and ([\d.]+)/i', $tail, $between)) { return trans('validation.between.numeric', [ 'attribute' => $attribute, 'min' => $between[1], 'max' => $between[2], ], $locale); } if (preg_match('/must match the format (.+)$/i', $tail, $format)) { return trans('validation.date_format', [ 'attribute' => $attribute, 'format' => trim($format[1]), ], $locale); } return null; } private static function attributeLabelFromEnglish(string $englishName, string $field, string $locale): string { $normalized = strtolower(trim($englishName)); if (preg_match('/^selected (.+)$/i', $normalized, $selected) === 1) { $normalized = $selected[1]; } $guess = str_replace(' ', '_', $normalized); return self::attributeLabel($guess !== '' ? $guess : $field, $locale); } private static function attributeLabel(string $field, string $locale): string { $candidates = array_values(array_unique([ $field, self::flatFieldName($field), preg_replace('/\.\d+\./', '.*.', $field) ?? $field, preg_replace('/\.\d+\./', '.', $field) ?? $field, ])); foreach ($candidates as $candidate) { $key = 'validation.attributes.'.$candidate; $label = trans($key, [], $locale); if ($label !== $key) { return $label; } } return self::flatFieldName($field); } private static function flatFieldName(string $field): string { if (preg_match('/\.([a-zA-Z0-9_]+)$/', $field, $matches) === 1) { return $matches[1]; } return $field; } }