value object. * * Unknown locale keys are rejected rather than ignored, so a typo like "gb" * fails loudly instead of silently writing a translation nobody will ever read. */ class Translatable implements ValidationRule { public function __construct( protected bool $requireDefaultLocale = false, protected ?int $max = null, ) {} public function validate(string $attribute, mixed $value, Closure $fail): void { $supported = array_keys(config('locales.supported')); $default = (string) config('locales.default'); if (is_string($value)) { $value = [app()->getLocale() => $value]; } if (! is_array($value)) { $fail(__('rules.translatable.type', ['attribute' => $attribute])); return; } foreach ($value as $locale => $translation) { if (! in_array((string) $locale, $supported, true)) { $fail(__('rules.translatable.unsupported_locale', [ 'attribute' => $attribute, 'locale' => (string) $locale, 'supported' => implode(', ', $supported), ])); return; } if ($translation !== null && ! is_string($translation)) { $fail(__('rules.translatable.type', ['attribute' => $attribute])); return; } if ($this->max !== null && is_string($translation) && mb_strlen($translation) > $this->max) { $fail(__('rules.translatable.max', [ 'attribute' => $attribute, 'locale' => (string) $locale, 'max' => $this->max, ])); return; } } // The default locale is the fallback every other language borrows from // when it has no translation, so a record without it can render blank. if ($this->requireDefaultLocale && blank($value[$default] ?? null)) { $fail(__('rules.translatable.default_required', [ 'attribute' => $attribute, 'locale' => $default, ])); } } }