diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d91eca83..b2d7ea361 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.6.1 under development -- no changes in this release. +- Chg #802: Use translatable human-readable file sizes in `File` validation messages and add numeric, unit, and raw byte placeholders (@samdark) ## 2.6.0 June 02, 2026 diff --git a/docs/guide/en/built-in-rules-file.md b/docs/guide/en/built-in-rules-file.md index a8931b9aa..42f3527e4 100644 --- a/docs/guide/en/built-in-rules-file.md +++ b/docs/guide/en/built-in-rules-file.md @@ -79,6 +79,13 @@ This option should be used with care because the client can send any media type For filesystem-backed uploads, size checks use the actual file size on disk. For pathless streams, size checks use the PSR-7 upload size when available. If a size constraint is configured and the size can't be determined, validation fails. +Default size error messages use translatable human-readable units, for example `50 MB` when `maxSize` is +`50 * 1024 * 1024`. +In custom messages, `{limit}` and `{exactly}` contain the full human-readable size string. Use `{limitValue}` / +`{exactlyValue}` with `{limitUnit}` / `{exactlyUnit}` when a custom message needs the numeric value and unit separately. +Use `{limitBytes}` or `{exactlyBytes}` when a custom message needs the raw byte value for ICU number or plural +formatting. + `size` is mutually exclusive with `minSize` and `maxSize`. When both `minSize` and `maxSize` are set, `minSize` must be less than or equal to `maxSize`. diff --git a/messages/de/yii-validator.php b/messages/de/yii-validator.php index 4dc23f906..32ed14034 100644 --- a/messages/de/yii-validator.php +++ b/messages/de/yii-validator.php @@ -9,6 +9,7 @@ use Yiisoft\Validator\Rule\Each; use Yiisoft\Validator\Rule\Email; use Yiisoft\Validator\Rule\Equal; +use Yiisoft\Validator\Rule\File; use Yiisoft\Validator\Rule\FilledAtLeast; use Yiisoft\Validator\Rule\FilledOnlyOneOf; use Yiisoft\Validator\Rule\GreaterThan; @@ -211,6 +212,44 @@ /** @see AnyRule */ 'At least one of the inner rules must pass the validation.' => 'Mindestens eine der inneren Regeln muss die Validierung bestehen.', + /** @see File */ + 'The size of {property} must be exactly {exactlyValue, number} {exactlyValue, plural, one{byte} other{bytes}}.' => + 'Die Größe von {property} muss genau {exactlyValue, number} {exactlyValue, plural, one{Byte} other{Byte}} betragen.', + 'The size of {property} must be exactly {exactlyValue, number} KB.' => + 'Die Größe von {property} muss genau {exactlyValue, number} KB betragen.', + 'The size of {property} must be exactly {exactlyValue, number} MB.' => + 'Die Größe von {property} muss genau {exactlyValue, number} MB betragen.', + 'The size of {property} must be exactly {exactlyValue, number} GB.' => + 'Die Größe von {property} muss genau {exactlyValue, number} GB betragen.', + 'The size of {property} must be exactly {exactlyValue, number} TB.' => + 'Die Größe von {property} muss genau {exactlyValue, number} TB betragen.', + 'The size of {property} must be exactly {exactlyValue, number} PB.' => + 'Die Größe von {property} muss genau {exactlyValue, number} PB betragen.', + 'The size of {property} cannot be smaller than {limitValue, number} {limitValue, plural, one{byte} other{bytes}}.' => + 'Die Größe von {property} darf nicht kleiner als {limitValue, number} {limitValue, plural, one{Byte} other{Byte}} sein.', + 'The size of {property} cannot be smaller than {limitValue, number} KB.' => + 'Die Größe von {property} darf nicht kleiner als {limitValue, number} KB sein.', + 'The size of {property} cannot be smaller than {limitValue, number} MB.' => + 'Die Größe von {property} darf nicht kleiner als {limitValue, number} MB sein.', + 'The size of {property} cannot be smaller than {limitValue, number} GB.' => + 'Die Größe von {property} darf nicht kleiner als {limitValue, number} GB sein.', + 'The size of {property} cannot be smaller than {limitValue, number} TB.' => + 'Die Größe von {property} darf nicht kleiner als {limitValue, number} TB sein.', + 'The size of {property} cannot be smaller than {limitValue, number} PB.' => + 'Die Größe von {property} darf nicht kleiner als {limitValue, number} PB sein.', + 'The size of {property} cannot be larger than {limitValue, number} {limitValue, plural, one{byte} other{bytes}}.' => + 'Die Größe von {property} darf nicht größer als {limitValue, number} {limitValue, plural, one{Byte} other{Byte}} sein.', + 'The size of {property} cannot be larger than {limitValue, number} KB.' => + 'Die Größe von {property} darf nicht größer als {limitValue, number} KB sein.', + 'The size of {property} cannot be larger than {limitValue, number} MB.' => + 'Die Größe von {property} darf nicht größer als {limitValue, number} MB sein.', + 'The size of {property} cannot be larger than {limitValue, number} GB.' => + 'Die Größe von {property} darf nicht größer als {limitValue, number} GB sein.', + 'The size of {property} cannot be larger than {limitValue, number} TB.' => + 'Die Größe von {property} darf nicht größer als {limitValue, number} TB sein.', + 'The size of {property} cannot be larger than {limitValue, number} PB.' => + 'Die Größe von {property} darf nicht größer als {limitValue, number} PB sein.', + /** @see Image */ '{Property} must be an image.' => '{Property} muss ein Bild sein.', 'The width of {property} must be exactly {exactly, number} {exactly, plural, one{pixel} other{pixels}}.' => diff --git a/messages/pl/yii-validator.php b/messages/pl/yii-validator.php index 095893bcb..a988fd95b 100644 --- a/messages/pl/yii-validator.php +++ b/messages/pl/yii-validator.php @@ -13,6 +13,7 @@ use Yiisoft\Validator\Rule\Each; use Yiisoft\Validator\Rule\Email; use Yiisoft\Validator\Rule\Equal; +use Yiisoft\Validator\Rule\File; use Yiisoft\Validator\Rule\GreaterThan; use Yiisoft\Validator\Rule\GreaterThanOrEqual; use Yiisoft\Validator\Rule\Image\Image; @@ -63,6 +64,25 @@ '{Property} is not a valid email address.' => '{Property} nie jest prawidłowym adresem e-mail.', /** @see FloatType */ '{Property} must be a float.' => '{Property} musi być liczbą zmiennoprzecinkową.', + /** @see File */ + 'The size of {property} must be exactly {exactlyValue, number} {exactlyValue, plural, one{byte} other{bytes}}.' => 'Rozmiar {property} musi wynosić dokładnie {exactlyValue, number} {exactlyValue, plural, one{bajt} few{bajty} many{bajtów} other{bajta}}.', + 'The size of {property} must be exactly {exactlyValue, number} KB.' => 'Rozmiar {property} musi wynosić dokładnie {exactlyValue, number} KB.', + 'The size of {property} must be exactly {exactlyValue, number} MB.' => 'Rozmiar {property} musi wynosić dokładnie {exactlyValue, number} MB.', + 'The size of {property} must be exactly {exactlyValue, number} GB.' => 'Rozmiar {property} musi wynosić dokładnie {exactlyValue, number} GB.', + 'The size of {property} must be exactly {exactlyValue, number} TB.' => 'Rozmiar {property} musi wynosić dokładnie {exactlyValue, number} TB.', + 'The size of {property} must be exactly {exactlyValue, number} PB.' => 'Rozmiar {property} musi wynosić dokładnie {exactlyValue, number} PB.', + 'The size of {property} cannot be smaller than {limitValue, number} {limitValue, plural, one{byte} other{bytes}}.' => 'Rozmiar {property} nie może być mniejszy niż {limitValue, number} {limitValue, plural, one{bajt} few{bajty} many{bajtów} other{bajta}}.', + 'The size of {property} cannot be smaller than {limitValue, number} KB.' => 'Rozmiar {property} nie może być mniejszy niż {limitValue, number} KB.', + 'The size of {property} cannot be smaller than {limitValue, number} MB.' => 'Rozmiar {property} nie może być mniejszy niż {limitValue, number} MB.', + 'The size of {property} cannot be smaller than {limitValue, number} GB.' => 'Rozmiar {property} nie może być mniejszy niż {limitValue, number} GB.', + 'The size of {property} cannot be smaller than {limitValue, number} TB.' => 'Rozmiar {property} nie może być mniejszy niż {limitValue, number} TB.', + 'The size of {property} cannot be smaller than {limitValue, number} PB.' => 'Rozmiar {property} nie może być mniejszy niż {limitValue, number} PB.', + 'The size of {property} cannot be larger than {limitValue, number} {limitValue, plural, one{byte} other{bytes}}.' => 'Rozmiar {property} nie może być większy niż {limitValue, number} {limitValue, plural, one{bajt} few{bajty} many{bajtów} other{bajta}}.', + 'The size of {property} cannot be larger than {limitValue, number} KB.' => 'Rozmiar {property} nie może być większy niż {limitValue, number} KB.', + 'The size of {property} cannot be larger than {limitValue, number} MB.' => 'Rozmiar {property} nie może być większy niż {limitValue, number} MB.', + 'The size of {property} cannot be larger than {limitValue, number} GB.' => 'Rozmiar {property} nie może być większy niż {limitValue, number} GB.', + 'The size of {property} cannot be larger than {limitValue, number} TB.' => 'Rozmiar {property} nie może być większy niż {limitValue, number} TB.', + 'The size of {property} cannot be larger than {limitValue, number} PB.' => 'Rozmiar {property} nie może być większy niż {limitValue, number} PB.', /** @see Image */ '{Property} must be an image.' => '{Property} musi być obrazem.', 'The width of {property} must be exactly {exactly, number} {exactly, plural, one{pixel} other{pixels}}.' => 'Szerokość {property} musi wynosić dokładnie {exactly, number} {exactly, plural, one{piksel} few{piksele} many{pikseli} other{piksela}}.', diff --git a/messages/pt-BR/yii-validator.php b/messages/pt-BR/yii-validator.php index 28123d596..ea19f00e1 100644 --- a/messages/pt-BR/yii-validator.php +++ b/messages/pt-BR/yii-validator.php @@ -9,6 +9,7 @@ use Yiisoft\Validator\Rule\Each; use Yiisoft\Validator\Rule\Email; use Yiisoft\Validator\Rule\Equal; +use Yiisoft\Validator\Rule\File; use Yiisoft\Validator\Rule\FilledAtLeast; use Yiisoft\Validator\Rule\FilledOnlyOneOf; use Yiisoft\Validator\Rule\GreaterThan; @@ -188,6 +189,44 @@ /** @see AnyRule */ 'At least one of the inner rules must pass the validation.' => 'Pelo menos uma das regras internas deve passar na validação.', + /** @see File */ + 'The size of {property} must be exactly {exactlyValue, number} {exactlyValue, plural, one{byte} other{bytes}}.' => + 'O tamanho de {property} deve ser exatamente {exactlyValue, number} {exactlyValue, plural, one{byte} few{bytes} many{bytes} other{bytes}}.', + 'The size of {property} must be exactly {exactlyValue, number} KB.' => + 'O tamanho de {property} deve ser exatamente {exactlyValue, number} KB.', + 'The size of {property} must be exactly {exactlyValue, number} MB.' => + 'O tamanho de {property} deve ser exatamente {exactlyValue, number} MB.', + 'The size of {property} must be exactly {exactlyValue, number} GB.' => + 'O tamanho de {property} deve ser exatamente {exactlyValue, number} GB.', + 'The size of {property} must be exactly {exactlyValue, number} TB.' => + 'O tamanho de {property} deve ser exatamente {exactlyValue, number} TB.', + 'The size of {property} must be exactly {exactlyValue, number} PB.' => + 'O tamanho de {property} deve ser exatamente {exactlyValue, number} PB.', + 'The size of {property} cannot be smaller than {limitValue, number} {limitValue, plural, one{byte} other{bytes}}.' => + 'O tamanho de {property} não pode ser menor que {limitValue, number} {limitValue, plural, one{byte} few{bytes} many{bytes} other{bytes}}.', + 'The size of {property} cannot be smaller than {limitValue, number} KB.' => + 'O tamanho de {property} não pode ser menor que {limitValue, number} KB.', + 'The size of {property} cannot be smaller than {limitValue, number} MB.' => + 'O tamanho de {property} não pode ser menor que {limitValue, number} MB.', + 'The size of {property} cannot be smaller than {limitValue, number} GB.' => + 'O tamanho de {property} não pode ser menor que {limitValue, number} GB.', + 'The size of {property} cannot be smaller than {limitValue, number} TB.' => + 'O tamanho de {property} não pode ser menor que {limitValue, number} TB.', + 'The size of {property} cannot be smaller than {limitValue, number} PB.' => + 'O tamanho de {property} não pode ser menor que {limitValue, number} PB.', + 'The size of {property} cannot be larger than {limitValue, number} {limitValue, plural, one{byte} other{bytes}}.' => + 'O tamanho de {property} não pode ser maior que {limitValue, number} {limitValue, plural, one{byte} few{bytes} many{bytes} other{bytes}}.', + 'The size of {property} cannot be larger than {limitValue, number} KB.' => + 'O tamanho de {property} não pode ser maior que {limitValue, number} KB.', + 'The size of {property} cannot be larger than {limitValue, number} MB.' => + 'O tamanho de {property} não pode ser maior que {limitValue, number} MB.', + 'The size of {property} cannot be larger than {limitValue, number} GB.' => + 'O tamanho de {property} não pode ser maior que {limitValue, number} GB.', + 'The size of {property} cannot be larger than {limitValue, number} TB.' => + 'O tamanho de {property} não pode ser maior que {limitValue, number} TB.', + 'The size of {property} cannot be larger than {limitValue, number} PB.' => + 'O tamanho de {property} não pode ser maior que {limitValue, number} PB.', + /** @see Image */ '{Property} must be an image.' => '{Property} deve ser uma imagem.', 'The width of {property} must be exactly {exactly, number} {exactly, plural, one{pixel} other{pixels}}.' => diff --git a/messages/ru/yii-validator.php b/messages/ru/yii-validator.php index 6741cfc06..4299cc497 100644 --- a/messages/ru/yii-validator.php +++ b/messages/ru/yii-validator.php @@ -9,6 +9,7 @@ use Yiisoft\Validator\Rule\Each; use Yiisoft\Validator\Rule\Email; use Yiisoft\Validator\Rule\Equal; +use Yiisoft\Validator\Rule\File; use Yiisoft\Validator\Rule\FilledAtLeast; use Yiisoft\Validator\Rule\FilledOnlyOneOf; use Yiisoft\Validator\Rule\GreaterThan; @@ -196,6 +197,44 @@ /** @see AnyRule */ 'At least one of the inner rules must pass the validation.' => 'Как минимум одно из внутренних правил должно пройти валидацию.', + /** @see File */ + 'The size of {property} must be exactly {exactlyValue, number} {exactlyValue, plural, one{byte} other{bytes}}.' => + 'Размер {property} должен быть ровно {exactlyValue, number} {exactlyValue, plural, one{байт} few{байта} many{байтов} other{байта}}.', + 'The size of {property} must be exactly {exactlyValue, number} KB.' => + 'Размер {property} должен быть ровно {exactlyValue, number} КБ.', + 'The size of {property} must be exactly {exactlyValue, number} MB.' => + 'Размер {property} должен быть ровно {exactlyValue, number} МБ.', + 'The size of {property} must be exactly {exactlyValue, number} GB.' => + 'Размер {property} должен быть ровно {exactlyValue, number} ГБ.', + 'The size of {property} must be exactly {exactlyValue, number} TB.' => + 'Размер {property} должен быть ровно {exactlyValue, number} ТБ.', + 'The size of {property} must be exactly {exactlyValue, number} PB.' => + 'Размер {property} должен быть ровно {exactlyValue, number} ПБ.', + 'The size of {property} cannot be smaller than {limitValue, number} {limitValue, plural, one{byte} other{bytes}}.' => + 'Размер {property} не может быть меньше {limitValue, number} {limitValue, plural, one{байта} few{байтов} many{байтов} other{байта}}.', + 'The size of {property} cannot be smaller than {limitValue, number} KB.' => + 'Размер {property} не может быть меньше {limitValue, number} КБ.', + 'The size of {property} cannot be smaller than {limitValue, number} MB.' => + 'Размер {property} не может быть меньше {limitValue, number} МБ.', + 'The size of {property} cannot be smaller than {limitValue, number} GB.' => + 'Размер {property} не может быть меньше {limitValue, number} ГБ.', + 'The size of {property} cannot be smaller than {limitValue, number} TB.' => + 'Размер {property} не может быть меньше {limitValue, number} ТБ.', + 'The size of {property} cannot be smaller than {limitValue, number} PB.' => + 'Размер {property} не может быть меньше {limitValue, number} ПБ.', + 'The size of {property} cannot be larger than {limitValue, number} {limitValue, plural, one{byte} other{bytes}}.' => + 'Размер {property} не может быть больше {limitValue, number} {limitValue, plural, one{байта} few{байтов} many{байтов} other{байта}}.', + 'The size of {property} cannot be larger than {limitValue, number} KB.' => + 'Размер {property} не может быть больше {limitValue, number} КБ.', + 'The size of {property} cannot be larger than {limitValue, number} MB.' => + 'Размер {property} не может быть больше {limitValue, number} МБ.', + 'The size of {property} cannot be larger than {limitValue, number} GB.' => + 'Размер {property} не может быть больше {limitValue, number} ГБ.', + 'The size of {property} cannot be larger than {limitValue, number} TB.' => + 'Размер {property} не может быть больше {limitValue, number} ТБ.', + 'The size of {property} cannot be larger than {limitValue, number} PB.' => + 'Размер {property} не может быть больше {limitValue, number} ПБ.', + /** @see Image */ '{Property} must be an image.' => '{Property} должно быть изображением.', 'The width of {property} must be exactly {exactly, number} {exactly, plural, one{pixel} other{pixels}}.' => diff --git a/messages/uz/yii-validator.php b/messages/uz/yii-validator.php index aa65c432c..9c0226bb3 100644 --- a/messages/uz/yii-validator.php +++ b/messages/uz/yii-validator.php @@ -9,6 +9,7 @@ use Yiisoft\Validator\Rule\Each; use Yiisoft\Validator\Rule\Email; use Yiisoft\Validator\Rule\Equal; +use Yiisoft\Validator\Rule\File; use Yiisoft\Validator\Rule\FilledAtLeast; use Yiisoft\Validator\Rule\FilledOnlyOneOf; use Yiisoft\Validator\Rule\GreaterThan; @@ -188,6 +189,44 @@ /** @see AnyRule */ 'At least one of the inner rules must pass the validation.' => 'Ichki qoidalardan kamida bittasi tekshiruvdan oʻtishi kerak.', + /** @see File */ + 'The size of {property} must be exactly {exactlyValue, number} {exactlyValue, plural, one{byte} other{bytes}}.' => + '{property} hajmi aynan {exactlyValue, number} {exactlyValue, plural, one{bayt} other{bayt}} boʻlishi kerak.', + 'The size of {property} must be exactly {exactlyValue, number} KB.' => + '{property} hajmi aynan {exactlyValue, number} KB boʻlishi kerak.', + 'The size of {property} must be exactly {exactlyValue, number} MB.' => + '{property} hajmi aynan {exactlyValue, number} MB boʻlishi kerak.', + 'The size of {property} must be exactly {exactlyValue, number} GB.' => + '{property} hajmi aynan {exactlyValue, number} GB boʻlishi kerak.', + 'The size of {property} must be exactly {exactlyValue, number} TB.' => + '{property} hajmi aynan {exactlyValue, number} TB boʻlishi kerak.', + 'The size of {property} must be exactly {exactlyValue, number} PB.' => + '{property} hajmi aynan {exactlyValue, number} PB boʻlishi kerak.', + 'The size of {property} cannot be smaller than {limitValue, number} {limitValue, plural, one{byte} other{bytes}}.' => + '{property} hajmi {limitValue, number} {limitValue, plural, one{bayt} other{bayt}}dan kichik boʻlishi mumkin emas.', + 'The size of {property} cannot be smaller than {limitValue, number} KB.' => + '{property} hajmi {limitValue, number} KBdan kichik boʻlishi mumkin emas.', + 'The size of {property} cannot be smaller than {limitValue, number} MB.' => + '{property} hajmi {limitValue, number} MBdan kichik boʻlishi mumkin emas.', + 'The size of {property} cannot be smaller than {limitValue, number} GB.' => + '{property} hajmi {limitValue, number} GBdan kichik boʻlishi mumkin emas.', + 'The size of {property} cannot be smaller than {limitValue, number} TB.' => + '{property} hajmi {limitValue, number} TBdan kichik boʻlishi mumkin emas.', + 'The size of {property} cannot be smaller than {limitValue, number} PB.' => + '{property} hajmi {limitValue, number} PBdan kichik boʻlishi mumkin emas.', + 'The size of {property} cannot be larger than {limitValue, number} {limitValue, plural, one{byte} other{bytes}}.' => + '{property} hajmi {limitValue, number} {limitValue, plural, one{bayt} other{bayt}}dan katta boʻlishi mumkin emas.', + 'The size of {property} cannot be larger than {limitValue, number} KB.' => + '{property} hajmi {limitValue, number} KBdan katta boʻlishi mumkin emas.', + 'The size of {property} cannot be larger than {limitValue, number} MB.' => + '{property} hajmi {limitValue, number} MBdan katta boʻlishi mumkin emas.', + 'The size of {property} cannot be larger than {limitValue, number} GB.' => + '{property} hajmi {limitValue, number} GBdan katta boʻlishi mumkin emas.', + 'The size of {property} cannot be larger than {limitValue, number} TB.' => + '{property} hajmi {limitValue, number} TBdan katta boʻlishi mumkin emas.', + 'The size of {property} cannot be larger than {limitValue, number} PB.' => + '{property} hajmi {limitValue, number} PBdan katta boʻlishi mumkin emas.', + /** @see Image */ '{Property} must be an image.' => '{Property} rasm boʻlishi kerak.', 'The width of {property} must be exactly {exactly, number} {exactly, plural, one{pixel} other{pixels}}.' => diff --git a/src/Rule/File.php b/src/Rule/File.php index 158e20bcd..13043ad47 100644 --- a/src/Rule/File.php +++ b/src/Rule/File.php @@ -127,7 +127,10 @@ final class File implements DumpedRuleInterface, SkipOnErrorInterface, WhenInter * - `{property}`: the translated label of the property being validated. * - `{Property}`: the translated label of the property being validated, capitalized. * - `{file}`: the validated file name when it is available. - * - `{exactly}`: expected exact size in bytes. + * - `{exactly}`: expected exact size as a human-readable string. + * - `{exactlyValue}`: expected exact size numeric value converted to a human-readable unit. + * - `{exactlyUnit}`: expected exact size unit identifier. Possible values are `byte`, `KB`, `MB`, `GB`, `TB`, `PB`. + * - `{exactlyBytes}`: expected exact size in bytes. * @param string $tooSmallMessage A message used when the file size is less than {@see $minSize}. * * You may use the following placeholders in the message: @@ -135,7 +138,10 @@ final class File implements DumpedRuleInterface, SkipOnErrorInterface, WhenInter * - `{property}`: the translated label of the property being validated. * - `{Property}`: the translated label of the property being validated, capitalized. * - `{file}`: the validated file name when it is available. - * - `{limit}`: expected minimum size in bytes. + * - `{limit}`: expected minimum size as a human-readable string. + * - `{limitValue}`: expected minimum size numeric value converted to a human-readable unit. + * - `{limitUnit}`: expected minimum size unit identifier. Possible values are `byte`, `KB`, `MB`, `GB`, `TB`, `PB`. + * - `{limitBytes}`: expected minimum size in bytes. * @param string $tooBigMessage A message used when the file size is greater than {@see $maxSize}. * * You may use the following placeholders in the message: @@ -143,7 +149,10 @@ final class File implements DumpedRuleInterface, SkipOnErrorInterface, WhenInter * - `{property}`: the translated label of the property being validated. * - `{Property}`: the translated label of the property being validated, capitalized. * - `{file}`: the validated file name when it is available. - * - `{limit}`: expected maximum size in bytes. + * - `{limit}`: expected maximum size as a human-readable string. + * - `{limitValue}`: expected maximum size numeric value converted to a human-readable unit. + * - `{limitUnit}`: expected maximum size unit identifier. Possible values are `byte`, `KB`, `MB`, `GB`, `TB`, `PB`. + * - `{limitBytes}`: expected maximum size in bytes. * @param string $unableToDetermineSizeMessage A message used when file size constraints are configured, but the * file size can't be determined. * diff --git a/src/Rule/FileHandler.php b/src/Rule/FileHandler.php index 77d354926..509448516 100644 --- a/src/Rule/FileHandler.php +++ b/src/Rule/FileHandler.php @@ -21,6 +21,7 @@ use function is_string; use function mime_content_type; use function pathinfo; +use function round; use function str_contains; use function str_ends_with; use function str_replace; @@ -51,6 +52,19 @@ */ final class FileHandler implements RuleHandlerInterface { + private const DEFAULT_NOT_EXACT_SIZE_MESSAGES = [ + 'The size of {property} must be exactly {exactly, number} {exactly, plural, one{byte} other{bytes}}.', + 'The size of {property} must be exactly {exactly}.', + ]; + private const DEFAULT_TOO_SMALL_MESSAGES = [ + 'The size of {property} cannot be smaller than {limit, number} {limit, plural, one{byte} other{bytes}}.', + 'The size of {property} cannot be smaller than {limit}.', + ]; + private const DEFAULT_TOO_BIG_MESSAGES = [ + 'The size of {property} cannot be larger than {limit, number} {limit, plural, one{byte} other{bytes}}.', + 'The size of {property} cannot be larger than {limit}.', + ]; + public function validate(mixed $value, RuleInterface $rule, ValidationContext $context): Result { if (!$rule instanceof File) { @@ -279,27 +293,160 @@ private function validateSize(array $file, File $rule, ValidationContext $contex } if ($rule->getSize() !== null && $size !== $rule->getSize()) { + $ruleSize = $rule->getSize(); + $sizeParameters = $this->getSizeParameters($ruleSize); $result->addError( - $rule->getNotExactSizeMessage(), - $this->getParameters($context, $file, ['exactly' => $rule->getSize()]), + $this->getNotExactSizeMessage($rule, $sizeParameters['unit']), + $this->getParameters( + $context, + $file, + [ + 'exactly' => $sizeParameters['text'], + 'exactlyValue' => $sizeParameters['value'], + 'exactlyUnit' => $sizeParameters['unit'], + 'exactlyBytes' => $sizeParameters['bytes'], + ], + ), ); } if ($rule->getMinSize() !== null && $size < $rule->getMinSize()) { + $minSize = $rule->getMinSize(); + $sizeParameters = $this->getSizeParameters($minSize); $result->addError( - $rule->getTooSmallMessage(), - $this->getParameters($context, $file, ['limit' => $rule->getMinSize()]), + $this->getTooSmallMessage($rule, $sizeParameters['unit']), + $this->getParameters( + $context, + $file, + [ + 'limit' => $sizeParameters['text'], + 'limitValue' => $sizeParameters['value'], + 'limitUnit' => $sizeParameters['unit'], + 'limitBytes' => $sizeParameters['bytes'], + ], + ), ); } if ($rule->getMaxSize() !== null && $size > $rule->getMaxSize()) { + $maxSize = $rule->getMaxSize(); + $sizeParameters = $this->getSizeParameters($maxSize); $result->addError( - $rule->getTooBigMessage(), - $this->getParameters($context, $file, ['limit' => $rule->getMaxSize()]), + $this->getTooBigMessage($rule, $sizeParameters['unit']), + $this->getParameters( + $context, + $file, + [ + 'limit' => $sizeParameters['text'], + 'limitValue' => $sizeParameters['value'], + 'limitUnit' => $sizeParameters['unit'], + 'limitBytes' => $sizeParameters['bytes'], + ], + ), ); } } + private function getNotExactSizeMessage(File $rule, string $unit): string + { + $message = $rule->getNotExactSizeMessage(); + if (!in_array($message, self::DEFAULT_NOT_EXACT_SIZE_MESSAGES, true)) { + return $message; + } + + return match ($unit) { + 'byte' => 'The size of {property} must be exactly {exactlyValue, number} ' + . '{exactlyValue, plural, one{byte} other{bytes}}.', + 'KB' => 'The size of {property} must be exactly {exactlyValue, number} KB.', + 'MB' => 'The size of {property} must be exactly {exactlyValue, number} MB.', + 'GB' => 'The size of {property} must be exactly {exactlyValue, number} GB.', + 'TB' => 'The size of {property} must be exactly {exactlyValue, number} TB.', + 'PB' => 'The size of {property} must be exactly {exactlyValue, number} PB.', + default => $message, + }; + } + + private function getTooSmallMessage(File $rule, string $unit): string + { + $message = $rule->getTooSmallMessage(); + if (!in_array($message, self::DEFAULT_TOO_SMALL_MESSAGES, true)) { + return $message; + } + + return match ($unit) { + 'byte' => 'The size of {property} cannot be smaller than {limitValue, number} ' + . '{limitValue, plural, one{byte} other{bytes}}.', + 'KB' => 'The size of {property} cannot be smaller than {limitValue, number} KB.', + 'MB' => 'The size of {property} cannot be smaller than {limitValue, number} MB.', + 'GB' => 'The size of {property} cannot be smaller than {limitValue, number} GB.', + 'TB' => 'The size of {property} cannot be smaller than {limitValue, number} TB.', + 'PB' => 'The size of {property} cannot be smaller than {limitValue, number} PB.', + default => $message, + }; + } + + private function getTooBigMessage(File $rule, string $unit): string + { + $message = $rule->getTooBigMessage(); + if (!in_array($message, self::DEFAULT_TOO_BIG_MESSAGES, true)) { + return $message; + } + + return match ($unit) { + 'byte' => 'The size of {property} cannot be larger than {limitValue, number} ' + . '{limitValue, plural, one{byte} other{bytes}}.', + 'KB' => 'The size of {property} cannot be larger than {limitValue, number} KB.', + 'MB' => 'The size of {property} cannot be larger than {limitValue, number} MB.', + 'GB' => 'The size of {property} cannot be larger than {limitValue, number} GB.', + 'TB' => 'The size of {property} cannot be larger than {limitValue, number} TB.', + 'PB' => 'The size of {property} cannot be larger than {limitValue, number} PB.', + default => $message, + }; + } + + /** + * @return array{value: int|float, unit: string, bytes: int, text: string} + */ + private function getSizeParameters(int $size): array + { + if ($size < 1024) { + return [ + 'value' => $size, + 'unit' => 'byte', + 'bytes' => $size, + 'text' => $size . ' ' . ($size === 1 ? 'byte' : 'bytes'), + ]; + } + + $value = (float) $size; + foreach (['KB', 'MB', 'GB', 'TB'] as $unit) { + $value /= 1024; + $roundedValue = round($value, 2); + if ($roundedValue < 1024) { + return [ + 'value' => $roundedValue, + 'unit' => $unit, + 'bytes' => $size, + 'text' => $this->formatSizeText($roundedValue, $unit), + ]; + } + } + + $value /= 1024; + $roundedValue = round($value, 2); + return [ + 'value' => $roundedValue, + 'unit' => 'PB', + 'bytes' => $size, + 'text' => $this->formatSizeText($roundedValue, 'PB'), + ]; + } + + private function formatSizeText(int|float $value, string $unit): string + { + return $value . ' ' . $unit; + } + /** * @psalm-param FileData $file */ diff --git a/tests/MessagesTest.php b/tests/MessagesTest.php index ab12efa89..9afafe605 100644 --- a/tests/MessagesTest.php +++ b/tests/MessagesTest.php @@ -48,7 +48,11 @@ public function testBase(): void public static function dataNonEmpty(): array { return [ + ['de'], + ['pl'], + ['pt-BR'], ['ru'], + ['uz'], ]; } diff --git a/tests/Rule/FileTest.php b/tests/Rule/FileTest.php index ecc9bb820..5b319c239 100644 --- a/tests/Rule/FileTest.php +++ b/tests/Rule/FileTest.php @@ -10,6 +10,12 @@ use ReflectionException; use SplFileInfo; use stdClass; +use Yiisoft\Translator\CategorySource; +use Yiisoft\Translator\IdMessageReader; +use Yiisoft\Translator\IntlMessageFormatter; +use Yiisoft\Translator\Message\Php\MessageSource; +use Yiisoft\Translator\SimpleMessageFormatter; +use Yiisoft\Translator\Translator; use Yiisoft\Validator\PropertyTranslator\ArrayPropertyTranslator; use Yiisoft\Validator\PropertyTranslatorInterface; use Yiisoft\Validator\PropertyTranslatorProviderInterface; @@ -25,6 +31,8 @@ use Yiisoft\Validator\Validator; use function chmod; +use function dirname; +use function extension_loaded; use function file_put_contents; use function fopen; use function fwrite; @@ -149,15 +157,18 @@ public static function dataOptions(): array 'parameters' => [], ], 'notExactSizeMessage' => [ - 'template' => 'The size of {property} must be exactly {exactly, number} {exactly, plural, one{byte} other{bytes}}.', + 'template' => 'The size of {property} must be exactly {exactly, number} ' + . '{exactly, plural, one{byte} other{bytes}}.', 'parameters' => [], ], 'tooSmallMessage' => [ - 'template' => 'The size of {property} cannot be smaller than {limit, number} {limit, plural, one{byte} other{bytes}}.', + 'template' => 'The size of {property} cannot be smaller than {limit, number} ' + . '{limit, plural, one{byte} other{bytes}}.', 'parameters' => [], ], 'tooBigMessage' => [ - 'template' => 'The size of {property} cannot be larger than {limit, number} {limit, plural, one{byte} other{bytes}}.', + 'template' => 'The size of {property} cannot be larger than {limit, number} ' + . '{limit, plural, one{byte} other{bytes}}.', 'parameters' => [], ], 'unableToDetermineSizeMessage' => [ @@ -369,6 +380,60 @@ public static function dataValidationFailed(): array new File(maxSize: 920), ['' => ['The size of value cannot be larger than 920 bytes.']], ], + 'exact size mismatch uses human-readable size' => [ + self::TEXT_FILE, + new File(size: 50 * 1024 * 1024), + ['' => ['The size of value must be exactly 50 MB.']], + ], + 'too small uses human-readable size' => [ + self::TEXT_FILE, + new File(minSize: 1536), + ['' => ['The size of value cannot be smaller than 1.5 KB.']], + ], + 'too big uses human-readable size' => [ + self::JPG_FILE, + new File(maxSize: 512), + ['' => ['The size of value cannot be larger than 512 bytes.']], + ], + 'size boundary rounds to next unit' => [ + self::createStreamUpload('large.bin', 'application/octet-stream', 1024 * 1024), + new File(maxSize: 1024 * 1024 - 1), + ['' => ['The size of value cannot be larger than 1 MB.']], + ], + 'custom message uses human-readable size placeholders' => [ + self::createStreamUpload('large.bin', 'application/octet-stream', 60 * 1024 * 1024), + new File( + maxSize: 50 * 1024 * 1024, + tooBigMessage: '{file} is larger than {limit}.', + ), + ['' => ['large.bin is larger than 50 MB.']], + ], + 'custom message can use human-readable size value and unit placeholders' => [ + self::createStreamUpload('large.bin', 'application/octet-stream', 60 * 1024 * 1024), + new File( + maxSize: 50 * 1024 * 1024, + tooBigMessage: '{file} is larger than {limitValue, number} {limitUnit}.', + ), + ['' => ['large.bin is larger than 50 MB.']], + ], + 'custom message can use raw byte size placeholder' => [ + self::JPG_FILE, + new File( + maxSize: 512, + tooBigMessage: '{file} limit is {limit}; raw limit is {limitBytes, number} ' + . '{limitBytes, plural, one{byte} other{bytes}}.', + ), + ['' => ['16x18.jpg limit is 512 bytes; raw limit is 512 bytes.']], + ], + 'custom exact size message can use raw byte size placeholder' => [ + self::JPG_FILE, + new File( + size: 512, + notExactSizeMessage: '{file} must be {exactly}; raw size is {exactlyBytes, number} ' + . '{exactlyBytes, plural, one{byte} other{bytes}}.', + ), + ['' => ['16x18.jpg must be 512 bytes; raw size is 512 bytes.']], + ], 'stream upload unknown exact size' => [ self::createStreamUpload('resume.txt', 'text/plain', null), new File(extensions: 'txt', mimeTypes: 'text/plain', size: 22, trustClientMediaType: true), @@ -503,6 +568,76 @@ public function testUnreadableFileMimeValidationDoesNotEmitWarning(): void ); } + public function testDefaultSizeMessageWithSimpleMessageFormatter(): void + { + $translator = (new Translator())->addCategorySources( + new CategorySource( + Validator::DEFAULT_TRANSLATION_CATEGORY, + new IdMessageReader(), + new SimpleMessageFormatter(), + ), + ); + + $result = (new Validator(translator: $translator))->validate(self::JPG_FILE, new File(maxSize: 512)); + + $this->assertSame( + ['' => ['The size of value cannot be larger than 512 bytes.']], + $result->getErrorMessagesIndexedByPath(), + ); + } + + public function testDefaultSizeMessagesAreTranslatedToRussian(): void + { + if (!extension_loaded('intl')) { + $this->markTestSkipped('The "intl" extension is required for Russian plural rules.'); + } + + $messagesPath = dirname(__DIR__) . '/../messages'; + $this->assertDirectoryExists($messagesPath); + + $translator = (new Translator('ru'))->withLocale('ru')->addCategorySources( + new CategorySource( + Validator::DEFAULT_TRANSLATION_CATEGORY, + new MessageSource($messagesPath), + new IntlMessageFormatter(), + ), + ); + $validator = new Validator(translator: $translator); + + $result = $validator->validate(self::JPG_FILE, new File(maxSize: 512)); + $this->assertSame( + ['' => ['Размер value не может быть больше 512 байтов.']], + $result->getErrorMessagesIndexedByPath(), + ); + + $result = $validator->validate(self::EMPTY_JPG_FILE, new File(minSize: 1)); + $this->assertSame( + ['' => ['Размер value не может быть меньше 1 байта.']], + $result->getErrorMessagesIndexedByPath(), + ); + + $result = $validator->validate(self::TEXT_FILE, new File(minSize: 1024)); + $this->assertSame( + ['' => ['Размер value не может быть меньше 1 КБ.']], + $result->getErrorMessagesIndexedByPath(), + ); + + $result = $validator->validate(self::TEXT_FILE, new File(size: 50 * 1024 * 1024)); + $this->assertSame( + ['' => ['Размер value должен быть ровно 50 МБ.']], + $result->getErrorMessagesIndexedByPath(), + ); + + $result = $validator->validate( + self::createStreamUpload('large.bin', 'application/octet-stream', 1024 * 1024), + new File(maxSize: 1024 * 1024 - 1), + ); + $this->assertSame( + ['' => ['Размер value не может быть больше 1 МБ.']], + $result->getErrorMessagesIndexedByPath(), + ); + } + protected function getDifferentRuleInHandlerItems(): array { return [File::class, FileHandler::class];