From ae2f690346c1c9a2f964d65ae6f434abf0783084 Mon Sep 17 00:00:00 2001 From: bambamboole Date: Sun, 12 May 2024 17:41:55 +0200 Subject: [PATCH 1/3] Add translation finder with a command to statically search through code and extract translation usages --- composer.json | 1 + src/Console/FindTranslationsCommand.php | 23 +++++ ...aravelTranslationDumperServiceProvider.php | 39 +++++--- src/TranslationFinder.php | 95 +++++++++++++++++++ tests/Feature/TranslationFinderTest.php | 27 ++++++ tests/Feature/fixtures/code/TestClass.php | 15 +++ .../Feature/fixtures/code/template.blade.php | 2 + 7 files changed, 187 insertions(+), 15 deletions(-) create mode 100644 src/Console/FindTranslationsCommand.php create mode 100644 src/TranslationFinder.php create mode 100644 tests/Feature/TranslationFinderTest.php create mode 100644 tests/Feature/fixtures/code/TestClass.php create mode 100644 tests/Feature/fixtures/code/template.blade.php diff --git a/composer.json b/composer.json index 5c6acca..89b7738 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ ], "require": { "php": "^8.1", + "illuminate/console": "^9.0|^10.0|^11.0", "illuminate/support": "^9.0|^10.0|^11.0", "illuminate/translation": "^9.0|^10.0|^11.0" }, diff --git a/src/Console/FindTranslationsCommand.php b/src/Console/FindTranslationsCommand.php new file mode 100644 index 0000000..a6e84de --- /dev/null +++ b/src/Console/FindTranslationsCommand.php @@ -0,0 +1,23 @@ +findTranslations(); + $this->info(sprintf('Found %s translations', count($translations))); + $dumper->dump($translations); + + return self::SUCCESS; + } +} diff --git a/src/LaravelTranslationDumperServiceProvider.php b/src/LaravelTranslationDumperServiceProvider.php index 3703c22..ef7aca7 100644 --- a/src/LaravelTranslationDumperServiceProvider.php +++ b/src/LaravelTranslationDumperServiceProvider.php @@ -2,6 +2,7 @@ namespace Bambamboole\LaravelTranslationDumper; +use Bambamboole\LaravelTranslationDumper\Console\FindTranslationsCommand; use Illuminate\Contracts\Config\Repository; use Illuminate\Contracts\Foundation\Application; use Illuminate\Filesystem\Filesystem; @@ -13,6 +14,9 @@ class LaravelTranslationDumperServiceProvider extends ServiceProvider public function boot(): void { if ($this->app->runningInConsole()) { + $this->commands([ + FindTranslationsCommand::class, + ]); $this->publishes([ dirname(__DIR__).'/config/config.php' => $this->app->configPath('translation.php'), ], 'config'); @@ -23,23 +27,28 @@ public function register(): void { $this->mergeConfigFrom(__DIR__.'/../config/config.php', 'translation'); - if ($this->app->make(Repository::class)->get('translation.dump_translations')) { - $this->app->singleton( - TranslationDumper::class, - static fn (Application $app) => new TranslationDumper( - new Filesystem(), - new ArrayExporter(), - $app->langPath(), - $app->make(Repository::class)->get('app.locale'), - $app->make(Repository::class)->get('translation.dump_prefix'), - ), - ); + $this->app->singleton( + TranslationDumper::class, + static fn (Application $app) => new TranslationDumper( + new Filesystem(), + new ArrayExporter(), + $app->langPath(), + $app->make(Repository::class)->get('app.locale'), + $app->make(Repository::class)->get('translation.dump_prefix'), + ), + ); - $this->app->bind( - TranslationDumperInterface::class, - static fn (Application $app) => $app->make($app->make(Repository::class)->get('translation.dumper')) - ); + $this->app->bind( + TranslationDumperInterface::class, + static fn (Application $app) => $app->make($app->make(Repository::class)->get('translation.dumper')) + ); + + $this->app->singleton( + TranslationFinder::class, + static fn (Application $app) => new TranslationFinder($app->basePath()), + ); + if ($this->app->make(Repository::class)->get('translation.dump_translations')) { $this->app->extend( 'translator', static fn (Translator $translator, $app) => new DumpingTranslator( diff --git a/src/TranslationFinder.php b/src/TranslationFinder.php new file mode 100644 index 0000000..26de2a6 --- /dev/null +++ b/src/TranslationFinder.php @@ -0,0 +1,95 @@ +]'. // Must not have an alphanum or _ or > before real method + '('.implode('|', $functions).')'. // Must start with one of the functions + '\('. // Match opening parenthesis + "[\'\"]". // Match " or ' + '('. // Start a new group to match: + '[\/a-zA-Z0-9_-]+'. // Must start with group + "([.](?! )[^\1)]+)+". // Be followed by one or more items/keys + ')'. // Close group + "[\'\"]". // Closing quote + '[\),]'; // Close parentheses or new parameter + + $stringPattern = + '[^\w]'. // Must not have an alphanum before real method + '('.implode('|', $functions).')'. // Must start with one of the functions + '\(\s*'. // Match opening parenthesis + "(?P['\"])". // Match " or ' and store in {quote} + "(?P(?:\\\k{quote}|(?!\k{quote}).)*)". // Match any string that can be {quote} escaped + '\k{quote}'. // Match " or ' previously matched + '\s*[\),]'; // Close parentheses or new parameter + + // Find all PHP + Twig files in the app folder, except for storage + $finder = new Finder(); + $files = $finder->in($this->basePath) + ->exclude('storage') + ->exclude('vendor') + ->name('*.php') + ->files(); + + /** @var \Symfony\Component\Finder\SplFileInfo $file */ + foreach ($files as $file) { + // Search the current file for the pattern + if (preg_match_all("/$groupPattern/siU", $file->getContents(), $matches)) { + // Get all matches + foreach ($matches[2] as $key) { + $groupKeys[] = $key; + } + } + + if (preg_match_all("/$stringPattern/siU", $file->getContents(), $matches)) { + foreach ($matches['string'] as $key) { + if (preg_match("/(^[\/a-zA-Z0-9_-]+([.][^\1)\ ]+)+$)/siU", $key, $groupMatches)) { + // group{.group}.key format, already in $groupKeys but also matched here + // do nothing, it has to be treated as a group + continue; + } + + //TODO: This can probably be done in the regex, but I couldn't do it. + //skip keys which contain namespacing characters, unless they also contain a + //space, which makes it JSON. + if (! (Str::contains($key, '::') && Str::contains($key, '.')) + || Str::contains($key, ' ')) { + $stringKeys[] = $key; + } + } + } + } + // Remove duplicates + $groupKeys = array_unique($groupKeys); + $stringKeys = array_unique($stringKeys); + + return array_merge($groupKeys, $stringKeys); + } +} diff --git a/tests/Feature/TranslationFinderTest.php b/tests/Feature/TranslationFinderTest.php new file mode 100644 index 0000000..709572c --- /dev/null +++ b/tests/Feature/TranslationFinderTest.php @@ -0,0 +1,27 @@ +createSubject()->findTranslations(); + + self::assertSame([ + 'test.dotted', + 'test.dotted.additional', + 'This is just a sentence', + 'test undotted key', + 'test', + ], $translations); + } + + private function createSubject(): TranslationFinder + { + return new TranslationFinder(__DIR__.'/fixtures/code'); + } +} diff --git a/tests/Feature/fixtures/code/TestClass.php b/tests/Feature/fixtures/code/TestClass.php new file mode 100644 index 0000000..fa71ac5 --- /dev/null +++ b/tests/Feature/fixtures/code/TestClass.php @@ -0,0 +1,15 @@ +{{ __('test.dotted') }} +
@lang('This is just a sentence')
From 4dcad3b21626b4260311dae81d266b4f734becc4 Mon Sep 17 00:00:00 2001 From: bambamboole Date: Sun, 12 May 2024 17:46:05 +0200 Subject: [PATCH 2/3] fix ordering --- src/TranslationFinder.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/TranslationFinder.php b/src/TranslationFinder.php index 26de2a6..4877489 100644 --- a/src/TranslationFinder.php +++ b/src/TranslationFinder.php @@ -89,7 +89,10 @@ public function findTranslations(): array // Remove duplicates $groupKeys = array_unique($groupKeys); $stringKeys = array_unique($stringKeys); + // Merge and order them + $keys = array_merge($groupKeys, $stringKeys); + ksort($keys, SORT_NATURAL | SORT_FLAG_CASE); - return array_merge($groupKeys, $stringKeys); + return $keys; } } From ceb23b059179c80095ae64404be2e8ed3c787bf0 Mon Sep 17 00:00:00 2001 From: bambamboole Date: Sun, 12 May 2024 17:48:26 +0200 Subject: [PATCH 3/3] Assert only values exist and not the ordering --- tests/Feature/TranslationFinderTest.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/Feature/TranslationFinderTest.php b/tests/Feature/TranslationFinderTest.php index 709572c..db48f03 100644 --- a/tests/Feature/TranslationFinderTest.php +++ b/tests/Feature/TranslationFinderTest.php @@ -11,13 +11,16 @@ public function testItCanFindTranslationsInCode(): void { $translations = $this->createSubject()->findTranslations(); - self::assertSame([ + $expected = [ 'test.dotted', 'test.dotted.additional', 'This is just a sentence', 'test undotted key', 'test', - ], $translations); + ]; + foreach ($expected as $item) { + self::assertContains($item, $translations); + } } private function createSubject(): TranslationFinder