diff --git a/.codeclimate.yml b/.codeclimate.yml index 1d955bb3..6078e07b 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -10,6 +10,8 @@ checks: enabled: false method-complexity: enabled: true + config: + threshold: 6 method-count: enabled: true method-lines: diff --git a/.gitignore b/.gitignore index a8fd3ed4..0bd2672a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ vendor/ composer.lock coverage/ +.phpunit.result.cache diff --git a/src/Translatable/Validation/RuleFactory.php b/src/Translatable/Validation/RuleFactory.php new file mode 100644 index 00000000..e2cc6a9e --- /dev/null +++ b/src/Translatable/Validation/RuleFactory.php @@ -0,0 +1,112 @@ +format = $format ?? $config->get('translatable.rule_factory.format'); + $this->prefix = $prefix ?? $config->get('translatable.rule_factory.prefix'); + $this->suffix = $suffix ?? $config->get('translatable.rule_factory.suffix'); + } + + public static function make(array $rules, ?int $format = null, ?string $prefix = null, ?string $suffix = null, ?array $locales = null): array + { + $factory = app()->make(static::class, compact('format', 'prefix', 'suffix')); + + $factory->setLocales($locales); + + return $factory->parse($rules); + } + + public function setLocales(?array $locales = null): self + { + /** @var Locales */ + $helper = app(Locales::class); + + if (is_null($locales)) { + $this->locales = $helper->all(); + + return $this; + } + + foreach ($locales as $locale) { + if (! $helper->has($locale)) { + throw new InvalidArgumentException(sprintf('The locale [%s] is not defined in available locales.', $locale)); + } + } + + $this->locales = $locales; + + return $this; + } + + public function parse(array $input): array + { + $rules = []; + + foreach ($input as $key => $value) { + if (! $this->isTranslatable($key)) { + $rules[$key] = $value; + continue; + } + + foreach ($this->locales as $locale) { + $rules[$this->formatKey($locale, $key)] = $value; + } + } + + return $rules; + } + + protected function formatKey(string $locale, string $key): string + { + switch ($this->format) { + case self::FORMAT_ARRAY: + return preg_replace($this->getPattern(), $locale.'.$1', $key); + case self::FORMAT_KEY: + return preg_replace($this->getPattern(), '$1:'.$locale, $key); + } + } + + protected function getPattern(): string + { + $prefix = preg_quote($this->prefix); + $suffix = preg_quote($this->suffix); + + return '/'.$prefix.'([^\.'.$prefix.$suffix.']+)'.$suffix.'/'; + } + + protected function isTranslatable(string $key): bool + { + return strpos($key, $this->prefix) !== false && strpos($key, $this->suffix) !== false; + } +} diff --git a/src/config/translatable.php b/src/config/translatable.php index 6864e284..ddf71d08 100644 --- a/src/config/translatable.php +++ b/src/config/translatable.php @@ -131,4 +131,19 @@ | */ 'to_array_always_loads_translations' => true, + + /* + |-------------------------------------------------------------------------- + | Configure the default behavior of the rule factory + |-------------------------------------------------------------------------- + | The default values used to control the behavior of the RuleFactory. + | Here you can set your own default format and delimiters for + | your whole app. + * + */ + 'rule_factory' => [ + 'format' => \Astrotomic\Translatable\Validation\RuleFactory::FORMAT_ARRAY, + 'prefix' => '%', + 'suffix' => '%', + ], ]; diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php new file mode 100644 index 00000000..102e96db --- /dev/null +++ b/tests/ValidationTest.php @@ -0,0 +1,279 @@ + 'required', + 'author_id' => [ + 'required', + 'int', + ], + ]; + + $this->assertEquals($rules, RuleFactory::make($rules)); + } + + public function test_format_array_it_replaces_single_key() + { + $rules = [ + 'title' => 'required', + '%content%' => 'required', + ]; + + $this->assertEquals([ + 'title' => 'required', + 'en.content' => 'required', + 'de.content' => 'required', + 'de-DE.content' => 'required', + 'de-AT.content' => 'required', + ], RuleFactory::make($rules, RuleFactory::FORMAT_ARRAY)); + } + + public function test_format_array_it_replaces_sub_key() + { + $rules = [ + 'title' => 'required', + 'translations.%content%' => 'required', + ]; + + $this->assertEquals([ + 'title' => 'required', + 'translations.en.content' => 'required', + 'translations.de.content' => 'required', + 'translations.de-DE.content' => 'required', + 'translations.de-AT.content' => 'required', + ], RuleFactory::make($rules, RuleFactory::FORMAT_ARRAY)); + } + + public function test_format_array_it_replaces_middle_key() + { + $rules = [ + 'title' => 'required', + 'translations.%content%.body' => 'required', + ]; + + $this->assertEquals([ + 'title' => 'required', + 'translations.en.content.body' => 'required', + 'translations.de.content.body' => 'required', + 'translations.de-DE.content.body' => 'required', + 'translations.de-AT.content.body' => 'required', + ], RuleFactory::make($rules, RuleFactory::FORMAT_ARRAY)); + } + + public function test_format_array_it_replaces_middle_key_with_custom_prefix() + { + $rules = [ + 'title' => 'required', + 'translations.{content%.body' => 'required', + ]; + + $this->assertEquals([ + 'title' => 'required', + 'translations.en.content.body' => 'required', + 'translations.de.content.body' => 'required', + 'translations.de-DE.content.body' => 'required', + 'translations.de-AT.content.body' => 'required', + ], RuleFactory::make($rules, RuleFactory::FORMAT_ARRAY, '{')); + } + + public function test_format_array_it_replaces_middle_key_with_custom_suffix() + { + $rules = [ + 'title' => 'required', + 'translations.%content}.body' => 'required', + ]; + + $this->assertEquals([ + 'title' => 'required', + 'translations.en.content.body' => 'required', + 'translations.de.content.body' => 'required', + 'translations.de-DE.content.body' => 'required', + 'translations.de-AT.content.body' => 'required', + ], RuleFactory::make($rules, RuleFactory::FORMAT_ARRAY, '%', '}')); + } + + public function test_format_array_it_replaces_middle_key_with_custom_delimiters() + { + $rules = [ + 'title' => 'required', + 'translations.{content}.body' => 'required', + ]; + + $this->assertEquals([ + 'title' => 'required', + 'translations.en.content.body' => 'required', + 'translations.de.content.body' => 'required', + 'translations.de-DE.content.body' => 'required', + 'translations.de-AT.content.body' => 'required', + ], RuleFactory::make($rules, RuleFactory::FORMAT_ARRAY, '{', '}')); + } + + public function test_format_array_it_replaces_middle_key_with_custom_regex_delimiters() + { + $rules = [ + 'title' => 'required', + 'translations.$content$.body' => 'required', + ]; + + $this->assertEquals([ + 'title' => 'required', + 'translations.en.content.body' => 'required', + 'translations.de.content.body' => 'required', + 'translations.de-DE.content.body' => 'required', + 'translations.de-AT.content.body' => 'required', + ], RuleFactory::make($rules, RuleFactory::FORMAT_ARRAY, '$', '$')); + } + + public function test_format_array_it_uses_config_as_default() + { + app('config')->set('translatable.rule_factory', [ + 'format' => RuleFactory::FORMAT_ARRAY, + 'prefix' => '{', + 'suffix' => '}', + ]); + + $rules = [ + 'title' => 'required', + '{content}' => 'required', + '%content%' => 'required', + ]; + + $this->assertEquals([ + 'title' => 'required', + '%content%' => 'required', + 'en.content' => 'required', + 'de.content' => 'required', + 'de-DE.content' => 'required', + 'de-AT.content' => 'required', + ], RuleFactory::make($rules)); + } + + public function test_format_key_it_replaces_single_key() + { + $rules = [ + 'title' => 'required', + '%content%' => 'required', + ]; + + $this->assertEquals([ + 'title' => 'required', + 'content:en' => 'required', + 'content:de' => 'required', + 'content:de-DE' => 'required', + 'content:de-AT' => 'required', + ], RuleFactory::make($rules, RuleFactory::FORMAT_KEY)); + } + + public function test_format_key_it_replaces_sub_key() + { + $rules = [ + 'title' => 'required', + 'translations.%content%' => 'required', + ]; + + $this->assertEquals([ + 'title' => 'required', + 'translations.content:en' => 'required', + 'translations.content:de' => 'required', + 'translations.content:de-DE' => 'required', + 'translations.content:de-AT' => 'required', + ], RuleFactory::make($rules, RuleFactory::FORMAT_KEY)); + } + + public function test_format_key_it_replaces_middle_key() + { + $rules = [ + 'title' => 'required', + 'translations.%content%.body' => 'required', + ]; + + $this->assertEquals([ + 'title' => 'required', + 'translations.content:en.body' => 'required', + 'translations.content:de.body' => 'required', + 'translations.content:de-DE.body' => 'required', + 'translations.content:de-AT.body' => 'required', + ], RuleFactory::make($rules, RuleFactory::FORMAT_KEY)); + } + + public function test_format_key_it_uses_config_as_default() + { + app('config')->set('translatable.rule_factory', [ + 'format' => RuleFactory::FORMAT_KEY, + 'prefix' => '{', + 'suffix' => '}', + ]); + + $rules = [ + 'title' => 'required', + '{content}' => 'required', + '%content%' => 'required', + ]; + + $this->assertEquals([ + 'title' => 'required', + '%content%' => 'required', + 'content:en' => 'required', + 'content:de' => 'required', + 'content:de-DE' => 'required', + 'content:de-AT' => 'required', + ], RuleFactory::make($rules)); + } + + public function test_it_replaces_key_with_custom_locales() + { + $rules = [ + 'title' => 'required', + 'translations.%content%.body' => 'required', + ]; + + $this->assertEquals([ + 'title' => 'required', + 'translations.en.content.body' => 'required', + 'translations.de.content.body' => 'required', + ], RuleFactory::make($rules, RuleFactory::FORMAT_ARRAY, '%', '%', [ + 'en', + 'de', + ])); + } + + public function test_it_throws_exception_with_undefined_locales() + { + $this->expectException(InvalidArgumentException::class); + + $rules = [ + 'title' => 'required', + 'translations.$content$.body' => 'required', + ]; + + RuleFactory::make($rules, RuleFactory::FORMAT_ARRAY, '%', '%', [ + 'en', + 'de', + 'at', + ]); + } + + protected function setUp(): void + { + $this->setUpTheTestEnvironment(); + app('config')->set('translatable.locales', [ + 'en', + 'de' => [ + 'DE', + 'AT', + ], + ]); + $this->getLocalesHelper()->load(); + } + + private function getLocalesHelper(): Locales + { + return app(Locales::class); + } +}