From 3a57d3350100cdc3889000e1595026e4fb107e67 Mon Sep 17 00:00:00 2001 From: Amir Rami Date: Sat, 16 Jan 2021 14:53:51 +0100 Subject: [PATCH] Default export to php lang files (#12) * Exporting translations into default php files * Fix styling * Tests written and PHPDocblocks added * Fix styling * Update README.md, add comments in config file, flush view folder as well on test finish, code cleanup Co-authored-by: Amir Co-authored-by: amiranagram --- README.md | 30 +++- composer.json | 1 + config/localizator.php | 21 ++- src/Collections/DefaultKeyCollection.php | 29 ++++ src/Collections/JsonKeyCollection.php | 16 ++ src/Commands/LocalizeCommand.php | 61 ++++++-- src/Contracts/Collectable.php | 18 +++ src/Contracts/Translatable.php | 14 ++ src/Contracts/Writable.php | 16 ++ src/Facades/Localizator.php | 26 ++++ src/Localizator.php | 130 ----------------- src/Parser.php | 30 ---- src/ServiceProvider.php | 48 +++++- .../Collectors/DefaultKeyCollector.php | 70 +++++++++ src/Services/Collectors/JsonKeyCollector.php | 31 ++++ src/Services/FileFinder.php | 41 ++++++ src/Services/Localizator.php | 52 +++++++ src/Services/Parser.php | 135 +++++++++++++++++ src/Services/Writers/DefaultWriter.php | 82 +++++++++++ src/Services/Writers/JsonWriter.php | 28 ++++ tests/Concerns/CreatesTestFiles.php | 85 +++++++++++ tests/Concerns/ImportsLangFiles.php | 42 ++++++ tests/LocalizatorTest.php | 137 ++++++++++++++++-- tests/TestCase.php | 109 ++++++++++---- 24 files changed, 1024 insertions(+), 228 deletions(-) create mode 100644 src/Collections/DefaultKeyCollection.php create mode 100644 src/Collections/JsonKeyCollection.php create mode 100644 src/Contracts/Collectable.php create mode 100644 src/Contracts/Translatable.php create mode 100644 src/Contracts/Writable.php create mode 100644 src/Facades/Localizator.php delete mode 100644 src/Localizator.php delete mode 100644 src/Parser.php create mode 100644 src/Services/Collectors/DefaultKeyCollector.php create mode 100644 src/Services/Collectors/JsonKeyCollector.php create mode 100644 src/Services/FileFinder.php create mode 100644 src/Services/Localizator.php create mode 100644 src/Services/Parser.php create mode 100644 src/Services/Writers/DefaultWriter.php create mode 100644 src/Services/Writers/JsonWriter.php create mode 100644 tests/Concerns/CreatesTestFiles.php create mode 100644 tests/Concerns/ImportsLangFiles.php diff --git a/README.md b/README.md index c9f9040..82f730c 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,7 @@ [![Total Downloads](https://img.shields.io/packagist/dt/amirami/localizator.svg)](https://packagist.org/packages/amirami/localizator) [![Latest Version on Packagist](https://img.shields.io/packagist/v/amirami/localizator.svg)](https://packagist.org/packages/amirami/localizator) -Localizator is a small tool for Laravel that gives you the ability to extract untranslated string from your project files with one command. Current version extracts only to JSON - language files. +Localizator is a small tool for Laravel that gives you the ability to extract untranslated string from your project files with one command. ## Installation @@ -53,12 +52,31 @@ php artisan vendor:publish --provider="Amirami\Localizator\ServiceProvider" --ta This is the contents of the published config file: ```php + [ + /** + * Short keys. This is the default for Laravel. + * They are stored in PHP files inside folders name by their locale code. + * Laravel installation comes with default: auth.php, pagination.php, passwords.php and validation.php + */ + 'default' => true, + /** + * Translations strings as key. + * They are stored in JSON file for each locale. + */ + 'json' => true, + ], + /** * Search criteria for files. */ - 'search' => [ + 'search' => [ /** * Directories which should be looked inside. */ @@ -81,9 +99,10 @@ return [ /** * Should the localize command sort extracted strings alphabetically? */ - 'sort' => true, + 'sort' => true, ]; + ``` ## Usage @@ -94,7 +113,8 @@ To extract all the strings, it's as simple as running: php artisan localize de,fr ``` -This commands will create (if don't exist) `de.json` and `fr.json` files inside the `resources/lang` directory. +This command will create (if don't exist) `de.json` and `fr.json` files inside the `resources/lang` directory. +If you have short keys enabled and used in your files (e.g. `pagination.next`) the localize command will create folders `de` and `fr` inside `resources/lang` directory and PHP files inside by the short key's prefix (e.g. `pagination.php`). You can also run the artisan command without the country code arguments. diff --git a/composer.json b/composer.json index 398adbd..40f78b2 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ "php": "^7.2|^7.4", "ext-json": "*", "illuminate/support": "^6.0|^8.0", + "illuminate/filesystem": "^6.0|^8.0", "symfony/finder": "^4.4|^5.1" }, "require-dev": { diff --git a/config/localizator.php b/config/localizator.php index 69437c1..4314dbf 100644 --- a/config/localizator.php +++ b/config/localizator.php @@ -2,10 +2,27 @@ return [ + /** + * Localize types of translation strings. + */ + 'localize' => [ + /** + * Short keys. This is the default for Laravel. + * They are stored in PHP files inside folders name by their locale code. + * Laravel installation comes with default: auth.php, pagination.php, passwords.php and validation.php + */ + 'default' => true, + /** + * Translations strings as key. + * They are stored in JSON file for each locale. + */ + 'json' => true, + ], + /** * Search criteria for files. */ - 'search' => [ + 'search' => [ /** * Directories which should be looked inside. */ @@ -28,6 +45,6 @@ /** * Should the localize command sort extracted strings alphabetically? */ - 'sort' => true, + 'sort' => true, ]; diff --git a/src/Collections/DefaultKeyCollection.php b/src/Collections/DefaultKeyCollection.php new file mode 100644 index 0000000..4423da9 --- /dev/null +++ b/src/Collections/DefaultKeyCollection.php @@ -0,0 +1,29 @@ +sortBy(function ($item, $key) { + return $key; + }, SORT_STRING); + } + + /** + * @param mixed $items + * @return static + */ + public function merge($items): DefaultKeyCollection + { + return parent::merge(Arr::dot($items)); + } +} diff --git a/src/Collections/JsonKeyCollection.php b/src/Collections/JsonKeyCollection.php new file mode 100644 index 0000000..8daeae0 --- /dev/null +++ b/src/Collections/JsonKeyCollection.php @@ -0,0 +1,16 @@ +sortBy(function ($item, $key) { + return $key; + }, SORT_STRING); + } +} diff --git a/src/Commands/LocalizeCommand.php b/src/Commands/LocalizeCommand.php index 6a1120d..c39a008 100644 --- a/src/Commands/LocalizeCommand.php +++ b/src/Commands/LocalizeCommand.php @@ -2,10 +2,13 @@ namespace Amirami\Localizator\Commands; -use Amirami\Localizator\Localizator; +use Amirami\Localizator\Facades\Localizator; use Illuminate\Console\Command; -use Symfony\Component\Console\Input\InputArgument; +/** + * Class LocalizeCommand + * @package Amirami\Localizator\Commands + */ class LocalizeCommand extends Command { /** @@ -20,33 +23,61 @@ class LocalizeCommand extends Command * * @var string */ - protected $description = 'Generate local files with strings found in scanned files.'; + protected $description = 'Generate locale files with strings found in scanned files.'; /** + * Execute the localize command. + * * @return void */ public function handle(): void { - $languages = $this->argument('lang') - ? explode(',', $this->argument('lang')) - : [config('app.locale')]; + $locales = $this->getLocales(); + $progressBar = $this->output->createProgressBar(count($locales)); + + $this->info('Localizing: ' . implode(', ', $locales)); - $localizator = app(Localizator::class); + $files = app('localizator.finder')->getFiles(); + $parser = app('localizator.parser'); - foreach ($languages as $language) { - $localizator->localize($language); + $parser->parseKeys($files); - $this->info('Translatable strings have been generated for locale: '.$language); + $progressBar->setFormat('%current%/%max% [%bar%] %percent:3s%% %message%'); + $progressBar->setMessage('Localizing...'); + $progressBar->start(); + + foreach ($locales as $locale) { + $progressBar->setMessage("Localizing {$locale}..."); + + foreach ($this->getTypes() as $type) { + Localizator::localize($parser->getKeys($locale, $type), $type, $locale); + } + + $progressBar->advance(); } + + $progressBar->finish(); + + $this->info( + "\nTranslatable strings have been generated for locale(s): " . implode(', ', $locales) + ); + } + + /** + * @return array + */ + protected function getLocales(): array + { + return $this->argument('lang') + ? explode(',', $this->argument('lang')) + : [config('app.locale')]; } /** - * @return array|array[] + * @return array */ - protected function getArguments(): array + protected function getTypes(): array { - return [ - ['lang', InputArgument::REQUIRED, 'Argument'], - ]; + return array_keys(array_filter(config('localizator.localize'))); } } diff --git a/src/Contracts/Collectable.php b/src/Contracts/Collectable.php new file mode 100644 index 0000000..7720e64 --- /dev/null +++ b/src/Contracts/Collectable.php @@ -0,0 +1,18 @@ +finder = $finder; - } - - /** - * @param string $language - * - * @throws \JsonException - * - * @return bool - */ - public function localize(string $language): bool - { - $strings = $this->parseStrings( - $this->findAndCollectFiles() - ); - - $translated = $this->getExisting($language); - - $strings = $strings->merge($translated); - - if (config('localizator.sort')) { - $strings = $this->sortAlphabetically($strings); - } - - return $this->writeToFile( - $language, - $strings->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) - ); - } - - protected function parseStrings(Collection $files): Collection - { - $parser = app(Parser::class); - $parsedStrings = collect(); - - $files - ->map(function (SplFileInfo $file) use ($parser) { - return $parser->getStrings($file); - }) - ->flatten() - ->each(function (string $string) use ($parsedStrings) { - $parsedStrings->put($string, $string); - }); - - return $parsedStrings; - } - - /** - * @return Collection - */ - protected function findAndCollectFiles(): Collection - { - $config = config('localizator.search'); - $directories = array_map(static function ($dir) { - return base_path($dir); - }, $config['dirs']); - - return new Collection( - $this->finder->in($directories)->name($config['patterns'])->files() - ); - } - - /** - * @param string $language - * - * @throws \JsonException - * - * @return Collection - */ - protected function getExisting(string $language): Collection - { - $locale = resource_path('lang/'.$language.'.json'); - - if (! file_exists($locale)) { - return collect(); - } - - return collect( - json_decode(file_get_contents($locale), true, 512, JSON_THROW_ON_ERROR) - ); - } - - /** - * @param Collection $strings - * - * @return Collection - */ - protected function sortAlphabetically(Collection $strings): Collection - { - return $strings->sortBy(function ($item, $key) { - return $key; - }, SORT_STRING); - } - - /** - * @param string $language - * @param string $contents - * - * @return bool - */ - protected function writeToFile(string $language, string $contents): bool - { - $file = resource_path('lang/'.$language.'.json'); - - return (bool) (new Filesystem())->put($file, $contents); - } -} diff --git a/src/Parser.php b/src/Parser.php deleted file mode 100644 index b90b141..0000000 --- a/src/Parser.php +++ /dev/null @@ -1,30 +0,0 @@ -searchPattern($function), $file->getContents(), $matches) - ) { - $strings->push($matches[2]); - } - } - - return $strings->count() ? $strings->flatten()->unique() : $strings; - } - - protected function searchPattern(string $function): string - { - return '/('.$function.')\(\h*[\'"](.+)[\'"]\h*[),]/U'; - } -} diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index d4ae181..84ee137 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -3,15 +3,26 @@ namespace Amirami\Localizator; use Amirami\Localizator\Commands\LocalizeCommand; +use Amirami\Localizator\Services\Collectors\DefaultKeyCollector; +use Amirami\Localizator\Services\Collectors\JsonKeyCollector; +use Amirami\Localizator\Services\FileFinder; +use Amirami\Localizator\Services\Localizator; +use Amirami\Localizator\Services\Parser; +use Amirami\Localizator\Services\Writers\DefaultWriter; +use Amirami\Localizator\Services\Writers\JsonWriter; use Illuminate\Support\ServiceProvider as BaseServiceProvider; +/** + * Class ServiceProvider + * @package Amirami\Localizator + */ class ServiceProvider extends BaseServiceProvider { - public function boot() + /** + * @return void + */ + public function boot(): void { - $this->app->bind(Localizator::class); - $this->app->bind(Parser::class); - if ($this->app->runningInConsole()) { $this->publishes([ __DIR__.'/../config/localizator.php' => config_path('localizator.php'), @@ -21,8 +32,35 @@ public function boot() } } - public function register() + /** + * @return void + */ + public function register(): void { $this->mergeConfigFrom(__DIR__.'/../config/localizator.php', 'localizator'); + + $this->registerContainerClasses(); + } + + /** + * @return void + */ + private function registerContainerClasses(): void + { + $this->app->singleton('localizator', Localizator::class); + + $this->app->singleton('localizator.finder', function ($app) { + return new FileFinder($app['config']['localizator']); + }); + + $this->app->singleton('localizator.parser', function ($app) { + return new Parser($app['config']['localizator']); + }); + + $this->app->bind('localizator.writers.default', DefaultWriter::class); + $this->app->bind('localizator.writers.json', JsonWriter::class); + + $this->app->bind('localizator.collector.default', DefaultKeyCollector::class); + $this->app->bind('localizator.collector.json', JsonKeyCollector::class); } } diff --git a/src/Services/Collectors/DefaultKeyCollector.php b/src/Services/Collectors/DefaultKeyCollector.php new file mode 100644 index 0000000..9531736 --- /dev/null +++ b/src/Services/Collectors/DefaultKeyCollector.php @@ -0,0 +1,70 @@ +getFiles($locale) + ->each(function (SplFileInfo $fileInfo) use ($locale, $translated) { + $translated->put( + $fileInfo->getFilenameWithoutExtension(), + $this->requireFile($locale, $fileInfo) + ); + }); + + return $translated; + } + + /** + * @param string $locale + * @return Collection + */ + protected function getFiles(string $locale): Collection + { + $dir = resource_path('lang' . DIRECTORY_SEPARATOR . $locale); + + if (! file_exists($dir)) { + if (! mkdir($dir, 0755) && ! is_dir($dir)) { + throw new RuntimeException(sprintf('Directory "%s" was not created', $dir)); + } + + return new Collection; + } + + return new Collection( + (new Finder)->in($dir)->name('*.php')->files() + ); + } + + /** + * @param string $locale + * @param SplFileInfo $fileInfo + * @return array + * @noinspection PhpIncludeInspection + */ + protected function requireFile(string $locale, SplFileInfo $fileInfo): array + { + return require resource_path( + 'lang' . DIRECTORY_SEPARATOR . $locale . DIRECTORY_SEPARATOR . $fileInfo->getRelativePathname() + ); + } +} diff --git a/src/Services/Collectors/JsonKeyCollector.php b/src/Services/Collectors/JsonKeyCollector.php new file mode 100644 index 0000000..81e8a0f --- /dev/null +++ b/src/Services/Collectors/JsonKeyCollector.php @@ -0,0 +1,31 @@ +config = $config; + } + + /** + * @return Collection + */ + public function getFiles(): Collection + { + $directories = array_map(static function ($dir) { + return base_path($dir); + }, $this->config['search']['dirs']); + + return new Collection( + (new Finder)->in($directories)->name($this->config['search']['patterns'])->files() + ); + } +} diff --git a/src/Services/Localizator.php b/src/Services/Localizator.php new file mode 100644 index 0000000..f0d79dc --- /dev/null +++ b/src/Services/Localizator.php @@ -0,0 +1,52 @@ +getCollector($type)->getTranslated($locale); + + $keys = $keys + ->merge($translated) + ->when(config('localizator.sort'), function (Translatable $keyCollection) { + return $keyCollection->sortAlphabetically(); + }); + + $this->getWriter($type)->put($locale, $keys); + } + + /** + * @param string $type + * @return Writable + */ + protected function getWriter(string $type): Writable + { + return app("localizator.writers.$type"); + } + + /** + * @param string $type + * @return Collectable + */ + protected function getCollector(string $type): Collectable + { + return app("localizator.collector.$type"); + } +} diff --git a/src/Services/Parser.php b/src/Services/Parser.php new file mode 100644 index 0000000..07f0fc8 --- /dev/null +++ b/src/Services/Parser.php @@ -0,0 +1,135 @@ +config = $config; + $this->defaultKeys = new DefaultKeyCollection; + $this->jsonKeys = new JsonKeyCollection; + } + + /** + * @param Collection $files + */ + public function parseKeys(Collection $files): void + { + $files + ->map(function (SplFileInfo $file) { + return $this->getStrings($file); + }) + ->flatten() + ->each(function (string $string) { + if ($this->isDotKey($string)) { + $this->defaultKeys->push($string); + } else { + $this->jsonKeys->push($string); + } + }); + } + + /** + * @param $key + * @return bool + */ + protected function isDotKey($key): bool + { + return (bool)preg_match('/^[^.\s]\S*\.\S*[^.\s]$/', $key); + } + + /** + * @param SplFileInfo $file + * @return Collection + */ + protected function getStrings(SplFileInfo $file): Collection + { + $keys = new Collection; + + foreach ($this->config['search']['functions'] as $function) { + if ( + preg_match_all($this->searchPattern($function), $file->getContents(), $matches) + ) { + $keys->push($matches[2]); + } + } + + return $keys->count() ? $keys->flatten()->unique() : $keys; + } + + /** + * @param string $function + * @return string + */ + protected function searchPattern(string $function): string + { + return '/(' . $function . ')\(\h*[\'"](.+)[\'"]\h*[),]/U'; + } + + /** + * @param string $locale + * @param string $type + * @return Collection + */ + public function getKeys(string $locale, string $type): Collection + { + switch ($type) { + case 'default': + return $this->defaultKeys->combine( + $this->combineValues($locale, $type, $this->defaultKeys) + ); + case 'json': + return $this->jsonKeys->combine( + $this->combineValues($locale, $type, $this->jsonKeys) + ); + } + + throw new RuntimeException('Export type not recognized! Only recognized types are "default" and "json".'); + } + + /** + * @param string $locale + * @param string $type + * @param Collection $values + * @return Collection + */ + protected function combineValues(string $locale, string $type, Collection $values): Collection + { + if ($type === 'default' || $locale !== config('app.locale')) { + return (new Collection)->pad($values->count(), ''); + } + + return $values; + } +} diff --git a/src/Services/Writers/DefaultWriter.php b/src/Services/Writers/DefaultWriter.php new file mode 100644 index 0000000..85b9d25 --- /dev/null +++ b/src/Services/Writers/DefaultWriter.php @@ -0,0 +1,82 @@ +elevate($keys) + ->each(function ($contents, $fileName) use ($locale) { + $file = $this->getFile($locale, $fileName); + + (new Filesystem)->put( + $file, + $this->exportArray($contents) + ); + }); + } + + /** + * @param Translatable $keys + * @return DefaultKeyCollection + */ + protected function elevate(Translatable $keys): DefaultKeyCollection + { + $elevated = []; + + $keys->each(function ($value, $key) use (&$elevated) { + Arr::set($elevated, $key, $value); + }); + + return new DefaultKeyCollection($elevated); + } + + /** + * @param array $contents + * @return string + */ + protected function exportArray(array $contents): string + { + $export = var_export($contents, true); + + $patterns = [ + "/array \(/" => '[', + "/^([ ]*)\)(,?)$/m" => '$1$1]$2', + "/=>[ ]?\n[ ]+\[/" => '=> [', + "/([ ]*)(\'[^\']+\') => ([\[\'])/" => '$1$1$2 => $3', + ]; + + $export = preg_replace( + array_keys($patterns), + array_values($patterns), + $export + ); + + return sprintf("put( + $file, + $keys->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) + ); + } +} diff --git a/tests/Concerns/CreatesTestFiles.php b/tests/Concerns/CreatesTestFiles.php new file mode 100644 index 0000000..01d76f6 --- /dev/null +++ b/tests/Concerns/CreatesTestFiles.php @@ -0,0 +1,85 @@ +createTestFile( + $contents, + 'views' . DIRECTORY_SEPARATOR . "{$fileName}.blade.php" + ); + } + + /** + * @param string $contents + * @param string $fileName + * @return void + */ + protected function createTestLangFile(string $contents, string $fileName): void + { + $this->createTestFile( + $contents, + 'lang' . DIRECTORY_SEPARATOR . $fileName + ); + } + + /** + * @param array $contents + * @param string $locale + * @return void + */ + protected function createTestJsonLangFile(array $contents, string $locale): void + { + $this->createTestLangFile( + json_encode($contents, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), + "{$locale}.json" + ); + } + + /** + * @param array $contents + * @param string $fileName + * @param string $locale + * @return void + */ + protected function createTestDefaultLangFile(array $contents, string $fileName, string $locale): void + { + $export = sprintf("createTestLangFile( + $export, + $locale . DIRECTORY_SEPARATOR . "{$fileName}.php" + ); + } +} diff --git a/tests/Concerns/ImportsLangFiles.php b/tests/Concerns/ImportsLangFiles.php new file mode 100644 index 0000000..5b4f801 --- /dev/null +++ b/tests/Concerns/ImportsLangFiles.php @@ -0,0 +1,42 @@ +getLangFilePath($locale . DIRECTORY_SEPARATOR . "{$fileName}.php"); + } + + /** + * @param string $locale + * @return array + */ + protected function getJsonLangContents(string $locale): array + { + return json_decode( + file_get_contents($this->getLangFilePath("{$locale}.json")), + true + ); + } +} diff --git a/tests/LocalizatorTest.php b/tests/LocalizatorTest.php index e5cdac3..460c088 100644 --- a/tests/LocalizatorTest.php +++ b/tests/LocalizatorTest.php @@ -2,30 +2,143 @@ namespace Amirami\Localizator\Tests; +/** + * Class LocalizatorTest + * @package Amirami\Localizator\Tests + */ class LocalizatorTest extends TestCase { - public function testLocalizeCommandBasic() + /** + * @return void + */ + public function testLocalizeCommandBasic(): void { - // - } + $this->createTestView("{{ __('Localizator') }} {{ __('app.name') }}"); - public function testLocalizeCommandWithDefinedLanguages() - { - // + // Run localize command. + $this->artisan('localize') + ->assertExitCode(0); + + // Do created locale files exist? + self::assertDefaultLangFilesExist('en', ['app']); + self::assertJsonLangFilesExist('en'); + + // Do their contents match the expected results? + $enDefaultContents = $this->getDefaultLangContents('en', 'app'); + $enJsonContents = $this->getJsonLangContents('en'); + self::assertEquals(['name' => ''], $enDefaultContents); + self::assertEquals(['Localizator' => 'Localizator'], $enJsonContents); + + // Cleanup. + self::flushDirectories('lang', 'views'); } - public function testLocalizeCommandWithSortingJsonKeys() + /** + * @return void + */ + public function testLocalizeCommandWithDefinedLocales(): void { - // + $this->createTestView("{{ __('Localizator') }} {{ __('app.name') }}"); + + // Run localize command. + $this->artisan('localize', ['lang' => 'en,de']) + ->assertExitCode(0); + + // Do created locale files exist? + self::assertDefaultLangFilesExist(['en', 'de'], ['app']); + self::assertJsonLangFilesExist(['en', 'de']); + + // Do their contents match the expected results? + $enDefaultContents = $this->getDefaultLangContents('en', 'app'); + $enJsonContents = $this->getJsonLangContents('en'); + $deDefaultContents = $this->getDefaultLangContents('de', 'app'); + $deJsonContents = $this->getJsonLangContents('de'); + self::assertEquals(['name' => ''], $enDefaultContents); + self::assertEquals(['name' => ''], $deDefaultContents); + self::assertEquals(['Localizator' => 'Localizator'], $enJsonContents); + self::assertEquals(['Localizator' => ''], $deJsonContents); + + // Cleanup. + self::flushDirectories('lang', 'views'); } - public function testLocalizeCommandWithCustomDefinedPatterns() + /** + * @return void + */ + public function testLocalizeCommandWithSortingKeys(): void { - // + $this->createTestView("{{ __('Delete') }} {{ __('Cancel') }} {{ __('Login') }}", 'test-1'); + $this->createTestView("{{ __('auth.throttle') }} {{ __('auth.failed') }} {{ __('auth.password') }}", 'test-2'); + + // Run localize command. + $this->artisan('localize') + ->assertExitCode(0); + + // Do created locale files exist? + self::assertDefaultLangFilesExist('en', ['auth']); + self::assertJsonLangFilesExist('en'); + + // Do their contents match the expected results? + $enDefaultContents = $this->getDefaultLangContents('en', 'auth'); + $enJsonContents = $this->getJsonLangContents('en'); + + // Did it sort the translation keys like we expected? + self::assertEquals([ + 'failed' => '', + 'password' => '', + 'throttle' => '', + ], $enDefaultContents); + self::assertEquals([ + 'Cancel' => 'Cancel', + 'Delete' => 'Delete', + 'Login' => 'Login', + ], $enJsonContents); + + // Cleanup. + self::flushDirectories('lang', 'views'); } - public function testLocalizeCommandByMergingTheExistingTranslations() + /** + * @return void + */ + public function testLocalizeCommandByMergingTheExistingTranslations(): void { - // + $this->createTestView("{{ __('Delete') }} {{ __('Cancel') }} {{ __('Login') }}", 'test-1'); + $this->createTestView("{{ __('auth.throttle') }} {{ __('auth.failed') }} {{ __('auth.password') }}", 'test-2'); + + $this->createTestDefaultLangFile([ + 'password' => 'Das eingegebene Passwort ist nicht korrekt.', + ], 'auth', 'de'); + $this->createTestJsonLangFile([ + 'Login' => 'Anmelden', + 'Delete' => 'Löschen', + ], 'de'); + + // Run localize command. + $this->artisan('localize', ['lang' => 'de']) + ->assertExitCode(0); + + // Do created locale files exist? + self::assertDefaultLangFilesExist('de', ['auth']); + self::assertJsonLangFilesExist('de'); + + // Do their contents match the expected results? + $enDefaultContents = $this->getDefaultLangContents('de', 'auth'); + $enJsonContents = $this->getJsonLangContents('de'); + + // Did it preserve the already translated keys? + self::assertEquals([ + 'failed' => '', + 'password' => 'Das eingegebene Passwort ist nicht korrekt.', + 'throttle' => '', + ], $enDefaultContents); + self::assertEquals([ + 'Cancel' => '', + 'Delete' => 'Löschen', + 'Login' => 'Anmelden', + ], $enJsonContents); + + // Cleanup. + self::flushDirectories('lang', 'views'); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 2905d89..923b960 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,25 +3,19 @@ namespace Amirami\Localizator\Tests; use Amirami\Localizator\ServiceProvider; +use Amirami\Localizator\Tests\Concerns\CreatesTestFiles; +use Amirami\Localizator\Tests\Concerns\ImportsLangFiles; use Orchestra\Testbench\TestCase as Orchestra; use Symfony\Component\Finder\Finder; +/** + * Class TestCase + * @package Amirami\Localizator\Tests + */ class TestCase extends Orchestra { - /** - * @var string[] - */ - protected $searchDirectories = ['resources/views']; - - /** - * @var string[] - */ - protected $searchPatterns = ['*.php']; - - /** - * @var string[] - */ - protected $searchFunctions = ['__', 'trans', '@lang']; + use CreatesTestFiles; + use ImportsLangFiles; /** * Setup the test environment. @@ -33,13 +27,24 @@ public function setUp(): void parent::setUp(); } + /** + * This method is called after the last test of this test class is run. + * + * @return void + */ + public static function tearDownAfterClass(): void + { + // Flush one last time after all tests have finished running. + self::flushDirectories('lang', 'views'); + } + /** * Get package providers. * - * @param \Illuminate\Foundation\Application $app + * @param \Illuminate\Foundation\Application|\Illuminate\Contracts\Foundation\Application $app * @return array|string[] */ - protected function getPackageProviders($app) + protected function getPackageProviders($app): array { return [ ServiceProvider::class, @@ -49,38 +54,84 @@ protected function getPackageProviders($app) /** * Define environment setup. * - * @param \Illuminate\Foundation\Application $app - * + * @param \Illuminate\Foundation\Application|\Illuminate\Contracts\Foundation\Application $app * @return void */ - public function getEnvironmentSetUp($app) + public function getEnvironmentSetUp($app): void { $app->setBasePath(__DIR__ . DIRECTORY_SEPARATOR . 'Mock'); + } + + /** + * @param string $fileName + * @param string $message + * @return void + */ + protected static function assertLangFileExists(string $fileName, string $message = ''): void + { + static::assertFileExists( + resource_path('lang' . DIRECTORY_SEPARATOR . $fileName), + $message + ); + } - $app['config']->set('app.locale', 'en'); + /** + * @param string|string[] $locales + * @param array $fileNames + * @param string $message + */ + protected static function assertDefaultLangFilesExist($locales, array $fileNames, string $message = ''): void + { + $locales = is_array($locales) ? $locales : (array) $locales; - $app['config']->set('localizator.search.dirs', $this->searchDirectories); - $app['config']->set('localizator.search.patterns', $this->searchPatterns); - $app['config']->set('localizator.search.functions', $this->searchFunctions); + foreach ($locales as $locale) { + foreach ($fileNames as $fileName) { + static::assertLangFileExists($locale . DIRECTORY_SEPARATOR . "{$fileName}.php", $message); + } + } + } + + /** + * @param string|string[] $locales + * @param string $message + * @return void + */ + protected static function assertJsonLangFilesExist($locales, string $message = ''): void + { + $locales = is_array($locales) ? $locales : (array) $locales; + + foreach ($locales as $locale) { + static::assertLangFileExists("{$locale}.json", $message); + } } /** * Delete all files from selected directories in resources folder. * - * @param mixed ...$dirNames - * + * @param string ...$dirNames * @return void */ - protected function cleanDirectories(...$dirNames): void + protected static function flushDirectories(...$dirNames): void { $dirNames = array_map(static function ($dirName) { return resource_path($dirName); }, $dirNames); - $files = (new Finder())->in($dirNames)->files(); + $finder = (new Finder())->in($dirNames); + $directories = []; + + foreach ($finder as $fileInfo) { + if (file_exists($fileInfo->getRealPath())) { + if (is_dir($fileInfo->getRealPath())) { + $directories[] = $fileInfo; + } else { + unlink($fileInfo->getRealPath()); + } + } + } - foreach ($files as $file) { - unlink($file->getPathname()); + foreach ($directories as $directory) { + rmdir($directory->getRealPath()); } } }