diff --git a/README.md b/README.md
index e588023..acd6bb3 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,38 @@ php artisan vendor:publish --tag="translation-linter-config"
You should read through the config, which serves as additional documentation and make changes as needed.
+## Missing Command
+This reads through all your code and finds all your language function usage.
+Then attempts to find matches in your language files and will output any
+keys in your code that do not exist as a language key.
+
+```sh
+$ php artisan translation:missing
+
+ ERROR 3 missing translations found.
+
++--------+--------------------------------+---------------------+
+| Locale | Key | File |
++--------+--------------------------------+---------------------+
+| en | Missing PHP Class | app/ExampleJson.php |
+| en | Only Missing English PHP Class | app/ExampleJson.php |
+| de | Missing PHP Class | app/ExampleJson.php |
++--------+--------------------------------+---------------------+
+```
+
+You can generate a baseline file which will be used to ignore specific keys with the
+`--generate-baseline` or `-b` command options:
+
+```sh
+$ php artisan translation:missing --generate-baseline
+
+ INFO Baseline file written with 49 translation keys.
+
+$ php artisan translation:missing
+
+ INFO No missing translations found!
+```
+
## Unused Command
This reads through all your code and finds all your language function usage.
Then attempts to find matches in your language files and will output any
@@ -65,20 +97,6 @@ $ php artisan translation:unused
INFO No unused translations found!
```
-## Roadmap
-- [x] Supports JSON and PHP translation files
- - You can enable / disable file types in the config
- - You can add your own custom file readers
-- [x] Supports multiple locales
-- [x] Supports parsing many code types
- - Default: php, js and vue
- - You can add more file extensions in the config
-- [x] [Unused Command](#unused-command)
-- [ ] Missing Command - _coming soon_
-- [ ] Orphaned Command - _coming soon_
-- [ ] Lint Command - _coming soon_
- - This would run all of the other commands in a single command.
-
## Testing
```bash
diff --git a/config/translation-linter.php b/config/translation-linter.php
index 6e2049c..b7203b8 100644
--- a/config/translation-linter.php
+++ b/config/translation-linter.php
@@ -88,6 +88,50 @@
],
],
+ 'missing' => [
+ /*
+ |--------------------------------------------------------------------------
+ | Baseline File
+ |--------------------------------------------------------------------------
+ |
+ | This is the location of the baseline file that is used to ignore specific
+ | translation keys. You can generate this file by using the `--generate-baseline`
+ | option when running the command. You should commit this file.
+ |
+ */
+ 'baseline' => lang_path('.lint/missing.json'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Output Fields
+ |--------------------------------------------------------------------------
+ |
+ | The following array lists the "fields" that are displayed by the command
+ | when missing translations are found. Set any of these to `false` to hide
+ | them from the output or change all to `false` to not show anything.
+ |
+ */
+ 'fields' => [
+ 'locale' => true,
+ 'key' => true,
+ 'file' => true,
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Missing Language Filters
+ |--------------------------------------------------------------------------
+ |
+ | The following array lists the "filters" that will be used to filter out
+ | erroneously detected missing translations.
+ |
+ | All filters must implement the filter interface or they will be skipped:
+ | \Fidum\LaravelTranslationLinter\Contracts\Filter
+ |
+ */
+ 'filters' => [],
+ ],
+
'unused' => [
/*
|--------------------------------------------------------------------------
diff --git a/src/Collections/ApplicationFileCollection.php b/src/Collections/ApplicationFileCollection.php
new file mode 100644
index 0000000..4487faa
--- /dev/null
+++ b/src/Collections/ApplicationFileCollection.php
@@ -0,0 +1,26 @@
+some(function (ApplicationFileObject $object) use ($key) {
+ return $object->namespaceHintedKey === $key;
+ });
+ }
+
+ public function doesntContainKey(string $key): bool
+ {
+ return ! $this->containsKey($key);
+ }
+}
diff --git a/src/Collections/Concerns/CollectsFields.php b/src/Collections/Concerns/CollectsFields.php
new file mode 100644
index 0000000..2da7900
--- /dev/null
+++ b/src/Collections/Concerns/CollectsFields.php
@@ -0,0 +1,21 @@
+filter()->keys();
+ }
+
+ public function headers(): array
+ {
+ return $this->enabled()
+ ->map(fn ($v) => Str::headline($v))
+ ->toArray();
+ }
+}
diff --git a/src/Collections/Concerns/CollectsFilters.php b/src/Collections/Concerns/CollectsFilters.php
new file mode 100644
index 0000000..2ce0b2a
--- /dev/null
+++ b/src/Collections/Concerns/CollectsFilters.php
@@ -0,0 +1,26 @@
+every(function (string $filterClass) use ($object) {
+ $interface = Filter::class;
+
+ if (is_subclass_of($filterClass, $interface)) {
+ /** @var Filter $filter */
+ $filter = app($filterClass);
+
+ return $filter->shouldReport($object);
+ }
+
+ throw new InvalidArgumentException("Filter [$filterClass] needs to implement [$interface].");
+ });
+ }
+}
diff --git a/src/Collections/MissingFieldCollection.php b/src/Collections/MissingFieldCollection.php
new file mode 100644
index 0000000..29f5ace
--- /dev/null
+++ b/src/Collections/MissingFieldCollection.php
@@ -0,0 +1,12 @@
+filter()->keys();
- }
-
- public function headers(): array
- {
- return $this->enabled()
- ->map(fn ($v) => Str::headline($v))
- ->toArray();
- }
+ use CollectsFields;
}
diff --git a/src/Collections/UnusedFilterCollection.php b/src/Collections/UnusedFilterCollection.php
index f3fde61..db8fb67 100644
--- a/src/Collections/UnusedFilterCollection.php
+++ b/src/Collections/UnusedFilterCollection.php
@@ -2,27 +2,11 @@
namespace Fidum\LaravelTranslationLinter\Collections;
+use Fidum\LaravelTranslationLinter\Collections\Concerns\CollectsFilters;
use Fidum\LaravelTranslationLinter\Contracts\Collections\UnusedFilterCollection as UnusedFilterCollectionContract;
-use Fidum\LaravelTranslationLinter\Contracts\Filters\Filter;
-use Fidum\LaravelTranslationLinter\Data\ResultObject;
-use http\Exception\InvalidArgumentException;
use Illuminate\Support\Collection;
class UnusedFilterCollection extends Collection implements UnusedFilterCollectionContract
{
- public function shouldReport(ResultObject $object): bool
- {
- return $this->every(function (string $filterClass) use ($object) {
- $interface = Filter::class;
-
- if (is_subclass_of($filterClass, $interface)) {
- /** @var Filter $filter */
- $filter = app($filterClass);
-
- return $filter->shouldReport($object);
- }
-
- throw new InvalidArgumentException("Filter [$filterClass] needs to implement [$interface].");
- });
- }
+ use CollectsFilters;
}
diff --git a/src/Commands/MissingCommand.php b/src/Commands/MissingCommand.php
new file mode 100644
index 0000000..54d90b9
--- /dev/null
+++ b/src/Commands/MissingCommand.php
@@ -0,0 +1,60 @@
+option('generate-baseline');
+ $results = $linter->execute();
+
+ if ($baseline) {
+ $results = $results->whereShouldReport($filters);
+
+ $writer->execute($results);
+
+ $this->components->info("Baseline file written with {$results->count()} translation keys.");
+
+ return self::SUCCESS;
+ }
+
+ $filters->push(IgnoreKeysFromMissingBaselineFileFilter::class);
+
+ $results = $results
+ ->when($this->argument('paths'), function (ResultObjectCollection $items, array $files) {
+ return $items->filter(fn (ResultObject $object) => in_array($object->file->getPathname(), $files));
+ })
+ ->whereShouldReport($filters);
+
+ if ($results->isEmpty()) {
+ $this->components->info('No missing translations found!');
+
+ return self::SUCCESS;
+ }
+
+ $this->components->error(sprintf('%d missing translations found', $results->count()));
+ $this->table($fields->headers(), $results->toCommandTableOutputArray($fields));
+
+ return self::FAILURE;
+ }
+}
diff --git a/src/Contracts/Collections/ApplicationFileCollection.php b/src/Contracts/Collections/ApplicationFileCollection.php
new file mode 100644
index 0000000..2bdaa0e
--- /dev/null
+++ b/src/Contracts/Collections/ApplicationFileCollection.php
@@ -0,0 +1,18 @@
+ $this->locale,
'key' => $this->namespaceHintedKey,
'value' => $this->value,
+ 'file' => str($this->file->getPathname())
+ ->replace(base_path(), '')
+ ->ltrim(DIRECTORY_SEPARATOR)
+ ->toString(),
];
}
}
diff --git a/src/Filters/IgnoreKeysFromMissingBaselineFileFilter.php b/src/Filters/IgnoreKeysFromMissingBaselineFileFilter.php
new file mode 100644
index 0000000..733644e
--- /dev/null
+++ b/src/Filters/IgnoreKeysFromMissingBaselineFileFilter.php
@@ -0,0 +1,19 @@
+reader
+ ->execute()
+ ->shouldReport($object->locale, $object->namespaceHintedKey);
+ }
+}
diff --git a/src/LaravelTranslationLinterServiceProvider.php b/src/LaravelTranslationLinterServiceProvider.php
index 95faaf4..d527638 100644
--- a/src/LaravelTranslationLinterServiceProvider.php
+++ b/src/LaravelTranslationLinterServiceProvider.php
@@ -2,10 +2,17 @@
namespace Fidum\LaravelTranslationLinter;
+use Fidum\LaravelTranslationLinter\Collections\ApplicationFileCollection;
+use Fidum\LaravelTranslationLinter\Collections\MissingFieldCollection;
+use Fidum\LaravelTranslationLinter\Collections\MissingFilterCollection;
use Fidum\LaravelTranslationLinter\Collections\ResultObjectCollection;
use Fidum\LaravelTranslationLinter\Collections\UnusedFieldCollection;
use Fidum\LaravelTranslationLinter\Collections\UnusedFilterCollection;
+use Fidum\LaravelTranslationLinter\Commands\MissingCommand;
use Fidum\LaravelTranslationLinter\Commands\UnusedCommand;
+use Fidum\LaravelTranslationLinter\Contracts\Collections\ApplicationFileCollection as ApplicationFileCollectionContract;
+use Fidum\LaravelTranslationLinter\Contracts\Collections\MissingFieldCollection as MissingFieldCollectionContract;
+use Fidum\LaravelTranslationLinter\Contracts\Collections\MissingFilterCollection as MissingFilterCollectionContract;
use Fidum\LaravelTranslationLinter\Contracts\Collections\ResultObjectCollection as ResultObjectCollectionContract;
use Fidum\LaravelTranslationLinter\Contracts\Collections\UnusedFieldCollection as UnusedFieldCollectionContract;
use Fidum\LaravelTranslationLinter\Contracts\Collections\UnusedFilterCollection as UnusedFilterCollectionContract;
@@ -14,23 +21,29 @@
use Fidum\LaravelTranslationLinter\Contracts\Finders\ApplicationFileFinder as ApplicationFileFinderContract;
use Fidum\LaravelTranslationLinter\Contracts\Finders\LanguageFileFinder as LanguageFileFinderContract;
use Fidum\LaravelTranslationLinter\Contracts\Finders\LanguageNamespaceFinder as LanguageNamespaceFinderContract;
+use Fidum\LaravelTranslationLinter\Contracts\Linters\MissingTranslationLinter as MissingTranslationLinterContract;
use Fidum\LaravelTranslationLinter\Contracts\Linters\UnusedTranslationLinter as UnusedTranslationLinterContract;
use Fidum\LaravelTranslationLinter\Contracts\Parsers\ApplicationFileParser as ApplicationFileParserContract;
use Fidum\LaravelTranslationLinter\Contracts\Readers\ApplicationFileReader as ApplicationFileReaderContract;
use Fidum\LaravelTranslationLinter\Contracts\Readers\LanguageFileReader as LanguageFileReaderContract;
+use Fidum\LaravelTranslationLinter\Contracts\Readers\MissingBaselineFileReader as MissingBaselineFileReaderContract;
use Fidum\LaravelTranslationLinter\Contracts\Readers\UnusedBaselineFileReader as UnusedBaselineFileReaderContract;
+use Fidum\LaravelTranslationLinter\Contracts\Writers\MissingBaselineFileWriter as MissingBaselineFileWriterContract;
use Fidum\LaravelTranslationLinter\Contracts\Writers\UnusedBaselineFileWriter as UnusedBaselineFileWriterContract;
use Fidum\LaravelTranslationLinter\Factories\LanguageKeyFactory;
use Fidum\LaravelTranslationLinter\Factories\LanguageNamespaceKeyFactory;
use Fidum\LaravelTranslationLinter\Finders\ApplicationFileFinder;
use Fidum\LaravelTranslationLinter\Finders\LanguageFileFinder;
use Fidum\LaravelTranslationLinter\Finders\LanguageNamespaceFinder;
+use Fidum\LaravelTranslationLinter\Linters\MissingTranslationLinter;
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;
+use Fidum\LaravelTranslationLinter\Readers\MissingBaselineFileReader;
use Fidum\LaravelTranslationLinter\Readers\UnusedBaselineFileReader;
+use Fidum\LaravelTranslationLinter\Writers\MissingBaselineFileWriter;
use Fidum\LaravelTranslationLinter\Writers\UnusedBaselineFileWriter;
use Illuminate\Contracts\Support\DeferrableProvider;
use Illuminate\Foundation\Application;
@@ -44,11 +57,14 @@ public function configurePackage(Package $package): void
$package
->name('laravel-translation-linter')
->hasConfigFile()
+ ->hasCommand(MissingCommand::class)
->hasCommand(UnusedCommand::class);
}
public function registeringPackage()
{
+ $this->app->bind(ApplicationFileCollectionContract::class, ApplicationFileCollection::class);
+
$this->app->bind(ApplicationFileFinderContract::class, ApplicationFileFinder::class);
$this->app->when(ApplicationFileFinder::class)
@@ -86,6 +102,32 @@ public function registeringPackage()
$this->app->bind(LanguageNamespaceKeyFactoryContract::class, LanguageNamespaceKeyFactory::class);
+ $this->app->scoped(MissingBaselineFileReaderContract::class, MissingBaselineFileReader::class);
+
+ $this->app->when(MissingBaselineFileReader::class)
+ ->needs('$file')
+ ->giveConfig('translation-linter.missing.baseline');
+
+ $this->app->bind(MissingBaselineFileWriterContract::class, MissingBaselineFileWriter::class);
+
+ $this->app->when(MissingBaselineFileWriter::class)
+ ->needs('$file')
+ ->giveConfig('translation-linter.missing.baseline');
+
+ $this->app->bind(MissingFieldCollectionContract::class, function (Application $app) {
+ return MissingFieldCollection::wrap($app->make('config')->get('translation-linter.missing.fields'));
+ });
+
+ $this->app->bind(MissingFilterCollectionContract::class, function (Application $app) {
+ return MissingFilterCollection::wrap($app->make('config')->get('translation-linter.missing.filters'));
+ });
+
+ $this->app->bind(MissingTranslationLinterContract::class, MissingTranslationLinter::class);
+
+ $this->app->when(MissingTranslationLinter::class)
+ ->needs('$locales')
+ ->giveConfig('translation-linter.lang.locales');
+
$this->app->bind(ResultObjectCollectionContract::class, ResultObjectCollection::class);
$this->app->scoped(UnusedBaselineFileReaderContract::class, UnusedBaselineFileReader::class);
@@ -118,6 +160,7 @@ public function registeringPackage()
public function provides()
{
return [
+ ApplicationFileCollectionContract::class,
ApplicationFileFinderContract::class,
ApplicationFileParserContract::class,
ApplicationFileReaderContract::class,
@@ -127,6 +170,11 @@ public function provides()
LanguageKeyFactoryContract::class,
LanguageNamespaceFinderContract::class,
LanguageNamespaceKeyFactoryContract::class,
+ MissingBaselineFileReaderContract::class,
+ MissingBaselineFileWriterContract::class,
+ MissingFieldCollectionContract::class,
+ MissingFilterCollectionContract::class,
+ MissingTranslationLinterContract::class,
ResultObjectCollectionContract::class,
UnusedBaselineFileReaderContract::class,
UnusedBaselineFileWriterContract::class,
diff --git a/src/Linters/MissingTranslationLinter.php b/src/Linters/MissingTranslationLinter.php
new file mode 100644
index 0000000..2875f34
--- /dev/null
+++ b/src/Linters/MissingTranslationLinter.php
@@ -0,0 +1,45 @@
+results->reset();
+ $used = $this->used->execute();
+
+ foreach ($this->locales as $locale) {
+ /** @var ApplicationFileObject $object */
+ foreach ($used as $object) {
+ if ($this->translator->hasForLocale($object->namespaceHintedKey, $locale)) {
+ continue;
+ }
+
+ $this->results->push(new ResultObject(
+ file: $object->file,
+ key: $object->key,
+ locale: $locale,
+ namespaceHint: $object->namespaceHint,
+ namespaceHintedKey: $object->namespaceHintedKey,
+ ));
+ }
+ }
+
+ return $this->results;
+ }
+}
diff --git a/src/Linters/UnusedTranslationLinter.php b/src/Linters/UnusedTranslationLinter.php
index b53d2d4..40872b0 100644
--- a/src/Linters/UnusedTranslationLinter.php
+++ b/src/Linters/UnusedTranslationLinter.php
@@ -62,7 +62,7 @@ public function execute(): ResultObjectCollection
key: $groupedKey
);
- if ($used->doesntContain($namespacedKey)) {
+ if ($used->doesntContainKey($namespacedKey)) {
$this->results->push(new ResultObject(
file: $file,
key: $groupedKey,
diff --git a/src/Parsers/ApplicationFileParser.php b/src/Parsers/ApplicationFileParser.php
index 316b059..9ebd0ee 100644
--- a/src/Parsers/ApplicationFileParser.php
+++ b/src/Parsers/ApplicationFileParser.php
@@ -2,8 +2,10 @@
namespace Fidum\LaravelTranslationLinter\Parsers;
+use Fidum\LaravelTranslationLinter\Contracts\Collections\ApplicationFileCollection as ApplicationFileCollectionContract;
use Fidum\LaravelTranslationLinter\Contracts\Parsers\ApplicationFileParser as ApplicationFileParserContract;
-use Illuminate\Support\Collection;
+use Fidum\LaravelTranslationLinter\Data\ApplicationFileObject;
+use Illuminate\Support\Str;
use Symfony\Component\Finder\SplFileInfo;
readonly class ApplicationFileParser implements ApplicationFileParserContract
@@ -12,29 +14,38 @@
protected string $pattern;
- public function __construct(array $functions)
- {
+ public function __construct(
+ protected ApplicationFileCollectionContract $collection,
+ array $functions
+ ) {
$this->pattern = str_replace('[FUNCTIONS]', implode('|', $functions), static::REGEX);
}
- public function execute(SplFileInfo $file): Collection
+ public function execute(SplFileInfo $file): ApplicationFileCollectionContract
{
- $strings = new Collection();
-
$data = $file->getContents();
if (! preg_match_all($this->pattern, $data, $matches, PREG_OFFSET_CAPTURE)) {
// If pattern not found return
- return $strings;
+ return $this->collection;
}
foreach (current($matches) as $match) {
preg_match($this->pattern, $match[0], $string);
- $strings->push($string[2]);
+ $namespaceHintedKey = $string[2];
+
+ $this->collection->push(new ApplicationFileObject(
+ file: $file,
+ key: Str::after($namespaceHintedKey, '::') ?: null,
+ namespaceHint: Str::before($namespaceHintedKey, '::') ?: null,
+ namespaceHintedKey: $namespaceHintedKey,
+ ));
}
// Remove duplicates.
- return $strings->unique();
+ return $this->collection->unique(function (ApplicationFileObject $object) {
+ return $object->namespaceHintedKey;
+ });
}
}
diff --git a/src/Readers/ApplicationFileReader.php b/src/Readers/ApplicationFileReader.php
index c5301ab..039a2d2 100644
--- a/src/Readers/ApplicationFileReader.php
+++ b/src/Readers/ApplicationFileReader.php
@@ -2,30 +2,32 @@
namespace Fidum\LaravelTranslationLinter\Readers;
+use Fidum\LaravelTranslationLinter\Contracts\Collections\ApplicationFileCollection as ApplicationFileCollectionContract;
use Fidum\LaravelTranslationLinter\Contracts\Finders\ApplicationFileFinder;
use Fidum\LaravelTranslationLinter\Contracts\Parsers\ApplicationFileParser;
use Fidum\LaravelTranslationLinter\Contracts\Readers\ApplicationFileReader as ApplicationFileReaderContract;
-use Illuminate\Support\Collection;
+use Fidum\LaravelTranslationLinter\Data\ApplicationFileObject;
class ApplicationFileReader implements ApplicationFileReaderContract
{
public function __construct(
+ protected ApplicationFileCollectionContract $collection,
protected ApplicationFileFinder $finder,
protected ApplicationFileParser $parser,
) {}
- public function execute(): Collection
+ public function execute(): ApplicationFileCollectionContract
{
- $strings = new Collection();
-
// List files
$files = $this->finder->execute();
// Get all translatable strings from files
foreach ($files as $file) {
- $strings = $strings->merge($this->parser->execute($file));
+ $this->collection->push(...$this->parser->execute($file));
}
- return $strings->unique();
+ return $this->collection->unique(function (ApplicationFileObject $object) {
+ return $object->namespaceHintedKey.$object->file->getPathname();
+ });
}
}
diff --git a/src/Readers/Concerns/ReadsBaselineFile.php b/src/Readers/Concerns/ReadsBaselineFile.php
new file mode 100644
index 0000000..39e7847
--- /dev/null
+++ b/src/Readers/Concerns/ReadsBaselineFile.php
@@ -0,0 +1,25 @@
+decoded) {
+ return BaselineCollection::wrap($this->decoded);
+ }
+
+ if ($this->filesystem->exists($this->file)) {
+ $contents = $this->filesystem->get($this->file);
+
+ $this->decoded = json_decode($contents, true);
+ }
+
+ return BaselineCollection::wrap($this->decoded);
+ }
+}
diff --git a/src/Readers/MissingBaselineFileReader.php b/src/Readers/MissingBaselineFileReader.php
new file mode 100644
index 0000000..1773055
--- /dev/null
+++ b/src/Readers/MissingBaselineFileReader.php
@@ -0,0 +1,17 @@
+decoded) {
- return BaselineCollection::wrap($this->decoded);
- }
-
- if ($this->filesystem->exists($this->file)) {
- $contents = $this->filesystem->get($this->file);
-
- $this->decoded = json_decode($contents, true);
- }
-
- return BaselineCollection::wrap($this->decoded);
- }
}
diff --git a/src/Writers/Concerns/WritesBaselineFile.php b/src/Writers/Concerns/WritesBaselineFile.php
new file mode 100644
index 0000000..0592c46
--- /dev/null
+++ b/src/Writers/Concerns/WritesBaselineFile.php
@@ -0,0 +1,17 @@
+filesystem->dirname($this->file);
+
+ $this->filesystem->ensureDirectoryExists($path);
+
+ $this->filesystem->put($this->file, $results->toBaselineJson());
+ }
+}
diff --git a/src/Writers/MissingBaselineFileWriter.php b/src/Writers/MissingBaselineFileWriter.php
new file mode 100644
index 0000000..fee86e7
--- /dev/null
+++ b/src/Writers/MissingBaselineFileWriter.php
@@ -0,0 +1,17 @@
+filesystem->dirname($this->file);
-
- $this->filesystem->ensureDirectoryExists($path);
-
- $this->filesystem->put($this->file, $results->toBaselineJson());
- }
}
diff --git a/tests/.pest/snapshots/Commands/MissingCommandTest/it_errors_with_default_config.snap b/tests/.pest/snapshots/Commands/MissingCommandTest/it_errors_with_default_config.snap
new file mode 100644
index 0000000..6ac3a1b
--- /dev/null
+++ b/tests/.pest/snapshots/Commands/MissingCommandTest/it_errors_with_default_config.snap
@@ -0,0 +1,28 @@
+
+ ERROR 21 missing translations found.
+
++--------+----------------------------------------------+-----------------------------------+
+| Locale | Key | File |
++--------+----------------------------------------------+-----------------------------------+
+| en | example.missing | app/Example.php |
+| en | Missing PHP Class | app/ExampleJson.php |
+| en | Only Missing English PHP Class | app/ExampleJson.php |
+| en | example::folder/example.missing | app/ExampleMissing.php |
+| en | folder/example.missing | app/ExampleMissingOther.php |
+| en | example::example.missing | app/ExampleMissingOther.php |
+| en | example.vue.missing | resources/js/MissingComponent.vue |
+| en | folder/example.vue.missing | resources/js/MissingComponent.vue |
+| en | example::example.vue.missing | resources/js/MissingComponent.vue |
+| en | example::folder/example.vue.missing | resources/js/MissingComponent.vue |
+| en | Missing Vue Component | resources/js/MissingComponent.vue |
+| en | Missing Vendor Vue Component | resources/js/MissingComponent.vue |
+| en | example.blade.lang.missing | resources/views/missing.blade.php |
+| en | folder/example.blade.lang.missing | resources/views/missing.blade.php |
+| en | example::example.blade.lang.missing | resources/views/missing.blade.php |
+| en | example::folder/example.blade.lang.missing | resources/views/missing.blade.php |
+| en | Missing Blade File | resources/views/missing.blade.php |
+| en | example.blade.choice.missing | resources/views/missing.blade.php |
+| en | folder/example.blade.choice.missing | resources/views/missing.blade.php |
+| en | example::example.blade.choice.missing | resources/views/missing.blade.php |
+| en | example::folder/example.blade.choice.missing | resources/views/missing.blade.php |
++--------+----------------------------------------------+-----------------------------------+
diff --git a/tests/.pest/snapshots/Commands/MissingCommandTest/it_errors_with_default_no_fields.snap b/tests/.pest/snapshots/Commands/MissingCommandTest/it_errors_with_default_no_fields.snap
new file mode 100644
index 0000000..87afbe2
--- /dev/null
+++ b/tests/.pest/snapshots/Commands/MissingCommandTest/it_errors_with_default_no_fields.snap
@@ -0,0 +1,3 @@
+
+ ERROR 21 missing translations found.
+
diff --git a/tests/.pest/snapshots/Commands/MissingCommandTest/it_errors_with_different_fields.snap b/tests/.pest/snapshots/Commands/MissingCommandTest/it_errors_with_different_fields.snap
new file mode 100644
index 0000000..cc9316e
--- /dev/null
+++ b/tests/.pest/snapshots/Commands/MissingCommandTest/it_errors_with_different_fields.snap
@@ -0,0 +1,28 @@
+
+ ERROR 21 missing translations found.
+
++----------------------------------------------+-----------------------------------+
+| Key | File |
++----------------------------------------------+-----------------------------------+
+| example.missing | app/Example.php |
+| Missing PHP Class | app/ExampleJson.php |
+| Only Missing English PHP Class | app/ExampleJson.php |
+| example::folder/example.missing | app/ExampleMissing.php |
+| folder/example.missing | app/ExampleMissingOther.php |
+| example::example.missing | app/ExampleMissingOther.php |
+| example.vue.missing | resources/js/MissingComponent.vue |
+| folder/example.vue.missing | resources/js/MissingComponent.vue |
+| example::example.vue.missing | resources/js/MissingComponent.vue |
+| example::folder/example.vue.missing | resources/js/MissingComponent.vue |
+| Missing Vue Component | resources/js/MissingComponent.vue |
+| Missing Vendor Vue Component | resources/js/MissingComponent.vue |
+| example.blade.lang.missing | resources/views/missing.blade.php |
+| folder/example.blade.lang.missing | resources/views/missing.blade.php |
+| example::example.blade.lang.missing | resources/views/missing.blade.php |
+| example::folder/example.blade.lang.missing | resources/views/missing.blade.php |
+| Missing Blade File | resources/views/missing.blade.php |
+| example.blade.choice.missing | resources/views/missing.blade.php |
+| folder/example.blade.choice.missing | resources/views/missing.blade.php |
+| example::example.blade.choice.missing | resources/views/missing.blade.php |
+| example::folder/example.blade.choice.missing | resources/views/missing.blade.php |
++----------------------------------------------+-----------------------------------+
diff --git a/tests/.pest/snapshots/Commands/MissingCommandTest/it_errors_with_multiple_locales.snap b/tests/.pest/snapshots/Commands/MissingCommandTest/it_errors_with_multiple_locales.snap
new file mode 100644
index 0000000..53c6105
--- /dev/null
+++ b/tests/.pest/snapshots/Commands/MissingCommandTest/it_errors_with_multiple_locales.snap
@@ -0,0 +1,10 @@
+
+ ERROR 3 missing translations found.
+
++--------+--------------------------------+---------------------+
+| Locale | Key | File |
++--------+--------------------------------+---------------------+
+| en | Missing PHP Class | app/ExampleJson.php |
+| en | Only Missing English PHP Class | app/ExampleJson.php |
+| de | Missing PHP Class | app/ExampleJson.php |
++--------+--------------------------------+---------------------+
diff --git a/tests/.pest/snapshots/Commands/MissingCommandTest/it_errors_with_paths_argument.snap b/tests/.pest/snapshots/Commands/MissingCommandTest/it_errors_with_paths_argument.snap
new file mode 100644
index 0000000..1a1674c
--- /dev/null
+++ b/tests/.pest/snapshots/Commands/MissingCommandTest/it_errors_with_paths_argument.snap
@@ -0,0 +1,14 @@
+
+ ERROR 7 missing translations found.
+
++--------+-------------------------------------+-----------------------------------+
+| Locale | Key | File |
++--------+-------------------------------------+-----------------------------------+
+| en | example.missing | app/Example.php |
+| en | example.vue.missing | resources/js/MissingComponent.vue |
+| en | folder/example.vue.missing | resources/js/MissingComponent.vue |
+| en | example::example.vue.missing | resources/js/MissingComponent.vue |
+| en | example::folder/example.vue.missing | resources/js/MissingComponent.vue |
+| en | Missing Vue Component | resources/js/MissingComponent.vue |
+| en | Missing Vendor Vue Component | resources/js/MissingComponent.vue |
++--------+-------------------------------------+-----------------------------------+
diff --git a/tests/.pest/snapshots/Commands/MissingCommandTest/it_generates_baseline_file_then_successfully_ignores_baseline_keys.snap b/tests/.pest/snapshots/Commands/MissingCommandTest/it_generates_baseline_file_then_successfully_ignores_baseline_keys.snap
new file mode 100644
index 0000000..2e91978
--- /dev/null
+++ b/tests/.pest/snapshots/Commands/MissingCommandTest/it_generates_baseline_file_then_successfully_ignores_baseline_keys.snap
@@ -0,0 +1,3 @@
+
+ INFO Baseline file written with 49 translation keys.
+
diff --git a/tests/.pest/snapshots/Commands/MissingCommandTest/it_generates_baseline_file_then_successfully_ignores_baseline_keys__2.snap b/tests/.pest/snapshots/Commands/MissingCommandTest/it_generates_baseline_file_then_successfully_ignores_baseline_keys__2.snap
new file mode 100644
index 0000000..01b0cef
--- /dev/null
+++ b/tests/.pest/snapshots/Commands/MissingCommandTest/it_generates_baseline_file_then_successfully_ignores_baseline_keys__2.snap
@@ -0,0 +1,55 @@
+{
+ "en": [
+ "example.missing",
+ "Missing PHP Class",
+ "Only Missing English PHP Class",
+ "example::folder/example.missing",
+ "folder/example.missing",
+ "example::example.missing",
+ "example.vue.missing",
+ "folder/example.vue.missing",
+ "example::example.vue.missing",
+ "example::folder/example.vue.missing",
+ "Missing Vue Component",
+ "Missing Vendor Vue Component",
+ "example.blade.lang.missing",
+ "folder/example.blade.lang.missing",
+ "example::example.blade.lang.missing",
+ "example::folder/example.blade.lang.missing",
+ "Missing Blade File",
+ "example.blade.choice.missing",
+ "folder/example.blade.choice.missing",
+ "example::example.blade.choice.missing",
+ "example::folder/example.blade.choice.missing"
+ ],
+ "de": [
+ "example::example.used",
+ "example::folder/example.used",
+ "example.missing",
+ "Missing PHP Class",
+ "example::folder/example.missing",
+ "folder/example.missing",
+ "example::example.missing",
+ "example.vue.missing",
+ "folder/example.vue.missing",
+ "example::example.vue.missing",
+ "example::folder/example.vue.missing",
+ "Missing Vue Component",
+ "Missing Vendor Vue Component",
+ "example::example.vue.used",
+ "example::folder/example.vue.used",
+ "example.blade.lang.missing",
+ "folder/example.blade.lang.missing",
+ "example::example.blade.lang.missing",
+ "example::folder/example.blade.lang.missing",
+ "Missing Blade File",
+ "example.blade.choice.missing",
+ "folder/example.blade.choice.missing",
+ "example::example.blade.choice.missing",
+ "example::folder/example.blade.choice.missing",
+ "example::example.blade.lang.used",
+ "example::folder/example.blade.lang.used",
+ "example::example.blade.choice.used",
+ "example::folder/example.blade.choice.used"
+ ]
+}
\ No newline at end of file
diff --git a/tests/.pest/snapshots/Commands/MissingCommandTest/it_generates_baseline_file_then_successfully_ignores_baseline_keys__3.snap b/tests/.pest/snapshots/Commands/MissingCommandTest/it_generates_baseline_file_then_successfully_ignores_baseline_keys__3.snap
new file mode 100644
index 0000000..46a9a10
--- /dev/null
+++ b/tests/.pest/snapshots/Commands/MissingCommandTest/it_generates_baseline_file_then_successfully_ignores_baseline_keys__3.snap
@@ -0,0 +1,3 @@
+
+ INFO No missing translations found!
+
diff --git a/tests/.pest/snapshots/Commands/MissingCommandTest/it_outputs_success_message_when_no_missing_translations_found.snap b/tests/.pest/snapshots/Commands/MissingCommandTest/it_outputs_success_message_when_no_missing_translations_found.snap
new file mode 100644
index 0000000..46a9a10
--- /dev/null
+++ b/tests/.pest/snapshots/Commands/MissingCommandTest/it_outputs_success_message_when_no_missing_translations_found.snap
@@ -0,0 +1,3 @@
+
+ INFO No missing translations found!
+
diff --git a/tests/Commands/MissingCommandTest.php b/tests/Commands/MissingCommandTest.php
new file mode 100644
index 0000000..499c4bf
--- /dev/null
+++ b/tests/Commands/MissingCommandTest.php
@@ -0,0 +1,93 @@
+toBe(1)
+ ->and(Artisan::output())
+ ->toMatchSnapshot();
+});
+
+it('errors with paths argument', function () {
+ config()->set('translation-linter.missing.filters', []);
+ $firstFile = workbench_path('app/Example.php');
+ $secondFile = resource_path('js/MissingComponent.vue');
+
+ withoutMockingConsoleOutput();
+ expect(artisan("translation:missing \"$firstFile\" \"$secondFile\" \"/this/does/not/exist\""))
+ ->toBe(1)
+ ->and(Artisan::output())
+ ->toMatchSnapshot();
+});
+
+it('errors with different fields', function () {
+ config()->set('translation-linter.missing.fields.locale', false);
+
+ withoutMockingConsoleOutput();
+ expect(artisan('translation:missing'))
+ ->toBe(1)
+ ->and(Artisan::output())
+ ->toMatchSnapshot();
+});
+
+it('errors with default no fields', function () {
+ config()->set('translation-linter.missing.fields.locale', false);
+ config()->set('translation-linter.missing.fields.key', false);
+ config()->set('translation-linter.missing.fields.file', false);
+
+ withoutMockingConsoleOutput();
+ expect(artisan('translation:missing'))
+ // ->toBe(1)
+ ->and(Artisan::output())
+ ->toMatchSnapshot();
+});
+
+it('errors with multiple locales', function () {
+ config()->set('translation-linter.lang.locales', ['en', 'de']);
+ $firstFile = workbench_path('app/ExampleJson.php');
+
+ withoutMockingConsoleOutput();
+ expect(artisan("translation:missing \"$firstFile\""))
+ ->toBe(1)
+ ->and(Artisan::output())
+ ->toMatchSnapshot();
+});
+
+it('generates baseline file then successfully ignores baseline keys', function () {
+ config()->set('translation-linter.lang.locales', ['en', 'de']);
+
+ withoutMockingConsoleOutput();
+ expect(artisan('translation:missing --generate-baseline'))
+ ->toBe(0)
+ ->and(Artisan::output())
+ ->toMatchSnapshot();
+
+ expect($file = config('translation-linter.missing.baseline'))
+ ->toBeReadableFile()
+ ->and(file_get_contents($file))
+ ->toMatchSnapshot();
+
+ expect(artisan('translation:missing'))
+ ->toBe(0)
+ ->and(Artisan::output())
+ ->toMatchSnapshot();
+});
+
+it('outputs success message when no missing translations found', function () {
+ config()->set('translation-linter.lang.locales', []);
+ withoutMockingConsoleOutput();
+ expect(artisan('translation:missing'))
+ ->toBe(0)
+ ->and(Artisan::output())
+ ->toMatchSnapshot();
+});
diff --git a/workbench/app/Example.php b/workbench/app/Example.php
index dfc23dd..6cd471d 100644
--- a/workbench/app/Example.php
+++ b/workbench/app/Example.php
@@ -7,7 +7,7 @@
class Example
{
- public function handle(Validator $validator)
+ public function unused()
{
$example = __('example.used');
@@ -20,4 +20,9 @@ public function handle(Validator $validator)
'example::folder/example.used'
));
}
+
+ public function missing()
+ {
+ $example = __('example.missing');
+ }
}
diff --git a/workbench/app/ExampleJson.php b/workbench/app/ExampleJson.php
index 0d90c1a..b8505a5 100644
--- a/workbench/app/ExampleJson.php
+++ b/workbench/app/ExampleJson.php
@@ -10,5 +10,8 @@ public function handle(Validator $validator)
{
__('Used PHP Class');
__("Used Vendor PHP Class");
+
+ __('Missing PHP Class');
+ __('Only Missing English PHP Class');
}
}
diff --git a/workbench/app/ExampleMissing.php b/workbench/app/ExampleMissing.php
new file mode 100644
index 0000000..482087c
--- /dev/null
+++ b/workbench/app/ExampleMissing.php
@@ -0,0 +1,16 @@
+when(fn () => Lang::get(
+ 'example::folder/example.missing'
+ ));
+ }
+}
diff --git a/workbench/app/ExampleMissingOther.php b/workbench/app/ExampleMissingOther.php
new file mode 100644
index 0000000..846005a
--- /dev/null
+++ b/workbench/app/ExampleMissingOther.php
@@ -0,0 +1,17 @@
+loadTranslationsFrom(workbench_path('/vendor/example/lang'), 'example');
+ $this->loadJsonTranslationsFrom(workbench_path('/vendor/example/lang'));
}
}
diff --git a/workbench/lang/de.json b/workbench/lang/de.json
index 9a664b8..08daa39 100644
--- a/workbench/lang/de.json
+++ b/workbench/lang/de.json
@@ -1,6 +1,7 @@
{
"Used PHP Class": "Ich werde in einer PHP-Klasse verwendet",
"Unused PHP Class": "Ich werde in einer PHP-Klasse nicht verwendet",
+ "Only Missing English PHP Class": "-",
"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",
diff --git a/workbench/resources/js/MissingComponent.vue b/workbench/resources/js/MissingComponent.vue
new file mode 100644
index 0000000..051ef01
--- /dev/null
+++ b/workbench/resources/js/MissingComponent.vue
@@ -0,0 +1,22 @@
+
+ {{ __('example.vue.missing', {foo: 'bar'}) }}
+ {{ __(
+ 'folder/example.vue.missing',
+ {foo: 'bar'}
+ )}}
+
+
+
diff --git a/workbench/resources/js/ExampleComponent.vue b/workbench/resources/js/UnusedComponent.vue
similarity index 100%
rename from workbench/resources/js/ExampleComponent.vue
rename to workbench/resources/js/UnusedComponent.vue
diff --git a/workbench/resources/views/missing.blade.php b/workbench/resources/views/missing.blade.php
new file mode 100644
index 0000000..e569b90
--- /dev/null
+++ b/workbench/resources/views/missing.blade.php
@@ -0,0 +1,21 @@
+@lang('example.blade.lang.missing', ['foo' => 'bar'])
+@lang(
+ 'folder/example.blade.lang.missing',
+ ['foo' => 'bar'],
+)
+@lang('example::example.blade.lang.missing')
+@lang(
+ "example::folder/example.blade.lang.missing"
+)
+
+{{ __('Missing Blade File') }}
+
+@if(true)
+ @choice('example.blade.choice.missing', 1)
+ @choice('folder/example.blade.choice.missing', 1)
+ @choice('example::example.blade.choice.missing', 1)
+ @choice(
+ "example::folder/example.blade.choice.missing",
+ 1,
+ )
+@endif
diff --git a/workbench/resources/views/welcome.blade.php b/workbench/resources/views/unused.blade.php
similarity index 100%
rename from workbench/resources/views/welcome.blade.php
rename to workbench/resources/views/unused.blade.php