diff --git a/config/translation-linter.php b/config/translation-linter.php index 4813990..e806f01 100644 --- a/config/translation-linter.php +++ b/config/translation-linter.php @@ -4,7 +4,7 @@ 'application' => [ /* |-------------------------------------------------------------------------- - | Code Directories + | Application Directories |-------------------------------------------------------------------------- | | The following array lists the "directories" that will be scanned @@ -19,7 +19,7 @@ /* |-------------------------------------------------------------------------- - | Code Extensions + | Application File Extensions |-------------------------------------------------------------------------- | | The following array lists the file "extensions" that will be scanned for @@ -66,6 +66,25 @@ | */ 'locales' => [env('LOCALE_DEFAULT', 'en')], + + /* + |-------------------------------------------------------------------------- + | Language File Readers + |-------------------------------------------------------------------------- + | + | The following array lists the language file "readers" that will be + | parsed for translation keys. This should be mapped as a key value + | array. The key should be the "extension" and the value should be + | the "Reader" class that implements the require interface. + | + | If you want to disable reading a specific file type then you can + | remove it from the array below. + | + */ + 'readers' => [ + 'json' => \Fidum\LaravelTranslationLinter\Readers\JsonFileReader::class, + 'php' => \Fidum\LaravelTranslationLinter\Readers\PhpFileReader::class, + ], ], 'unused' => [ diff --git a/src/Commands/UnusedCommand.php b/src/Commands/UnusedCommand.php index 58997e6..0ef28ea 100644 --- a/src/Commands/UnusedCommand.php +++ b/src/Commands/UnusedCommand.php @@ -31,12 +31,12 @@ public function handle( } if ($results->isEmpty()) { - $this->comment('No unused translations found!'); + $this->components->info('No unused translations found!'); return self::SUCCESS; } - $this->error(sprintf('%d unused translations found', $results->count())); + $this->components->error(sprintf('%d unused translations found', $results->count())); $this->table($fields->headers(), $results->toCommandTableOutputArray($fields)); return self::FAILURE; diff --git a/src/Contracts/Finders/LanguageFileFinder.php b/src/Contracts/Finders/LanguageFileFinder.php index bd55ddf..9f9b7e9 100644 --- a/src/Contracts/Finders/LanguageFileFinder.php +++ b/src/Contracts/Finders/LanguageFileFinder.php @@ -6,5 +6,5 @@ interface LanguageFileFinder { - public function execute(string $path, array $extensions): Collection; + public function execute(string $path, string $locale): Collection; } diff --git a/src/Finders/LanguageFileFinder.php b/src/Finders/LanguageFileFinder.php index 40e0e75..5fffb95 100644 --- a/src/Finders/LanguageFileFinder.php +++ b/src/Finders/LanguageFileFinder.php @@ -3,17 +3,40 @@ namespace Fidum\LaravelTranslationLinter\Finders; use Fidum\LaravelTranslationLinter\Contracts\Finders\LanguageFileFinder as LanguageFileFinderContract; +use Fidum\LaravelTranslationLinter\Managers\LanguageFileReaderManager; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Collection; +use Symfony\Component\Finder\SplFileInfo; readonly class LanguageFileFinder implements LanguageFileFinderContract { - public function __construct(protected Filesystem $filesystem) {} + public function __construct( + protected Filesystem $filesystem, + protected LanguageFileReaderManager $manager, + ) {} - public function execute(string $path, array $extensions): Collection + public function execute(string $path, string $locale): Collection { - $files = new Collection($this->filesystem->allFiles($path)); + if ($this->filesystem->exists($path)) { + $files = new Collection($this->filesystem->allFiles($path)); + $extensions = $this->manager->getEnabledDrivers(); - return $files->filter(fn (\SplFileInfo $file) => in_array($file->getExtension(), $extensions)); + return $files->filter(function (SplFileInfo $file) use ($extensions, $locale) { + if (in_array($file->getExtension(), $extensions)) { + if ($file->getFilenameWithoutExtension() === $locale) { + return true; + } + + return str_contains( + $file->getPathname(), + DIRECTORY_SEPARATOR.$locale.DIRECTORY_SEPARATOR + ); + } + + return false; + }); + } + + return new Collection(); } } diff --git a/src/LaravelTranslationLinterServiceProvider.php b/src/LaravelTranslationLinterServiceProvider.php index 518eb80..6206c4c 100644 --- a/src/LaravelTranslationLinterServiceProvider.php +++ b/src/LaravelTranslationLinterServiceProvider.php @@ -20,6 +20,7 @@ use Fidum\LaravelTranslationLinter\Finders\LanguageFileFinder; use Fidum\LaravelTranslationLinter\Finders\LanguageNamespaceFinder; use Fidum\LaravelTranslationLinter\Linters\UnusedTranslationLinter; +use Fidum\LaravelTranslationLinter\Managers\LanguageFileReaderManager; use Fidum\LaravelTranslationLinter\Parsers\ApplicationFileParser; use Fidum\LaravelTranslationLinter\Readers\ApplicationFileReader; use Fidum\LaravelTranslationLinter\Readers\LanguageFileReader; @@ -62,6 +63,11 @@ public function registeringPackage() $this->app->bind(LanguageFileReaderContract::class, LanguageFileReader::class); + $this->app->scoped(LanguageFileReaderManager::class, LanguageFileReaderManager::class); + $this->app->when(LanguageFileReaderManager::class) + ->needs('$driverConfig') + ->giveConfig('translation-linter.lang.readers'); + $this->app->bind(LanguageNamespaceFinderContract::class, LanguageNamespaceFinder::class); $this->app->bind(UnusedFieldCollectionContract::class, function (Application $app) { @@ -89,6 +95,7 @@ public function provides() ApplicationFileReaderContract::class, LanguageFileFinderContract::class, LanguageFileReaderContract::class, + LanguageFileReaderManager::class, LanguageNamespaceFinderContract::class, UnusedFieldCollectionContract::class, UnusedFilterCollectionContract::class, diff --git a/src/Linters/UnusedTranslationLinter.php b/src/Linters/UnusedTranslationLinter.php index 6c21f72..8cb309d 100644 --- a/src/Linters/UnusedTranslationLinter.php +++ b/src/Linters/UnusedTranslationLinter.php @@ -35,7 +35,7 @@ public function execute(): Collection foreach ($namespaces as $namespace => $path) { $unused[$locale][$namespace] = []; - $files = $this->files->execute($path, ['php', 'json']); + $files = $this->files->execute($path, $locale); /** @var SplFileInfo $file */ foreach ($files as $file) { @@ -73,8 +73,8 @@ protected function getLanguageKey(SplFileInfo $file, string $language, string $k } return Str::of($file->getPath()) - ->finish('/') - ->after("/$language/") + ->finish(DIRECTORY_SEPARATOR) + ->after(DIRECTORY_SEPARATOR.$language.DIRECTORY_SEPARATOR) ->append($file->getFilenameWithoutExtension()) ->append('.') ->append($key) diff --git a/src/Managers/LanguageFileReaderManager.php b/src/Managers/LanguageFileReaderManager.php new file mode 100644 index 0000000..4ecca70 --- /dev/null +++ b/src/Managers/LanguageFileReaderManager.php @@ -0,0 +1,39 @@ +driverConfig as $driver => $readerClass) { + $this->extend($driver, fn (Container $app) => $app->get($readerClass)); + } + } + + public function getDefaultDriver() + { + return array_key_first($this->customCreators); + } + + public function isEnabled(string $driver) + { + return array_key_exists($driver, $this->customCreators); + } + + public function getEnabledDrivers() + { + return array_keys($this->customCreators); + } +} diff --git a/src/Readers/JsonFileReader.php b/src/Readers/JsonFileReader.php new file mode 100644 index 0000000..876c435 --- /dev/null +++ b/src/Readers/JsonFileReader.php @@ -0,0 +1,21 @@ +getContents(), true); + + if (! is_array($translations)) { + throw new InvalidArgumentException("Unable to extract an array from {$file->getPathname()}!"); + } + + return $translations; + } +} diff --git a/src/Readers/LanguageFileReader.php b/src/Readers/LanguageFileReader.php index 1559473..5bc8b5c 100644 --- a/src/Readers/LanguageFileReader.php +++ b/src/Readers/LanguageFileReader.php @@ -3,20 +3,25 @@ namespace Fidum\LaravelTranslationLinter\Readers; use Fidum\LaravelTranslationLinter\Contracts\Readers\LanguageFileReader as LanguageFileReaderContract; +use Fidum\LaravelTranslationLinter\Managers\LanguageFileReaderManager; use InvalidArgumentException; use Symfony\Component\Finder\SplFileInfo; class LanguageFileReader implements LanguageFileReaderContract { + public function __construct(protected LanguageFileReaderManager $manager) {} + public function execute(SplFileInfo $file): array { - $translations = match ($file->getExtension()) { - 'json' => json_decode($file->getContents(), true), - default => include $file->getPathname(), - }; + $extension = $file->getExtension(); + $translations = []; + + if ($this->manager->isEnabled($extension)) { + $translations = $this->manager->driver($extension)->execute($file); - if (! is_array($translations)) { - throw new InvalidArgumentException("Unable to extract an array from {$file->getPathname()}!"); + if (! $translations) { + throw new InvalidArgumentException("Unable to extract any data from {$file->getPathname()}!"); + } } return $translations; diff --git a/src/Readers/PhpFileReader.php b/src/Readers/PhpFileReader.php new file mode 100644 index 0000000..36f2b1f --- /dev/null +++ b/src/Readers/PhpFileReader.php @@ -0,0 +1,21 @@ +getPathname(); + + if (! is_array($translations)) { + throw new InvalidArgumentException("Unable to extract an array from {$file->getPathname()}!"); + } + + return $translations; + } +} diff --git a/tests/.pest/snapshots/Commands/UnusedCommandTest/it_can_test_with_default_no_fields.snap b/tests/.pest/snapshots/Commands/UnusedCommandTest/it_can_test_with_default_no_fields.snap deleted file mode 100644 index b3507db..0000000 --- a/tests/.pest/snapshots/Commands/UnusedCommandTest/it_can_test_with_default_no_fields.snap +++ /dev/null @@ -1 +0,0 @@ -11 unused translations found diff --git a/tests/.pest/snapshots/Commands/UnusedCommandTest/it_can_test_with_default_filters.snap b/tests/.pest/snapshots/Commands/UnusedCommandTest/it_errors_with_default_filters.snap similarity index 96% rename from tests/.pest/snapshots/Commands/UnusedCommandTest/it_can_test_with_default_filters.snap rename to tests/.pest/snapshots/Commands/UnusedCommandTest/it_errors_with_default_filters.snap index 5da698f..33a7e80 100644 --- a/tests/.pest/snapshots/Commands/UnusedCommandTest/it_can_test_with_default_filters.snap +++ b/tests/.pest/snapshots/Commands/UnusedCommandTest/it_errors_with_default_filters.snap @@ -1,4 +1,6 @@ -11 unused translations found + + ERROR 11 unused translations found. + +--------+-----------+------------------------------------+------------------------------+ | Locale | Namespace | Key | Value | +--------+-----------+------------------------------------+------------------------------+ diff --git a/tests/.pest/snapshots/Commands/UnusedCommandTest/it_errors_with_default_no_fields.snap b/tests/.pest/snapshots/Commands/UnusedCommandTest/it_errors_with_default_no_fields.snap new file mode 100644 index 0000000..d78c1b3 --- /dev/null +++ b/tests/.pest/snapshots/Commands/UnusedCommandTest/it_errors_with_default_no_fields.snap @@ -0,0 +1,3 @@ + + ERROR 11 unused translations found. + diff --git a/tests/.pest/snapshots/Commands/UnusedCommandTest/it_can_test_with_default_no_filters.snap b/tests/.pest/snapshots/Commands/UnusedCommandTest/it_errors_with_default_no_filters.snap similarity index 97% rename from tests/.pest/snapshots/Commands/UnusedCommandTest/it_can_test_with_default_no_filters.snap rename to tests/.pest/snapshots/Commands/UnusedCommandTest/it_errors_with_default_no_filters.snap index 974b0c9..17c566f 100644 --- a/tests/.pest/snapshots/Commands/UnusedCommandTest/it_can_test_with_default_no_filters.snap +++ b/tests/.pest/snapshots/Commands/UnusedCommandTest/it_errors_with_default_no_filters.snap @@ -1,4 +1,6 @@ -19 unused translations found + + ERROR 19 unused translations found. + +--------+-----------+------------------------------------+------------------------------+ | Locale | Namespace | Key | Value | +--------+-----------+------------------------------------+------------------------------+ diff --git a/tests/.pest/snapshots/Commands/UnusedCommandTest/it_can_test_with_default_restricted_fields.snap b/tests/.pest/snapshots/Commands/UnusedCommandTest/it_errors_with_default_restricted_fields.snap similarity index 94% rename from tests/.pest/snapshots/Commands/UnusedCommandTest/it_can_test_with_default_restricted_fields.snap rename to tests/.pest/snapshots/Commands/UnusedCommandTest/it_errors_with_default_restricted_fields.snap index a42b459..a5409b4 100644 --- a/tests/.pest/snapshots/Commands/UnusedCommandTest/it_can_test_with_default_restricted_fields.snap +++ b/tests/.pest/snapshots/Commands/UnusedCommandTest/it_errors_with_default_restricted_fields.snap @@ -1,4 +1,6 @@ -11 unused translations found + + ERROR 11 unused translations found. + +--------+------------------------------------+ | Locale | Key | +--------+------------------------------------+ diff --git a/tests/.pest/snapshots/Commands/UnusedCommandTest/it_errors_with_multiple_locales.snap b/tests/.pest/snapshots/Commands/UnusedCommandTest/it_errors_with_multiple_locales.snap new file mode 100644 index 0000000..ffb1295 --- /dev/null +++ b/tests/.pest/snapshots/Commands/UnusedCommandTest/it_errors_with_multiple_locales.snap @@ -0,0 +1,29 @@ + + ERROR 22 unused translations found. + ++--------+-----------+------------------------------------+----------------------------------------------------+ +| Locale | Namespace | Key | Value | ++--------+-----------+------------------------------------+----------------------------------------------------+ +| en | | Unused PHP Class | I am unused in php class | +| en | | Unused Blade File | I am unused in blade | +| en | | Unused Vue Component | I am unused in vue component | +| en | | example.unused | I am unused in php class | +| en | | example.blade.choice.unused | I am unused in blade | +| en | | example.blade.lang.unused | I am unused in blade | +| en | | example.vue.unused | I am unused in vue component | +| en | | folder/example.unused | I am unused in php class | +| en | | folder/example.blade.choice.unused | I am unused in blade | +| en | | folder/example.blade.lang.unused | I am unused in blade | +| en | | folder/example.vue.unused | I am unused in vue component | +| de | | Unused PHP Class | Ich werde in einer PHP-Klasse nicht verwendet | +| de | | Unused Blade File | Ich werde in Blade nicht verwendet | +| de | | Unused Vue Component | Ich werde in einem Vue-Komponenten nicht verwendet | +| de | | example.unused | Ich werde in einer PHP-Klasse nicht verwendet | +| de | | example.blade.choice.unused | Ich werde in Blade nicht verwendet | +| de | | example.blade.lang.unused | Ich werde in Blade nicht verwendet | +| de | | example.vue.unused | Ich werde in einem Vue-Komponenten nicht verwendet | +| de | | folder/example.unused | Ich werde in einer PHP-Klasse nicht verwendet | +| de | | folder/example.blade.choice.unused | Ich werde in Blade nicht verwendet | +| de | | folder/example.blade.lang.unused | Ich werde in Blade nicht verwendet | +| de | | folder/example.vue.unused | Ich werde in einem Vue-Komponenten nicht verwendet | ++--------+-----------+------------------------------------+----------------------------------------------------+ diff --git a/tests/.pest/snapshots/Commands/UnusedCommandTest/it_outputs_success_message_when_no_unused_translations_found.snap b/tests/.pest/snapshots/Commands/UnusedCommandTest/it_outputs_success_message_when_no_unused_translations_found.snap new file mode 100644 index 0000000..8178137 --- /dev/null +++ b/tests/.pest/snapshots/Commands/UnusedCommandTest/it_outputs_success_message_when_no_unused_translations_found.snap @@ -0,0 +1,3 @@ + + INFO No unused translations found! + diff --git a/tests/Commands/UnusedCommandTest.php b/tests/Commands/UnusedCommandTest.php index 215b261..bf69870 100644 --- a/tests/Commands/UnusedCommandTest.php +++ b/tests/Commands/UnusedCommandTest.php @@ -5,39 +5,61 @@ use function Pest\Laravel\artisan; use function Pest\Laravel\withoutMockingConsoleOutput; -it('can test with default filters', function () { +it('errors with default filters', function () { withoutMockingConsoleOutput(); - artisan('translation:unused'); - - expect(Artisan::output())->toMatchSnapshot(); + expect(artisan('translation:unused')) + ->toBe(1) + ->and(Artisan::output()) + ->toMatchSnapshot(); }); -it('can test with default no filters', function () { +it('errors with default no filters', function () { config()->set('translation-linter.unused.filters', []); withoutMockingConsoleOutput(); - artisan('translation:unused'); - - expect(Artisan::output())->toMatchSnapshot(); + expect(artisan('translation:unused')) + ->toBe(1) + ->and(Artisan::output()) + ->toMatchSnapshot(); }); -it('can test with default restricted fields', function () { +it('errors with default restricted fields', function () { config()->set('translation-linter.unused.fields.namespace', false); config()->set('translation-linter.unused.fields.value', false); withoutMockingConsoleOutput(); - artisan('translation:unused'); - - expect(Artisan::output())->toMatchSnapshot(); + expect(artisan('translation:unused')) + ->toBe(1) + ->and(Artisan::output()) + ->toMatchSnapshot(); }); -it('can test with default no fields', function () { +it('errors with default no fields', function () { config()->set('translation-linter.unused.fields.locale', false); config()->set('translation-linter.unused.fields.namespace', false); config()->set('translation-linter.unused.fields.key', false); config()->set('translation-linter.unused.fields.value', false); withoutMockingConsoleOutput(); - artisan('translation:unused'); + expect(artisan('translation:unused')) + ->toBe(1) + ->and(Artisan::output()) + ->toMatchSnapshot(); +}); + +it('errors with multiple locales', function () { + config()->set('translation-linter.lang.locales', ['en', 'de']); + withoutMockingConsoleOutput(); + expect(artisan('translation:unused')) + ->toBe(1) + ->and(Artisan::output()) + ->toMatchSnapshot(); +}); - expect(Artisan::output())->toMatchSnapshot(); +it('outputs success message when no unused translations found', function () { + config()->set('translation-linter.lang.locales', []); + withoutMockingConsoleOutput(); + expect(artisan('translation:unused')) + ->toBe(0) + ->and(Artisan::output()) + ->toMatchSnapshot(); }); diff --git a/workbench/lang/de.json b/workbench/lang/de.json new file mode 100644 index 0000000..9a664b8 --- /dev/null +++ b/workbench/lang/de.json @@ -0,0 +1,8 @@ +{ + "Used PHP Class": "Ich werde in einer PHP-Klasse verwendet", + "Unused PHP Class": "Ich werde in einer PHP-Klasse nicht verwendet", + "Used Blade File": "Ich werde in Blade verwendet", + "Unused Blade File": "Ich werde in Blade nicht verwendet", + "Used Vue Component": "Ich werde in einem Vue-Komponenten verwendet", + "Unused Vue Component": "Ich werde in einem Vue-Komponenten nicht verwendet" +} diff --git a/workbench/lang/de/example.php b/workbench/lang/de/example.php new file mode 100644 index 0000000..644845b --- /dev/null +++ b/workbench/lang/de/example.php @@ -0,0 +1,20 @@ + 'Ich werde in einer PHP-Klasse verwendet', + 'unused' => 'Ich werde in einer PHP-Klasse nicht verwendet', + 'blade' => [ + 'choice' => [ + 'used' => 'Ich werde in Blade verwendet', + 'unused' => 'Ich werde in Blade nicht verwendet', + ], + 'lang' => [ + 'used' => 'Ich werde in Blade verwendet', + 'unused' => 'Ich werde in Blade nicht verwendet', + ], + ], + 'vue' => [ + 'used' => 'Ich werde in einem Vue-Komponenten verwendet', + 'unused' => 'Ich werde in einem Vue-Komponenten nicht verwendet', + ], +]; diff --git a/workbench/lang/de/folder/example.php b/workbench/lang/de/folder/example.php new file mode 100644 index 0000000..644845b --- /dev/null +++ b/workbench/lang/de/folder/example.php @@ -0,0 +1,20 @@ + 'Ich werde in einer PHP-Klasse verwendet', + 'unused' => 'Ich werde in einer PHP-Klasse nicht verwendet', + 'blade' => [ + 'choice' => [ + 'used' => 'Ich werde in Blade verwendet', + 'unused' => 'Ich werde in Blade nicht verwendet', + ], + 'lang' => [ + 'used' => 'Ich werde in Blade verwendet', + 'unused' => 'Ich werde in Blade nicht verwendet', + ], + ], + 'vue' => [ + 'used' => 'Ich werde in einem Vue-Komponenten verwendet', + 'unused' => 'Ich werde in einem Vue-Komponenten nicht verwendet', + ], +];