diff --git a/README.md b/README.md index c63be39..07e14e4 100644 --- a/README.md +++ b/README.md @@ -267,6 +267,38 @@ $validation_a = $validator->make($dataset_a, [ $validation_a->validate(); ``` +## Translation + +Translation is different with custom messages. +Translation may needed when you use custom message for rule `in`, `not_in`, `mimes`, and `uploaded_file`. + +For example if you use rule `in:1,2,3` we will set invalid message like "The Attribute only allows '1', '2', or '3'" +where part "'1', '2', or '3'" is comes from ":allowed_values" tag. +So if you have custom Indonesian message ":attribute hanya memperbolehkan :allowed_values", +we will set invalid message like "Attribute hanya memperbolehkan '1', '2', or '3'" which is the "or" word is not part of Indonesian language. + +So, to solve this problem, we can use translation like this: + +```php +// Set translation for words 'and' and 'or'. +$validator->setTranslations([ + 'and' => 'dan', + 'or' => 'atau' +]); + +// Set custom message for 'in' rule +$validator->setMessage('in', ":attribute hanya memperbolehkan :allowed_values"); + +// Validate +$validation = $validator->validate($inputs, [ + 'nomor' => 'in:1,2,3' +]); + +$message = $validation->errors()->first('nomor'); // "Nomor hanya memperbolehkan '1', '2', atau '3'" +``` + +> Actually, our built-in rules only use words 'and' and 'or' that you may need to translates. + ## Working with Error Message Errors messages are collected in `Rakit\Validation\ErrorBag` object that you can get it using `errors()` method. diff --git a/src/Helper.php b/src/Helper.php index c0a3150..816f020 100644 --- a/src/Helper.php +++ b/src/Helper.php @@ -203,4 +203,49 @@ public static function snakeCase(string $value, string $delimiter = '_'): string return $value; } + + /** + * Join string[] to string with given $separator and $lastSeparator. + * + * @param array $pieces + * @param string $separator + * @param string|null $lastSeparator + * @return string + */ + public static function join(array $pieces, string $separator, string $lastSeparator = null): string + { + if (is_null($lastSeparator)) { + $lastSeparator = $separator; + } + + $last = array_pop($pieces); + + switch (count($pieces)) { + case 0: + return $last ?: ''; + case 1: + return $pieces[0] . $lastSeparator . $last; + default: + return implode($separator, $pieces) . $lastSeparator . $last; + } + } + + /** + * Wrap string[] by given $prefix and $suffix + * + * @param array $strings + * @param string $prefix + * @param string|null $suffix + * @return array + */ + public static function wraps(array $strings, string $prefix, string $suffix = null): array + { + if (is_null($suffix)) { + $suffix = $prefix; + } + + return array_map(function ($str) use ($prefix, $suffix) { + return $prefix . $str . $suffix; + }, $strings); + } } diff --git a/src/Rule.php b/src/Rule.php index a724240..a2674ab 100644 --- a/src/Rule.php +++ b/src/Rule.php @@ -21,6 +21,9 @@ abstract class Rule /** @var array */ protected $params = []; + /** @var array */ + protected $paramsTexts = []; + /** @var array */ protected $fillableParams = []; @@ -145,6 +148,28 @@ public function parameter(string $key) return isset($this->params[$key])? $this->params[$key] : null; } + /** + * Set parameter text that can be displayed in error message using ':param_key' + * + * @param string $key + * @param string $text + * @return void + */ + public function setParameterText(string $key, string $text) + { + $this->paramsTexts[$key] = $text; + } + + /** + * Get $paramsTexts + * + * @return array + */ + public function getParametersTexts(): array + { + return $this->paramsTexts; + } + /** * Check whether this rule is implicit * diff --git a/src/Rules/In.php b/src/Rules/In.php index ae98076..1971ed5 100644 --- a/src/Rules/In.php +++ b/src/Rules/In.php @@ -2,13 +2,14 @@ namespace Rakit\Validation\Rules; +use Rakit\Validation\Helper; use Rakit\Validation\Rule; class In extends Rule { /** @var string */ - protected $message = "The :attribute is not allowing :value"; + protected $message = "The :attribute only allows :allowed_values"; /** @var bool */ protected $strict = false; @@ -49,7 +50,12 @@ public function check($value): bool { $this->requireParameters(['allowed_values']); - $allowed_values = $this->parameter('allowed_values'); - return in_array($value, $allowed_values, $this->strict); + $allowedValues = $this->parameter('allowed_values'); + + $or = $this->validation ? $this->validation->getTranslation('or') : 'or'; + $allowedValuesText = Helper::join(Helper::wraps($allowedValues, "'"), ', ', ", {$or} "); + $this->setParameterText('allowed_values', $allowedValuesText); + + return in_array($value, $allowedValues, $this->strict); } } diff --git a/src/Rules/Mimes.php b/src/Rules/Mimes.php index d2ce8f1..9abb5a8 100644 --- a/src/Rules/Mimes.php +++ b/src/Rules/Mimes.php @@ -2,15 +2,16 @@ namespace Rakit\Validation\Rules; -use Rakit\Validation\Rule; +use Rakit\Validation\Helper; use Rakit\Validation\MimeTypeGuesser; +use Rakit\Validation\Rule; class Mimes extends Rule { use Traits\FileTrait; /** @var string */ - protected $message = "The :attribute file type is not allowed"; + protected $message = "The :attribute file type must be :allowed_types"; /** @var string|int */ protected $maxSize = null; @@ -60,6 +61,11 @@ public function check($value): bool { $allowedTypes = $this->parameter('allowed_types'); + if ($allowedTypes) { + $or = $this->validation ? $this->validation->getTranslation('or') : 'or'; + $this->setParameterText('allowed_types', Helper::join(Helper::wraps($allowedTypes, "'"), ', ', ", {$or} ")); + } + // below is Required rule job if (!$this->isValueFromUploadedFiles($value) or $value['error'] == UPLOAD_ERR_NO_FILE) { return true; diff --git a/src/Rules/NotIn.php b/src/Rules/NotIn.php index 4f4ad5e..f70275a 100644 --- a/src/Rules/NotIn.php +++ b/src/Rules/NotIn.php @@ -2,13 +2,14 @@ namespace Rakit\Validation\Rules; +use Rakit\Validation\Helper; use Rakit\Validation\Rule; class NotIn extends Rule { /** @var string */ - protected $message = "The :attribute is not allowing :value"; + protected $message = "The :attribute is not allowing :disallowed_values"; /** @var bool */ protected $strict = false; @@ -48,7 +49,13 @@ public function strict($strict = true) public function check($value): bool { $this->requireParameters(['disallowed_values']); + $disallowedValues = (array) $this->parameter('disallowed_values'); + + $and = $this->validation ? $this->validation->getTranslation('and') : 'and'; + $disallowedValuesText = Helper::join(Helper::wraps($disallowedValues, "'"), ', ', ", {$and} "); + $this->setParameterText('disallowed_values', $disallowedValuesText); + return !in_array($value, $disallowedValues, $this->strict); } } diff --git a/src/Rules/UploadedFile.php b/src/Rules/UploadedFile.php index eb530b0..ce5830e 100644 --- a/src/Rules/UploadedFile.php +++ b/src/Rules/UploadedFile.php @@ -12,7 +12,7 @@ class UploadedFile extends Rule implements BeforeValidate use Traits\FileTrait, Traits\SizeTrait; /** @var string */ - protected $message = "The :attribute is not valid"; + protected $message = "The :attribute is not valid uploaded file"; /** @var string|int */ protected $maxSize = null; @@ -133,6 +133,11 @@ public function check($value): bool $maxSize = $this->parameter('max_size'); $allowedTypes = $this->parameter('allowed_types'); + if ($allowedTypes) { + $or = $this->validation ? $this->validation->getTranslation('or') : 'or'; + $this->setParameterText('allowed_types', Helper::join(Helper::wraps($allowedTypes, "'"), ', ', ", {$or} ")); + } + // below is Required rule job if (!$this->isValueFromUploadedFiles($value) or $value['error'] == UPLOAD_ERR_NO_FILE) { return true; @@ -150,6 +155,7 @@ public function check($value): bool if ($minSize) { $bytesMinSize = $this->getBytesSize($minSize); if ($value['size'] < $bytesMinSize) { + $this->setMessage('The :attribute file is too small, minimum size is :min_size'); return false; } } @@ -157,6 +163,7 @@ public function check($value): bool if ($maxSize) { $bytesMaxSize = $this->getBytesSize($maxSize); if ($value['size'] > $bytesMaxSize) { + $this->setMessage('The :attribute file is too large, maximum size is :max_size'); return false; } } @@ -167,6 +174,7 @@ public function check($value): bool unset($guesser); if (!in_array($ext, $allowedTypes)) { + $this->setMessage('The :attribute file type must be :allowed_types'); return false; } } diff --git a/src/Traits/TranslationsTrait.php b/src/Traits/TranslationsTrait.php new file mode 100644 index 0000000..db935e7 --- /dev/null +++ b/src/Traits/TranslationsTrait.php @@ -0,0 +1,54 @@ +translations[$key] = $translation; + } + + /** + * Given $translations and set multiple translations + * + * @param array $translations + * @return void + */ + public function setTranslations(array $translations) + { + $this->translations = array_merge($this->translations, $translations); + } + + /** + * Given translation from given $key + * + * @param string $key + * @return string + */ + public function getTranslation(string $key): string + { + return array_key_exists($key, $this->translations) ? $this->translations[$key] : $key; + } + + /** + * Get all $translations + * + * @return array + */ + public function getTranslations(): array + { + return $this->translations; + } +} diff --git a/src/Validation.php b/src/Validation.php index 1d6765f..71f01b2 100644 --- a/src/Validation.php +++ b/src/Validation.php @@ -9,6 +9,7 @@ class Validation { + use Traits\TranslationsTrait; /** @var mixed */ protected $validator; @@ -386,7 +387,7 @@ protected function resolveAttributeName(Attribute $attribute): string protected function resolveMessage(Attribute $attribute, $value, Rule $validator): string { $primaryAttribute = $attribute->getPrimaryAttribute(); - $params = $validator->getParameters(); + $params = array_merge($validator->getParameters(), $validator->getParametersTexts()); $attributeKey = $attribute->getKey(); $ruleKey = $validator->getKey(); $alias = $attribute->getAlias() ?: $this->resolveAttributeName($attribute); diff --git a/src/Validator.php b/src/Validator.php index e220b37..ba25f62 100644 --- a/src/Validator.php +++ b/src/Validator.php @@ -4,10 +4,14 @@ class Validator { + use Traits\TranslationsTrait; /** @var array */ protected $messages = []; + /** @var translations */ + protected $translations = []; + /** @var array */ protected $validators = []; @@ -102,7 +106,10 @@ public function validate(array $inputs, array $rules, array $messages = []): Val public function make(array $inputs, array $rules, array $messages = []): Validation { $messages = array_merge($this->messages, $messages); - return new Validation($this, $inputs, $rules, $messages); + $validation = new Validation($this, $inputs, $rules, $messages); + $validation->setTranslations($this->getTranslations()); + + return $validation; } /** diff --git a/tests/HelperTest.php b/tests/HelperTest.php index a6275a3..8717840 100644 --- a/tests/HelperTest.php +++ b/tests/HelperTest.php @@ -131,4 +131,28 @@ public function testArrayUnset() 'message' => "lorem ipsum", ]); } + + public function testJoin() + { + $pieces0 = []; + $pieces1 = [1]; + $pieces2 = [1, 2]; + $pieces3 = [1, 2, 3]; + + $separator = ', '; + $lastSeparator = ', and '; + + $this->assertEquals(Helper::join($pieces0, $separator, $lastSeparator), ''); + $this->assertEquals(Helper::join($pieces1, $separator, $lastSeparator), '1'); + $this->assertEquals(Helper::join($pieces2, $separator, $lastSeparator), '1, and 2'); + $this->assertEquals(Helper::join($pieces3, $separator, $lastSeparator), '1, 2, and 3'); + } + + public function testWraps() + { + $inputs = [1, 2, 3]; + + $this->assertEquals(Helper::wraps($inputs, '-'), ['-1-', '-2-', '-3-']); + $this->assertEquals(Helper::wraps($inputs, '-', '+'), ['-1+', '-2+', '-3+']); + } } diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php index bfdf5a9..6bde12f 100644 --- a/tests/ValidatorTest.php +++ b/tests/ValidatorTest.php @@ -1277,4 +1277,144 @@ public function testGetInvalidData() $this->assertFalse(isset($stuffs['one'])); $this->assertFalse(isset($stuffs['two'])); } + + public function testRuleInInvalidMessages() + { + $validation = $this->validator->validate([ + 'number' => 1 + ], [ + 'number' => 'in:7,8,9', + ]); + + $this->assertEquals($validation->errors()->first('number'), "The Number only allows '7', '8', or '9'"); + + // Using translation + $this->validator->setTranslation('or', 'atau'); + + $validation = $this->validator->validate([ + 'number' => 1 + ], [ + 'number' => 'in:7,8,9', + ]); + + $this->assertEquals($validation->errors()->first('number'), "The Number only allows '7', '8', atau '9'"); + } + + public function testRuleNotInInvalidMessages() + { + $validation = $this->validator->validate([ + 'number' => 1 + ], [ + 'number' => 'not_in:1,2,3', + ]); + + $this->assertEquals($validation->errors()->first('number'), "The Number is not allowing '1', '2', and '3'"); + + // Using translation + $this->validator->setTranslation('and', 'dan'); + + $validation = $this->validator->validate([ + 'number' => 1 + ], [ + 'number' => 'not_in:1,2,3', + ]); + + $this->assertEquals($validation->errors()->first('number'), "The Number is not allowing '1', '2', dan '3'"); + } + + public function testRuleMimesInvalidMessages() + { + $file = [ + 'name' => 'sample.txt', + 'type' => 'plain/text', + 'tmp_name' => __FILE__, + 'size' => 1000, + 'error' => UPLOAD_ERR_OK, + ]; + + $validation = $this->validator->validate([ + 'sample' => $file, + ], [ + 'sample' => 'mimes:jpeg,png,bmp', + ]); + + $expectedMessage = "The Sample file type must be 'jpeg', 'png', or 'bmp'"; + $this->assertEquals($validation->errors()->first('sample'), $expectedMessage); + + // Using translation + $this->validator->setTranslation('or', 'atau'); + + $validation = $this->validator->validate([ + 'sample' => $file, + ], [ + 'sample' => 'mimes:jpeg,png,bmp', + ]); + + $expectedMessage = "The Sample file type must be 'jpeg', 'png', atau 'bmp'"; + $this->assertEquals($validation->errors()->first('sample'), $expectedMessage); + } + + public function testRuleUploadedFileInvalidMessages() + { + $file = [ + 'name' => 'sample.txt', + 'type' => 'plain/text', + 'tmp_name' => __FILE__, + 'size' => 1024 * 1024 * 2, // 2M + 'error' => UPLOAD_ERR_OK, + ]; + + $rule = $this->getMockedUploadedFileRule(); + + // Invalid uploaded file (!is_uploaded_file($file['tmp_name'])) + $validation = $this->validator->validate([ + 'sample' => $file, + ], [ + 'sample' => 'uploaded_file', + ]); + + $expectedMessage = "The Sample is not valid uploaded file"; + $this->assertEquals($validation->errors()->first('sample'), $expectedMessage); + + // Invalid min size + $validation = $this->validator->validate([ + 'sample' => $file, + ], [ + 'sample' => [(clone $rule)->minSize('3M')], + ]); + + $expectedMessage = "The Sample file is too small, minimum size is 3M"; + $this->assertEquals($validation->errors()->first('sample'), $expectedMessage); + + // Invalid max size + $validation = $this->validator->validate([ + 'sample' => $file, + ], [ + 'sample' => [(clone $rule)->maxSize('1M')], + ]); + + $expectedMessage = "The Sample file is too large, maximum size is 1M"; + $this->assertEquals($validation->errors()->first('sample'), $expectedMessage); + + // Invalid file types + $validation = $this->validator->validate([ + 'sample' => $file, + ], [ + 'sample' => [(clone $rule)->fileTypes(['jpeg', 'png', 'bmp'])], + ]); + + $expectedMessage = "The Sample file type must be 'jpeg', 'png', or 'bmp'"; + $this->assertEquals($validation->errors()->first('sample'), $expectedMessage); + + // Invalid file types with translation + $this->validator->setTranslation('or', 'atau'); + $validation = $this->validator->validate([ + 'sample' => $file, + ], [ + 'sample' => [(clone $rule)->fileTypes(['jpeg', 'png', 'bmp'])], + ]); + + $expectedMessage = "The Sample file type must be 'jpeg', 'png', atau 'bmp'"; + $this->assertEquals($validation->errors()->first('sample'), $expectedMessage); + } }