diff --git a/docs/10-about/02-contributing.md b/docs/10-about/02-contributing.md index 97593a6e3b7..eed87598827 100644 --- a/docs/10-about/02-contributing.md +++ b/docs/10-about/02-contributing.md @@ -78,6 +78,13 @@ If you've published the translations into your app and you'd like to check those php artisan filament:check-translations es --source=app ``` +You need to fix the translations where the English strings have changed. If you want to check the strings, you can list all translations with `--list` option: + +```bash +php artisan filament:check-translations es --list +php artisan filament:check-translations es --source=app --list +``` + ## Security vulnerabilities If you discover a security vulnerability within Filament, please email Dan Harrin via [dan@danharrin.com](mailto:dan@danharrin.com). All security vulnerabilities will be promptly addressed. diff --git a/packages/support/src/Commands/CheckTranslationsCommand.php b/packages/support/src/Commands/CheckTranslationsCommand.php index 5b9c248750b..d66ac65dfe0 100644 --- a/packages/support/src/Commands/CheckTranslationsCommand.php +++ b/packages/support/src/Commands/CheckTranslationsCommand.php @@ -21,7 +21,7 @@ #[AsCommand(name: 'filament:check-translations')] class CheckTranslationsCommand extends Command implements PromptsForMissingInput { - protected $description = 'Check for missing and removed translations'; + protected $description = 'Check for missing and removed translations or list all translations'; protected $name = 'filament:check-translations'; @@ -52,12 +52,19 @@ protected function getOptions(): array description: 'The directory containing the translations to check - either \'vendor\' or \'app\'', default: 'vendor', ), + new InputOption( + name: 'list', + shortcut: 'l', + mode: InputOption::VALUE_NONE, + description: 'List translations', + ), ]; } public function handle(): int { $this->scan('filament'); + $this->scan('panels'); $this->scan('actions'); $this->scan('forms'); $this->scan('infolists'); @@ -79,9 +86,11 @@ public function handle(): int protected function scan(string $package): void { $localeRootDirectory = match ($source = $this->option('source')) { - 'app' => lang_path("vendor/{$package}"), + 'app' => $package == 'support' + ? lang_path('vendor/filament') + : lang_path("vendor/filament-{$package}"), 'vendor' => base_path("vendor/filament/{$package}/resources/lang"), - default => throw new InvalidOptionException("{$source} is not a valid translation source. Must be `vendor` or `app`.") + default => throw new InvalidOptionException("{$source} is not a valid translation source. Must be `vendor` or `app`."), }; $filesystem = app(Filesystem::class); @@ -124,53 +133,146 @@ protected function scan(string $package): void ); } - collect($files) + $existingFiles = collect($files) ->reject(function ($file) use ($localeRootDirectory) { return ! file_exists(implode(DIRECTORY_SEPARATOR, [$localeRootDirectory, 'en', $file->getRelativePathname()])); - }) - ->mapWithKeys(function (SplFileInfo $file) use ($localeDir, $localeRootDirectory) { - $expectedKeys = require implode(DIRECTORY_SEPARATOR, [$localeRootDirectory, 'en', $file->getRelativePathname()]); - $actualKeys = require $file->getPathname(); - - return [ - (string) str($file->getPathname())->after("{$localeDir}/") => [ - 'missing' => array_keys(array_diff_key( - Arr::dot($expectedKeys), - Arr::dot($actualKeys) - )), - 'removed' => array_keys(array_diff_key( - Arr::dot($actualKeys), - Arr::dot($expectedKeys) - )), - ], - ]; - }) - ->tap(function (Collection $files) use ($locale, $package) { - $missingKeysCount = $files->sum(fn ($file): int => count($file['missing'])); - $removedKeysCount = $files->sum(fn ($file): int => count($file['removed'])); - - $locale = locale_get_display_name($locale, 'en'); - - if ((! $missingKeysCount) && (! $removedKeysCount)) { - info("[✓] Package filament/{$package} has no missing or removed translation keys for {$locale}!\n"); - } elseif ($missingKeysCount && $removedKeysCount) { - warning("[!] Package filament/{$package} has {$missingKeysCount} missing translation " . Str::plural('key', $missingKeysCount) . " and {$removedKeysCount} removed translation " . Str::plural('key', $removedKeysCount) . " for {$locale}.\n"); - } elseif ($missingKeysCount) { - warning("[!] Package filament/{$package} has {$missingKeysCount} missing translation " . Str::plural('key', $missingKeysCount) . " for {$locale}.\n"); - } elseif ($removedKeysCount) { - warning("[!] Package filament/{$package} has {$removedKeysCount} removed translation " . Str::plural('key', $removedKeysCount) . " for {$locale}.\n"); - } - }) - ->filter(static fn ($keys): bool => count($keys['missing']) || count($keys['removed'])) - ->each(function ($keys, string $file) { - table( - [$file, ''], - [ - ...array_map(fn (string $key): array => [$key, 'Missing'], $keys['missing']), - ...array_map(fn (string $key): array => [$key, 'Removed'], $keys['removed']), - ], - ); }); + if ($this->option('list')) { + $translations = $this->translations($existingFiles, $localeDir, $localeRootDirectory); + $this->displayTranslations($translations, $locale, $package); + } else { + $missings = $this->check($existingFiles, $localeDir, $localeRootDirectory); + $this->displayMissings($missings, $locale, $package); + } + }); + } + + protected function translations(Collection $files, string $localeDir, string $localeRootDirectory): Collection + { + return $files->mapWithKeys(function (SplFileInfo $file) use ($localeDir, $localeRootDirectory) { + $expectedKeys = require implode(DIRECTORY_SEPARATOR, [$localeRootDirectory, 'en', $file->getRelativePathname()]); + $actualKeys = require $file->getPathname(); + $expectedKeysFlat = Arr::dot($expectedKeys); + $actualKeysFlat = Arr::dot($actualKeys); + $translations = collect($expectedKeysFlat) + ->map(function ($expectedKey, $key) use ($actualKeysFlat) { + $translation = $actualKeysFlat[$key] ?? '<<<<< MISSING! >>>>>'; + + return $expectedKey . ' --> ' . $translation; + }) + ->toArray(); + $removedKeys = collect($actualKeysFlat) + ->reject(function ($actualKey, $key) use ($expectedKeysFlat) { + return isset($expectedKeysFlat[$key]); + }) + ->map(function ($actualKey, $key) { + return 'Removed translation key: <<<<< ' . $key . ' >>>>>'; + }) + ->toArray(); + $translations += $removedKeys; + $expectedKeysCount = count($expectedKeysFlat); + $removedKeysCount = count($removedKeys); + $missingKeysCount = $expectedKeysCount - (count($actualKeysFlat) - $removedKeysCount); + + return [ + (string) str($file->getPathname())->after("{$localeDir}/") => [ + 'expected_keys_count' => $expectedKeysCount, + 'missing_keys_count' => $missingKeysCount, + 'removed_keys_count' => $removedKeysCount, + 'translation' => $translations, + ], + ]; + }); + } + + protected function displayTranslations(Collection $missings, string $locale, string $package): void + { + collect($missings) + ->tap(function (Collection $files) use ($locale, $package) { + $expectedKeysCount = $files->sum(fn ($file): int => $file['expected_keys_count']); + $missingKeysCount = $files->sum(fn ($file): int => $file['missing_keys_count']); + $removedKeysCount = $files->sum(fn ($file): int => $file['removed_keys_count']); + + $locale = locale_get_display_name($locale, 'en'); + info("Package filament/{$package} has {$expectedKeysCount} translation " . Str::plural('string', $expectedKeysCount) . " for {$locale}."); + $message = "{$missingKeysCount} missing translation " . Str::plural('string', $missingKeysCount) . '.'; + if ($missingKeysCount) { + warning($message); + } else { + info($message); + } + $message = "{$removedKeysCount} removed translation " . Str::plural('string', $removedKeysCount); + if ($removedKeysCount) { + warning($message); + } else { + info($message); + } + }) + ->each(function ($keys, string $file) { + $counts = [ + 'expected_keys_count' => '- Number of expected keys: ' . $keys['expected_keys_count'], + 'missing_keys_count' => '- Number of missing keys: ' . $keys['missing_keys_count'], + 'removed_keys_count' => '- Number of removed keys: ' . $keys['removed_keys_count'], + ]; + $keys['translation'] += $counts; + table( + [$file], + [ + ...array_map(fn (string $key): array => [$key], $keys['translation']), + ], + ); + }); + } + + protected function check(Collection $files, string $localeDir, string $localeRootDirectory): Collection + { + return $files->mapWithKeys(function (SplFileInfo $file) use ($localeDir, $localeRootDirectory) { + $expectedKeys = require implode(DIRECTORY_SEPARATOR, [$localeRootDirectory, 'en', $file->getRelativePathname()]); + $actualKeys = require $file->getPathname(); + + return [ + (string) str($file->getPathname())->after("{$localeDir}/") => [ + 'missing' => array_keys(array_diff_key( + Arr::dot($expectedKeys), + Arr::dot($actualKeys) + )), + 'removed' => array_keys(array_diff_key( + Arr::dot($actualKeys), + Arr::dot($expectedKeys) + )), + ], + ]; + }); + } + + protected function displayMissings(Collection $missings, string $locale, string $package): void + { + collect($missings) + ->tap(function (Collection $files) use ($locale, $package) { + $missingKeysCount = $files->sum(fn ($file): int => count($file['missing'])); + $removedKeysCount = $files->sum(fn ($file): int => count($file['removed'])); + + $locale = locale_get_display_name($locale, 'en'); + + if ((! $missingKeysCount) && (! $removedKeysCount)) { + info("[✓] Package filament/{$package} has no missing or removed translation keys for {$locale}!\n"); + } elseif ($missingKeysCount && $removedKeysCount) { + warning("[!] Package filament/{$package} has {$missingKeysCount} missing translation " . Str::plural('key', $missingKeysCount) . " and {$removedKeysCount} removed translation " . Str::plural('key', $removedKeysCount) . " for {$locale}.\n"); + } elseif ($missingKeysCount) { + warning("[!] Package filament/{$package} has {$missingKeysCount} missing translation " . Str::plural('key', $missingKeysCount) . " for {$locale}.\n"); + } elseif ($removedKeysCount) { + warning("[!] Package filament/{$package} has {$removedKeysCount} removed translation " . Str::plural('key', $removedKeysCount) . " for {$locale}.\n"); + } + }) + ->filter(static fn ($keys): bool => count($keys['missing']) || count($keys['removed'])) + ->each(function ($keys, string $file) { + table( + [$file, ''], + [ + ...array_map(fn (string $key): array => [$key, 'Missing'], $keys['missing']), + ...array_map(fn (string $key): array => [$key, 'Removed'], $keys['removed']), + ], + ); }); } }