From 234630c03a0dd035957021f184dbf448809682b1 Mon Sep 17 00:00:00 2001 From: Benaja Date: Sat, 13 Jan 2024 10:46:59 +0100 Subject: [PATCH 1/5] also import root language files like lang/de.json --- src/TranslationsManager.php | 66 ++++++++++++---- tests/Helpers.php | 31 ++++++++ tests/TranslationsManagerTest.php | 121 +++++++++++++++++++++--------- 3 files changed, 169 insertions(+), 49 deletions(-) create mode 100644 tests/Helpers.php diff --git a/src/TranslationsManager.php b/src/TranslationsManager.php index 4080470..d8374a0 100644 --- a/src/TranslationsManager.php +++ b/src/TranslationsManager.php @@ -25,6 +25,7 @@ public function getLocales(): array { $locales = collect(); + // get locales from direcotries foreach ($this->filesystem->directories(lang_path()) as $dir) { if (Str::contains($dir, 'vendor')) { continue; @@ -33,37 +34,72 @@ public function getLocales(): array $locales->push(basename($dir)); } + // get locales from json files in the root of the lang directory + collect($this->filesystem->files(lang_path()))->each(function ($file) use ($locales) { + // if (Str::contains($file->getFilename(), 'vendor')) { + // return; + // } + + if ($this->filesystem->extension($file) != 'json') { + return; + } + + if (! $locales->contains($file->getFilenameWithoutExtension())) { + $locales->push($file->getFilenameWithoutExtension()); + } + }); + return $locales->toArray(); } - public function getTranslations(string $local): array + public function getTranslations(string $locale): array { - if (blank($local)) { - $local = config('translations.source_language'); + if (blank($locale)) { + $locale = config('translations.source_language'); } - collect($this->filesystem->allFiles(lang_path($local))) + $translations = []; + $baseFileName = "{$locale}.json"; + + collect($this->filesystem->allFiles(lang_path($locale))) + ->map(function ($file) use ($locale) { + return $locale . DIRECTORY_SEPARATOR . $file->getFilename(); + }) + ->when($this->filesystem->exists(lang_path($baseFileName)), function ($collection) use ($baseFileName) { + return $collection->prepend($baseFileName); + }) ->filter(function ($file) { - return ! in_array($file->getFilename(), config('translations.exclude_files')); + foreach (config('translations.exclude_files') as $excludeFile) { + if (fnmatch($excludeFile, $file)) { + return false; + } + if (fnmatch($excludeFile, basename($file))) { + return false; + } + + return true; + } + + return ! in_array($file, config('translations.exclude_files')); }) ->filter(function ($file) { return $this->filesystem->extension($file) == 'php' || $this->filesystem->extension($file) == 'json'; }) - ->each(function ($file) { + ->each(function ($file) use (&$translations) { try { if ($this->filesystem->extension($file) == 'php') { - $this->translations[$file->getFilename()] = $this->filesystem->getRequire($file->getPathname()); + $translations[$file] = $this->filesystem->getRequire(lang_path($file)); } if ($this->filesystem->extension($file) == 'json') { - $this->translations[$file->getFilename()] = json_decode($this->filesystem->get($file), true); + $translations[$file] = json_decode($this->filesystem->get(lang_path($file)), true); } } catch (FileNotFoundException $e) { - $this->translations[$file->getFilename()] = []; + $translations[$file] = []; } }); - return $this->translations; + return $translations; } public function export(): void @@ -75,26 +111,26 @@ public function export(): void foreach ($phrasesTree as $locale => $groups) { foreach ($groups as $file => $phrases) { - $path = lang_path("$locale/$file"); + $path = lang_path("{$file}"); if (! $this->filesystem->isDirectory(dirname($path))) { - $this->filesystem->makeDirectory(dirname($path), 0755, true); + $this->filesystem->makeDirectory(dirname($path), 0o755, true); } if (! $this->filesystem->exists($path)) { - $this->filesystem->put($path, "filesystem->put($path, "filesystem->extension($path) == 'php') { try { - $this->filesystem->put($path, "filesystem->put($path, "error($e->getMessage()); } } if ($this->filesystem->extension($path) == 'json') { - $this->filesystem->put($path, json_encode($phrases, JSON_PRETTY_PRINT)); + $this->filesystem->put($path, json_encode($phrases, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); } } } diff --git a/tests/Helpers.php b/tests/Helpers.php new file mode 100644 index 0000000..85a1807 --- /dev/null +++ b/tests/Helpers.php @@ -0,0 +1,31 @@ +getLocales(); - expect($locales)->toBe(['en']); + expect($locales)->toBe(['de', 'en', 'fr']); }); it('returns the correct translations for a given locale', function () { - Config::set('translations.exclude_files', ['en']); - $filesystem = new FilesystemMock(); + createPhpLanguageFile(lang_path('en/auth.php'), [ + 'test' => 'Test', + ]); + createPhpLanguageFile(lang_path('en/validation.php'), [ + 'test' => 'Test1', + ]); + createJsonLangaueFile(lang_path('en.json'), [ + 'title' => 'My title', + ]); + + Config::set('translations.exclude_files', ['validation.php']); + Config::set('translations.source_language', 'en'); + $filesystem = new Filesystem(); + + $translationsManager = new TranslationsManager($filesystem); + $translations = $translationsManager->getTranslations('en'); + expect($translations)->toBe([ + 'en.json' => ['title' => 'My title'], + 'en/auth.php' => ['test' => 'Test'] + ]); + + $translations = $translationsManager->getTranslations(''); + expect($translations)->toBe([ + 'en.json' => ['title' => 'My title'], + 'en/auth.php' => ['test' => 'Test'] + ]); +}); + +it('it excludes the correct files', function () { + createPhpLanguageFile(lang_path('en/auth.php'), [ + 'test' => 'Test', + ]); + createPhpLanguageFile(lang_path('en/validation.php'), [ + 'test' => 'Test1', + ]); + createJsonLangaueFile(lang_path('en.json'), [ + 'title' => 'My title', + ]); + + Config::set('translations.exclude_files', ['*.php']); + Config::set('translations.source_language', 'en'); + $filesystem = new Filesystem(); $translationsManager = new TranslationsManager($filesystem); $translations = $translationsManager->getTranslations('en'); - expect($translations)->toBe(['auth.php' => [], 'validation.json' => []]); + expect($translations)->toBe([ + 'en.json' => ['title' => 'My title'], + ]); $translations = $translationsManager->getTranslations(''); - expect($translations)->toBe(['auth.php' => [], 'validation.json' => []]); + expect($translations)->toBe([ + 'en.json' => ['title' => 'My title'], + ]); }); test('export creates a new translation file with the correct content', function () { - $filesystem = new FilesystemMock(); + $filesystem = new Filesystem(); + createDirectoryIfNotExits(lang_path('en/auth.php')); $translation = Translation::factory([ 'source' => true, @@ -44,7 +102,7 @@ ])->has(Phrase::factory()->state([ 'phrase_id' => null, 'translation_file_id' => TranslationFile::factory([ - 'name' => 'auth', + 'name' => 'en/auth', 'extension' => 'php', ]), ]))->create(); @@ -52,42 +110,38 @@ $translationsManager = new TranslationsManager($filesystem); $translationsManager->export(); - $fileName = $translation->phrases[0]->file->name.'.'.$translation->phrases[0]->file->extension; - $fileNameInDisk = File::allFiles(lang_path($translation->language->code))[0]->getFilename(); + $fileName = lang_path($translation->phrases[0]->file->name . '.' . $translation->phrases[0]->file->extension); + $fileNameInDisk = File::allFiles(lang_path($translation->language->code))[0]->getPathname(); expect($fileName)->toBe($fileNameInDisk) - ->and(File::get(lang_path($translation->language->code.DIRECTORY_SEPARATOR.$fileName))) - ->toBe("phrases->pluck('value', 'key')->toArray(), VarExporter::TRAILING_COMMA_IN_ARRAY).';'.PHP_EOL); + ->and(File::get($fileName)) + ->toBe("phrases->pluck('value', 'key')->toArray(), VarExporter::TRAILING_COMMA_IN_ARRAY) . ';' . PHP_EOL); File::deleteDirectory(lang_path()); }); test('export can handle PHP translation files', function () { - App::useLangPath(__DIR__.'lang_test'); + createPhpLanguageFile('en/test.php', ['accepted' => 'The :attribute must be accepted.']); - if (! File::exists(lang_path('en'.DIRECTORY_SEPARATOR.'test.php'))) { - File::makeDirectory(lang_path('en'), 0755, true); - File::put(lang_path('en'.DIRECTORY_SEPARATOR.'test.php'), " 'The :attribute must be accepted.'], VarExporter::TRAILING_COMMA_IN_ARRAY).';'.PHP_EOL); - } - - $filesystem = new FilesystemMock(); + $filesystem = new Filesystem(); $translation = Translation::factory() ->has(Phrase::factory() - ->for(TranslationFile::factory()->state(['name' => 'test', 'extension' => 'php']), 'file') + ->for(TranslationFile::factory()->state(['name' => 'en/test', 'extension' => 'php']), 'file') ->state([ 'key' => 'accepted', 'value' => 'The :attribute must be accepted.', 'phrase_id' => null, ])) - ->has(Language::factory(['code' => 'en'])) + ->for(Language::factory()->state(['code' => 'en'])) ->create(); + $translationsManager = new TranslationsManager($filesystem); $translationsManager->export(); - $path = lang_path('en'.DIRECTORY_SEPARATOR.'test.php'); - $pathInDisk = lang_path($translation->language->code.DIRECTORY_SEPARATOR.'test.php'); + $path = lang_path('en' . DIRECTORY_SEPARATOR . 'test.php'); + $pathInDisk = lang_path($translation->language->code . DIRECTORY_SEPARATOR . 'test.php'); expect(File::get($path))->toBe(File::get($pathInDisk)); @@ -95,30 +149,25 @@ }); test('export can handle JSON translation files', function () { - App::useLangPath(__DIR__.'lang_test'); - - if (! File::exists(lang_path('en'.DIRECTORY_SEPARATOR.'test.json'))) { - File::makeDirectory(lang_path('en'), 0755, true); - File::put(lang_path('en'.DIRECTORY_SEPARATOR.'test.json'), json_encode(['accepted' => 'The :attribute must be accepted.'], JSON_PRETTY_PRINT)); - } - $filesystem = new FilesystemMock(); + createJsonLangaueFile('en/test.json', ['accepted' => 'The :attribute must be accepted.']); + $filesystem = new Filesystem(); $translation = Translation::factory() ->has(Phrase::factory() - ->for(TranslationFile::factory()->state(['name' => 'test', 'extension' => 'json']), 'file') + ->for(TranslationFile::factory()->state(['name' => 'en/test', 'extension' => 'json']), 'file') ->state([ 'key' => 'accepted', 'value' => 'The :attribute must be accepted.', 'phrase_id' => null, ])) - ->has(Language::factory(['code' => 'en'])) + ->for(Language::factory()->state(['code' => 'en'])) ->create(); $translationsManager = new TranslationsManager($filesystem); $translationsManager->export(); - $path = lang_path('en'.DIRECTORY_SEPARATOR.'test.json'); - $pathInDisk = lang_path($translation->language->code.DIRECTORY_SEPARATOR.'test.json'); + $path = lang_path('en' . DIRECTORY_SEPARATOR . 'test.json'); + $pathInDisk = lang_path($translation->language->code . DIRECTORY_SEPARATOR . 'test.json'); expect(File::get($path))->toBe(File::get($pathInDisk)); @@ -135,3 +184,7 @@ $parametersEmpty = $translationsManager->getPhraseParameters(''); expect($parametersEmpty)->toBe(null); }); + +afterEach(function () { + File::deleteDirectory(lang_path()); +}); From fbed735be0553245225b5ac757d78ed3f88cbffc Mon Sep 17 00:00:00 2001 From: Benaja Date: Sat, 13 Jan 2024 11:23:52 +0100 Subject: [PATCH 2/5] import all phrases from source --- .../Commands/ImportTranslationsCommand.php | 25 ++++++++++++++++++- src/Livewire/PhraseList.php | 8 ++++-- src/Livewire/TranslationsList.php | 4 +-- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/Console/Commands/ImportTranslationsCommand.php b/src/Console/Commands/ImportTranslationsCommand.php index b0262d1..0330036 100644 --- a/src/Console/Commands/ImportTranslationsCommand.php +++ b/src/Console/Commands/ImportTranslationsCommand.php @@ -91,6 +91,29 @@ public function syncTranslations(Translation $translation, string $locale): void $this->syncPhrases($translation, $key, $value, $locale, $file); } } + + if ($locale === config('translations.source_language')) { + return; + } + + $this->syncMissingTranslations($translation, $locale); + } + + public function syncMissingTranslations(Translation $source, string $locale): void + { + $language = Language::where('code', $locale)->first(); + $translation = Translation::firstOrCreate([ + 'language_id' => $language->id, + 'source' => false, + ]); + + $source->load('phrases.translation', 'phrases.file'); + + $source->phrases->each(function ($phrase) use ($translation, $locale) { + if (! $translation->phrases()->where('key', $phrase->key)->first()) { + $this->syncPhrases($phrase->translation, $phrase->key, '', $locale, $phrase->file->name.'.'.$phrase->file->extension); + } + }); } public function syncPhrases(Translation $source, $key, $value, $locale, $file): void @@ -102,7 +125,7 @@ public function syncPhrases(Translation $source, $key, $value, $locale, $file): $language = Language::where('code', $locale)->first(); if (! $language) { - $this->error(PHP_EOL."Language with code $locale not found"); + $this->error(PHP_EOL."Language with code {$locale} not found"); exit; } diff --git a/src/Livewire/PhraseList.php b/src/Livewire/PhraseList.php index b15a711..35f0597 100644 --- a/src/Livewire/PhraseList.php +++ b/src/Livewire/PhraseList.php @@ -68,9 +68,13 @@ public function getPhrases(): LengthAwarePaginator }) ->when($this->status, function (Builder $query) { if ($this->status == 1) { - $query->whereNotNull('value'); + $query->where(fn($query) => $query + ->where('value', '<>', '') + ->orWhereNull('value')); } elseif ($this->status == 2) { - $query->whereNull('value'); + $query->where(fn($query) => $query + ->where('value', '=', '') + ->orWhereNull('value')); } }) ->paginate($this->perPage)->onEachSide(0); diff --git a/src/Livewire/TranslationsList.php b/src/Livewire/TranslationsList.php index 26e876a..1dc5ba4 100644 --- a/src/Livewire/TranslationsList.php +++ b/src/Livewire/TranslationsList.php @@ -38,8 +38,8 @@ public function getTranslations(): LengthAwarePaginator public function getTranslationProgressPercentage(Translation $translation): float { $phrases = $translation->phrases()->toBase() - ->selectRaw('COUNT(CASE WHEN value IS NOT NULL THEN 1 END) AS translated') - ->selectRaw('COUNT(CASE WHEN value IS NULL THEN 1 END) AS untranslated') + ->selectRaw('COUNT(CASE WHEN value IS NOT NULL AND value <> \'\' THEN 1 END) AS translated') + ->selectRaw('COUNT(CASE WHEN value IS NULL OR value <> \'\' THEN 1 END) AS untranslated') ->selectRaw('COUNT(*) AS total') ->first(); From f9e2ce0e2b87cb9cde467f35827f0890877c21cb Mon Sep 17 00:00:00 2001 From: Benaja Date: Mon, 15 Jan 2024 14:03:35 +0100 Subject: [PATCH 3/5] include file name in translation key --- config/translations.php | 2 ++ database/factories/TranslationFileFactory.php | 1 + ...add_is_root_to_translation_files_table.php | 31 +++++++++++++++++++ .../Commands/ImportTranslationsCommand.php | 23 +++++++++++--- src/Livewire/Modals/CreateTranslation.php | 15 +++++++-- src/Models/TranslationFile.php | 4 +++ src/TranslationsManager.php | 28 ++++++++++++++--- tests/Helpers.php | 4 +++ tests/TestCase.php | 7 +++-- tests/TranslationsManagerTest.php | 28 ++++++++--------- 10 files changed, 115 insertions(+), 28 deletions(-) create mode 100644 database/migrations/2018_08_08_100001_add_is_root_to_translation_files_table.php diff --git a/config/translations.php b/config/translations.php index 8f58a02..58f4ff8 100644 --- a/config/translations.php +++ b/config/translations.php @@ -71,4 +71,6 @@ | */ 'database_connection' => env('TRANSLATIONS_DB_CONNECTION', null), + + 'include_file_in_key' => env('TRANSLATIONS_INCLUDE_FILE_IN_KEY', false), ]; diff --git a/database/factories/TranslationFileFactory.php b/database/factories/TranslationFileFactory.php index 7f65526..8ded611 100644 --- a/database/factories/TranslationFileFactory.php +++ b/database/factories/TranslationFileFactory.php @@ -14,6 +14,7 @@ public function definition(): array return [ 'name' => $this->faker->randomElement(['app', 'auth', 'pagination', 'passwords', 'validation']), 'extension' => $this->faker->randomElement(['json', 'php']), + 'is_root' => false, ]; } } diff --git a/database/migrations/2018_08_08_100001_add_is_root_to_translation_files_table.php b/database/migrations/2018_08_08_100001_add_is_root_to_translation_files_table.php new file mode 100644 index 0000000..920d7fa --- /dev/null +++ b/database/migrations/2018_08_08_100001_add_is_root_to_translation_files_table.php @@ -0,0 +1,31 @@ +boolean('is_root')->default(false)->after('extension'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('ltu_translation_files', function (Blueprint $table) { + $table->dropColumn('is_root'); + }); + } +}; diff --git a/src/Console/Commands/ImportTranslationsCommand.php b/src/Console/Commands/ImportTranslationsCommand.php index 0330036..5b7dc66 100644 --- a/src/Console/Commands/ImportTranslationsCommand.php +++ b/src/Console/Commands/ImportTranslationsCommand.php @@ -5,6 +5,7 @@ use Illuminate\Console\Command; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Schema; +use Illuminate\Support\Str; use Outhebox\LaravelTranslations\Models\Language; use Outhebox\LaravelTranslations\Models\Phrase; use Outhebox\LaravelTranslations\Models\Translation; @@ -40,14 +41,14 @@ public function handle(): void $this->importLanguages(); if ($this->option('fresh') && $this->confirm('Are you sure you want to truncate all translations and phrases?')) { - $this->info('Truncating translations and phrases...'.PHP_EOL); + $this->info('Truncating translations and phrases...' . PHP_EOL); $this->truncateTables(); } $translation = $this->createOrGetSourceLanguage(); - $this->info('Importing translations...'.PHP_EOL); + $this->info('Importing translations...' . PHP_EOL); $this->withProgressBar($this->manager->getLocales(), function ($locale) use ($translation) { $this->syncTranslations($translation, $locale); @@ -59,7 +60,7 @@ public function createOrGetSourceLanguage(): Translation $language = Language::where('code', config('translations.source_language'))->first(); if (! $language) { - $this->error('Language with code '.config('translations.source_language').' not found'.PHP_EOL); + $this->error('Language with code ' . config('translations.source_language') . ' not found' . PHP_EOL); exit; } @@ -111,7 +112,15 @@ public function syncMissingTranslations(Translation $source, string $locale): vo $source->phrases->each(function ($phrase) use ($translation, $locale) { if (! $translation->phrases()->where('key', $phrase->key)->first()) { - $this->syncPhrases($phrase->translation, $phrase->key, '', $locale, $phrase->file->name.'.'.$phrase->file->extension); + $fileName = $phrase->file->name . '.' . $phrase->file->extension; + + if ($phrase->file->name === config('translations.source_language')) { + $fileName = Str::replaceStart(config('translations.source_language') . '.', "{$locale}.", $fileName); + } else { + $fileName = Str::replaceStart(config('translations.source_language') . '/', "{$locale}/", $fileName); + } + + $this->syncPhrases($phrase->translation, $phrase->key, '', $locale, $fileName); } }); } @@ -125,7 +134,7 @@ public function syncPhrases(Translation $source, $key, $value, $locale, $file): $language = Language::where('code', $locale)->first(); if (! $language) { - $this->error(PHP_EOL."Language with code {$locale} not found"); + $this->error(PHP_EOL . "Language with code {$locale} not found"); exit; } @@ -135,11 +144,15 @@ public function syncPhrases(Translation $source, $key, $value, $locale, $file): 'source' => config('translations.source_language') === $locale, ]); + $isRoot = $file === $locale . '.json' || $file === $locale . '.php'; $translationFile = TranslationFile::firstOrCreate([ 'name' => pathinfo($file, PATHINFO_FILENAME), 'extension' => pathinfo($file, PATHINFO_EXTENSION), + 'is_root' => $isRoot, ]); + $key = config('translations.include_file_in_key') && ! $isRoot ? "{$translationFile->name}.{$key}" : $key; + $translation->phrases()->updateOrCreate([ 'key' => $key, 'group' => $translationFile->name, diff --git a/src/Livewire/Modals/CreateTranslation.php b/src/Livewire/Modals/CreateTranslation.php index 9a289ef..4f64639 100644 --- a/src/Livewire/Modals/CreateTranslation.php +++ b/src/Livewire/Modals/CreateTranslation.php @@ -6,6 +6,7 @@ use LivewireUI\Modal\ModalComponent; use Outhebox\LaravelTranslations\Models\Language; use Outhebox\LaravelTranslations\Models\Translation; +use Outhebox\LaravelTranslations\Models\TranslationFile; use WireUi\Traits\Actions; class CreateTranslation extends ModalComponent @@ -46,13 +47,23 @@ public function create(): void $sourceTranslation = Translation::where('source', true)->first(); foreach ($sourceTranslation->phrases()->with('file')->get() as $sourcePhrase) { + $file = $sourcePhrase->file; + + if ($file->is_root) { + $file = TranslationFile::firstOrCreate([ + 'name' => $translation->language->code, + 'extension' => $file->extension, + 'is_root' => true, + ]); + } + $translation->phrases()->create([ 'value' => null, 'key' => $sourcePhrase->key, - 'group' => $sourcePhrase->group, + 'group' => $file->name, 'phrase_id' => $sourcePhrase->id, 'parameters' => $sourcePhrase->parameters, - 'translation_file_id' => $sourcePhrase->file->id, + 'translation_file_id' => $file->id, ]); } diff --git a/src/Models/TranslationFile.php b/src/Models/TranslationFile.php index 13f85a4..eb350ff 100644 --- a/src/Models/TranslationFile.php +++ b/src/Models/TranslationFile.php @@ -19,6 +19,10 @@ class TranslationFile extends Model public $timestamps = false; + protected $casts = [ + 'is_root' => 'boolean', + ]; + public function phrases(): HasMany { return $this->hasMany(Phrase::class); diff --git a/src/TranslationsManager.php b/src/TranslationsManager.php index d8374a0..c79eae9 100644 --- a/src/TranslationsManager.php +++ b/src/TranslationsManager.php @@ -59,14 +59,20 @@ public function getTranslations(string $locale): array } $translations = []; - $baseFileName = "{$locale}.json"; + $rootFileName = "{$locale}.json"; - collect($this->filesystem->allFiles(lang_path($locale))) + $files = []; + + if ($this->filesystem->exists(lang_path($locale))) { + $files = $this->filesystem->allFiles(lang_path($locale)); + } + + collect($files) ->map(function ($file) use ($locale) { return $locale . DIRECTORY_SEPARATOR . $file->getFilename(); }) - ->when($this->filesystem->exists(lang_path($baseFileName)), function ($collection) use ($baseFileName) { - return $collection->prepend($baseFileName); + ->when($this->filesystem->exists(lang_path($rootFileName)), function ($collection) use ($rootFileName) { + return $collection->prepend($rootFileName); }) ->filter(function ($file) { foreach (config('translations.exclude_files') as $excludeFile) { @@ -142,7 +148,19 @@ protected function buildPhrasesTree($phrases, $locale): array $tree = []; foreach ($phrases as $phrase) { - Arr::set($tree[$locale][$phrase->file->file_name], $phrase->key, $phrase->value); + if ($phrase->file->is_root) { + $fileName = $phrase->file->file_name; + } else { + $fileName = "{$locale}/{$phrase->file->file_name}"; + } + + $key = $phrase->key; + + if (config('translations.include_file_in_key') && !$phrase->file->is_root) { + $key = Str::replaceStart($phrase->file->name . '.', '', $key); + } + + Arr::set($tree[$locale][$fileName], $key, $phrase->value); } return $tree; diff --git a/tests/Helpers.php b/tests/Helpers.php index 85a1807..6da096c 100644 --- a/tests/Helpers.php +++ b/tests/Helpers.php @@ -18,6 +18,8 @@ function createDirectoryIfNotExits($path) function createPhpLanguageFile($path, array $content) { + $path = lang_path($path); + createDirectoryIfNotExits($path); File::put($path, " 'Outhebox\\LaravelTranslations\\Database\\Factories\\'.class_basename($modelName).'Factory' + fn(string $modelName) => 'Outhebox\\LaravelTranslations\\Database\\Factories\\' . class_basename($modelName) . 'Factory' ); } @@ -33,7 +33,10 @@ public function getEnvironmentSetUp($app): void { Schema::dropAllTables(); - $migration = include __DIR__.'/../database/migrations/2018_08_08_100000_create_translations_tables.php'; + $migration = include __DIR__ . '/../database/migrations/2018_08_08_100000_create_translations_tables.php'; + $migration->up(); + + $migration = include __DIR__ . '/../database/migrations/2018_08_08_100001_add_is_root_to_translation_files_table.php'; $migration->up(); } } diff --git a/tests/TranslationsManagerTest.php b/tests/TranslationsManagerTest.php index b472b4f..85d501a 100644 --- a/tests/TranslationsManagerTest.php +++ b/tests/TranslationsManagerTest.php @@ -20,12 +20,12 @@ it('returns the correct list of locales', function () { $filesystem = new Filesystem(); // Create the source language directory - createPhpLanguageFile(lang_path('en/auth.php'), []); - createJsonLangaueFile(lang_path('en.json'), []); + createPhpLanguageFile('en/auth.php', []); + createJsonLangaueFile('en.json', []); - createJsonLangaueFile(lang_path('fr.json'), []); + createJsonLangaueFile('fr.json', []); - createPhpLanguageFile(lang_path('de/validation.php'), []); + createPhpLanguageFile('de/validation.php', []); $translationsManager = new TranslationsManager($filesystem); $locales = $translationsManager->getLocales(); @@ -34,13 +34,13 @@ }); it('returns the correct translations for a given locale', function () { - createPhpLanguageFile(lang_path('en/auth.php'), [ + createPhpLanguageFile('en/auth.php', [ 'test' => 'Test', ]); - createPhpLanguageFile(lang_path('en/validation.php'), [ + createPhpLanguageFile('en/validation.php', [ 'test' => 'Test1', ]); - createJsonLangaueFile(lang_path('en.json'), [ + createJsonLangaueFile('en.json', [ 'title' => 'My title', ]); @@ -63,13 +63,13 @@ }); it('it excludes the correct files', function () { - createPhpLanguageFile(lang_path('en/auth.php'), [ + createPhpLanguageFile('en/auth.php', [ 'test' => 'Test', ]); - createPhpLanguageFile(lang_path('en/validation.php'), [ + createPhpLanguageFile('en/validation.php', [ 'test' => 'Test1', ]); - createJsonLangaueFile(lang_path('en.json'), [ + createJsonLangaueFile('en.json', [ 'title' => 'My title', ]); @@ -102,7 +102,7 @@ ])->has(Phrase::factory()->state([ 'phrase_id' => null, 'translation_file_id' => TranslationFile::factory([ - 'name' => 'en/auth', + 'name' => 'auth', 'extension' => 'php', ]), ]))->create(); @@ -110,7 +110,7 @@ $translationsManager = new TranslationsManager($filesystem); $translationsManager->export(); - $fileName = lang_path($translation->phrases[0]->file->name . '.' . $translation->phrases[0]->file->extension); + $fileName = lang_path("en/" . $translation->phrases[0]->file->name . '.' . $translation->phrases[0]->file->extension); $fileNameInDisk = File::allFiles(lang_path($translation->language->code))[0]->getPathname(); expect($fileName)->toBe($fileNameInDisk) @@ -127,7 +127,7 @@ $translation = Translation::factory() ->has(Phrase::factory() - ->for(TranslationFile::factory()->state(['name' => 'en/test', 'extension' => 'php']), 'file') + ->for(TranslationFile::factory()->state(['name' => 'test', 'extension' => 'php']), 'file') ->state([ 'key' => 'accepted', 'value' => 'The :attribute must be accepted.', @@ -154,7 +154,7 @@ $translation = Translation::factory() ->has(Phrase::factory() - ->for(TranslationFile::factory()->state(['name' => 'en/test', 'extension' => 'json']), 'file') + ->for(TranslationFile::factory()->state(['name' => 'test', 'extension' => 'json']), 'file') ->state([ 'key' => 'accepted', 'value' => 'The :attribute must be accepted.', From 8bdbb7e9abac20fd68d472f62e994a2883eaeabb Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Fri, 19 Jan 2024 19:13:43 +0400 Subject: [PATCH 4/5] Fix conflicts --- composer.json | 2 + database/factories/LanguageFactory.php | 2 +- database/factories/TranslationFileFactory.php | 9 +- ...add_is_root_to_translation_files_table.php | 14 +- src/Actions/CopyPhrasesFromSourceAction.php | 15 +- src/Actions/SyncPhrasesAction.php | 5 + .../Controllers/TranslationController.php | 3 +- src/TranslationsManager.php | 6 +- src/TranslationsUIServiceProvider.php | 1 + tests/Helpers.php | 12 +- .../AuthenticatedSessionControllerTest.php | 115 +++++++-------- .../Auth/InvitationAcceptControllerTest.php | 92 +++++------- .../Auth/NewPasswordControllerTest.php | 123 +++++++--------- .../Auth/PasswordResetLinkControllerTest.php | 68 ++++----- .../Controllers/ContributorControllerTest.php | 75 ++++------ .../Http/Controllers/PhraseControllerTest.php | 53 +++++++ .../Controllers/ProfileControllerTest.php | 134 ++++++++---------- .../SourcePhraseControllerTest.php | 78 ++++++++++ .../Controllers/TranslationControllerTest.php | 51 +++++++ tests/Pest.php | 5 + tests/TranslationsManagerTest.php | 3 +- 21 files changed, 481 insertions(+), 385 deletions(-) create mode 100644 tests/Http/Controllers/PhraseControllerTest.php create mode 100644 tests/Http/Controllers/SourcePhraseControllerTest.php create mode 100644 tests/Http/Controllers/TranslationControllerTest.php create mode 100644 tests/Pest.php diff --git a/composer.json b/composer.json index 71913ca..535a325 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,8 @@ "nunomaduro/collision": "^7.0", "orchestra/testbench": "^8.0", "pestphp/pest": "^2.18", + "pestphp/pest-plugin-faker": "^2.0", + "pestphp/pest-plugin-laravel": "^2.2", "phpstan/extension-installer": "^1.1", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-phpunit": "^1.0", diff --git a/database/factories/LanguageFactory.php b/database/factories/LanguageFactory.php index 85405a1..b5ecd68 100644 --- a/database/factories/LanguageFactory.php +++ b/database/factories/LanguageFactory.php @@ -12,9 +12,9 @@ class LanguageFactory extends Factory public function definition(): array { return [ + 'rtl' => $this->faker->boolean(), 'code' => $this->faker->randomElement(['en', 'nl', 'fr', 'de', 'es', 'it', 'pt', 'ru', 'ja', 'zh']), 'name' => $this->faker->randomElement(['English', 'Dutch', 'French', 'German', 'Spanish', 'Italian', 'Portuguese', 'Russian', 'Japanese', 'Chinese']), - 'rtl' => $this->faker->boolean(), ]; } } diff --git a/database/factories/TranslationFileFactory.php b/database/factories/TranslationFileFactory.php index 011442d..5309dcd 100644 --- a/database/factories/TranslationFileFactory.php +++ b/database/factories/TranslationFileFactory.php @@ -13,8 +13,15 @@ public function definition(): array { return [ 'name' => $this->faker->randomElement(['app', 'auth', 'pagination', 'passwords', 'validation']), - 'extension' => $this->faker->randomElement(['json', 'php']), + 'extension' => 'php', 'is_root' => false, ]; } + + public function json(): self + { + return $this->state([ + 'extension' => 'json', + ]); + } } diff --git a/database/migrations/add_is_root_to_translation_files_table.php b/database/migrations/add_is_root_to_translation_files_table.php index 7b32984..b5af6ab 100644 --- a/database/migrations/add_is_root_to_translation_files_table.php +++ b/database/migrations/add_is_root_to_translation_files_table.php @@ -6,24 +6,14 @@ return new class() extends Migration { - /** - * Run the migrations. - * - * @return void - */ - public function up() + public function up(): void { Schema::table('ltu_translation_files', function (Blueprint $table) { $table->boolean('is_root')->default(false)->after('extension'); }); } - /** - * Reverse the migrations. - * - * @return void - */ - public function down() + public function down(): void { Schema::table('ltu_translation_files', function (Blueprint $table) { $table->dropColumn('is_root'); diff --git a/src/Actions/CopyPhrasesFromSourceAction.php b/src/Actions/CopyPhrasesFromSourceAction.php index b9bd34d..cc70f7e 100644 --- a/src/Actions/CopyPhrasesFromSourceAction.php +++ b/src/Actions/CopyPhrasesFromSourceAction.php @@ -3,6 +3,7 @@ namespace Outhebox\TranslationsUI\Actions; use Outhebox\TranslationsUI\Models\Translation; +use Outhebox\TranslationsUI\Models\TranslationFile; class CopyPhrasesFromSourceAction { @@ -11,14 +12,24 @@ public static function execute(Translation $translation): void $sourceTranslation = Translation::where('source', true)->first(); $sourceTranslation->phrases()->with('file')->get()->each(function ($sourcePhrase) use ($translation) { + $file = $sourcePhrase->file; + + if ($file->is_root) { + $file = TranslationFile::firstOrCreate([ + 'is_root' => true, + 'extension' => $file->extension, + 'name' => $translation->language->code, + ]); + } + $translation->phrases()->create([ 'value' => null, 'uuid' => str()->uuid(), 'key' => $sourcePhrase->key, - 'group' => $sourcePhrase->group, + 'group' => $file->name, 'phrase_id' => $sourcePhrase->id, 'parameters' => $sourcePhrase->parameters, - 'translation_file_id' => $sourcePhrase->file->id, + 'translation_file_id' => $file->id, ]); }); } diff --git a/src/Actions/SyncPhrasesAction.php b/src/Actions/SyncPhrasesAction.php index 993295e..36ac804 100644 --- a/src/Actions/SyncPhrasesAction.php +++ b/src/Actions/SyncPhrasesAction.php @@ -25,11 +25,16 @@ public static function execute(Translation $source, $key, $value, $locale, $file 'source' => config('translations.source_language') === $locale, ]); + $isRoot = $file === $locale.'.json' || $file === $locale.'.php'; + $translationFile = TranslationFile::firstOrCreate([ 'name' => pathinfo($file, PATHINFO_FILENAME), 'extension' => pathinfo($file, PATHINFO_EXTENSION), + 'is_root' => $isRoot, ]); + $key = config('translations.include_file_in_key') && ! $isRoot ? "{$translationFile->name}.{$key}" : $key; + $translation->phrases()->updateOrCreate([ 'key' => $key, 'group' => $translationFile->name, diff --git a/src/Http/Controllers/TranslationController.php b/src/Http/Controllers/TranslationController.php index 97adf16..8293137 100644 --- a/src/Http/Controllers/TranslationController.php +++ b/src/Http/Controllers/TranslationController.php @@ -72,8 +72,7 @@ public function store(Request $request): RedirectResponse 'languages' => 'required|array', ]); - $selectedLanguageIds = $request->input('languages'); - $languages = Language::whereIn('id', $selectedLanguageIds)->get(); + $languages = Language::whereIn('id', $request->input('languages'))->get(); foreach ($languages as $language) { CreateTranslationForLanguageAction::execute($language); diff --git a/src/TranslationsManager.php b/src/TranslationsManager.php index 1437362..3ddd67f 100644 --- a/src/TranslationsManager.php +++ b/src/TranslationsManager.php @@ -12,7 +12,7 @@ class TranslationsManager { public function __construct( - protected Filesystem $filesystem, + protected Filesystem $filesystem ) { } @@ -106,10 +106,10 @@ public function export(): void foreach ($phrasesTree as $locale => $groups) { foreach ($groups as $file => $phrases) { - $path = lang_path($file); + $path = lang_path("$locale/$file"); if (! $this->filesystem->isDirectory(dirname($path))) { - $this->filesystem->makeDirectory(dirname($path), 0o755, true); + $this->filesystem->makeDirectory(dirname($path), 0755, true); } if (! $this->filesystem->exists($path)) { diff --git a/src/TranslationsUIServiceProvider.php b/src/TranslationsUIServiceProvider.php index 4e0af77..24364eb 100644 --- a/src/TranslationsUIServiceProvider.php +++ b/src/TranslationsUIServiceProvider.php @@ -31,6 +31,7 @@ public function configurePackage(Package $package): void 'create_contributors_table', 'create_contributor_languages_table', 'create_invites_table', + 'add_is_root_to_translation_files_table', ]) ->hasCommands([ PublishCommand::class, diff --git a/tests/Helpers.php b/tests/Helpers.php index 6da096c..97a6e01 100644 --- a/tests/Helpers.php +++ b/tests/Helpers.php @@ -1,10 +1,10 @@ get(route('ltu.login')) - ->assertStatus(200); - - } - - /** @test */ - public function login_request_will_validate_email(): void - { - $response = $this->post(route('ltu.login.attempt'), [ - 'email' => 'not-an-email', - 'password' => 'password', - ])->assertRedirect(route('ltu.login')); - - $this->assertInstanceOf(ValidationException::class, $response->exception); - } - - /** @test */ - public function login_request_will_validate_password(): void - { - $response = $this->post(route('ltu.login.attempt'), [ - 'email' => $this->owner->email, - 'password' => 'what-is-my-password', - ])->assertSessionHasErrors(); - - $this->assertInstanceOf(ValidationException::class, $response->exception); - } - - /** @test */ - public function login_request_will_authenticate_user(): void - { - $this->withoutExceptionHandling(); - - $user = Contributor::factory([ - 'role' => RoleEnum::owner, - 'password' => Hash::make('password'), - ])->create(); - - $this->post(route('ltu.login.attempt'), [ - 'email' => $user->email, - 'password' => 'password', - ])->assertRedirect(route('ltu.translation.index')); - } - - /** @test */ - public function authenticated_users_can_access_dashboard(): void - { - $this->actingAs($this->owner, 'translations') - ->get(route('ltu.login')) - ->assertRedirect(route('ltu.translation.index')); - } - /** @test */ - public function authenticated_users_can_logout(): void - { - $this->actingAs($this->owner, 'translations') - ->get(route('ltu.logout')) - ->assertRedirect(route('ltu.login')); - } -} +it('login page can be rendered', function () { + $this->get(route('ltu.login')) + ->assertStatus(200); +}); + +it('login request will validate email', function () { + $response = $this->post(route('ltu.login.attempt'), [ + 'email' => 'not-an-email', + 'password' => 'password', + ])->assertRedirect(route('ltu.login')); + + expect($response->exception)->toBeInstanceOf(ValidationException::class); +}); + +it('login request will validate password', function () { + $response = $this->post(route('ltu.login.attempt'), [ + 'email' => $this->owner->email, + 'password' => 'what-is-my-password', + ])->assertSessionHasErrors(); + + expect($response->exception)->toBeInstanceOf(ValidationException::class); +}); + +it('login request will authenticate user', function () { + $this->withoutExceptionHandling(); + + $user = Contributor::factory([ + 'role' => RoleEnum::owner, + 'password' => Hash::make('password'), + ])->create(); + + $this->post(route('ltu.login.attempt'), [ + 'email' => $user->email, + 'password' => 'password', + ])->assertRedirect(route('ltu.translation.index')); +}); + +it('authenticated users can access dashboard', function () { + $this->actingAs($this->owner, 'translations') + ->get(route('ltu.login')) + ->assertRedirect(route('ltu.translation.index')); +}); + +it('authenticated users can logout', function () { + $this->actingAs($this->owner, 'translations') + ->get(route('ltu.logout')) + ->assertRedirect(route('ltu.login')); +}); diff --git a/tests/Http/Controllers/Auth/InvitationAcceptControllerTest.php b/tests/Http/Controllers/Auth/InvitationAcceptControllerTest.php index 0ae4684..26362a4 100644 --- a/tests/Http/Controllers/Auth/InvitationAcceptControllerTest.php +++ b/tests/Http/Controllers/Auth/InvitationAcceptControllerTest.php @@ -1,71 +1,55 @@ create(); - $this->get(route('ltu.invitation.accept', ['token' => $invite->token])) - ->assertStatus(200); - } +test('invitation accept page can be rendered', function () { + $invite = Invite::factory()->create(); - /** @test */ - public function invitation_accept_page_returns_404_if_token_is_invalid() - { - $this->get(route('ltu.invitation.accept', ['token' => 'invalid-token'])) - ->assertStatus(404); - } + $this->get(route('ltu.invitation.accept', ['token' => $invite->token])) + ->assertStatus(200); +}); - /** @test */ - public function invitation_accept_store_will_create_contributor_and_login() - { - $this->withoutExceptionHandling(); +test('invitation accept page returns 404 if token is invalid', function () { + $this->get(route('ltu.invitation.accept', ['token' => 'invalid-token'])) + ->assertStatus(404); +}); - $invite = Invite::factory()->create(); +test('invitation accept store will create contributor and login', function () { + $invite = Invite::factory()->create(); - $translator = Contributor::factory()->make([ - 'email' => $invite->email, - 'role' => RoleEnum::translator, - ]); + $translator = Contributor::factory()->make([ + 'email' => $invite->email, + 'role' => RoleEnum::translator, + ]); - $this->post(route('ltu.invitation.accept.store', ['token' => $invite->token]), [ - 'name' => $translator->name, - 'email' => $invite->email, - 'password' => 'password', - 'password_confirmation' => 'password', - ])->assertRedirect(route('ltu.translation.index')); + $this->post(route('ltu.invitation.accept.store', ['token' => $invite->token]), [ + 'name' => $translator->name, + 'email' => $invite->email, + 'password' => 'password', + 'password_confirmation' => 'password', + ])->assertRedirect(route('ltu.translation.index')); - $user = Contributor::where('email', $invite->email)->first(); + $user = Contributor::where('email', $invite->email)->first(); - $this->assertAuthenticatedAs($user, 'translations'); - } + $this->assertAuthenticatedAs($user, 'translations'); +}); - /** @test */ - public function invitation_accept_store_will_redirect_if_contributor_already_exists() - { - $invite = Invite::factory()->create(); +test('invitation accept store will redirect if contributor already exists', function () { + $invite = Invite::factory()->create(); - $contributor = Contributor::factory()->create([ - 'email' => $invite->email, - 'role' => RoleEnum::translator, - ]); + $contributor = Contributor::factory()->create([ + 'email' => $invite->email, + 'role' => RoleEnum::translator, + ]); - $this->post(route('ltu.invitation.accept.store', ['token' => $invite->token]), [ - 'name' => $contributor->name, - 'email' => $invite->email, - 'password' => 'password', - 'password_confirmation' => 'password', - ])->assertRedirect(route('ltu.login')); + $this->post(route('ltu.invitation.accept.store', ['token' => $invite->token]), [ + 'name' => $contributor->name, + 'email' => $invite->email, + 'password' => 'password', + 'password_confirmation' => 'password', + ])->assertRedirect(route('ltu.login')); - $this->assertGuest('translations'); - } -} + $this->assertGuest('translations'); +}); diff --git a/tests/Http/Controllers/Auth/NewPasswordControllerTest.php b/tests/Http/Controllers/Auth/NewPasswordControllerTest.php index e4e3233..6da4ecb 100644 --- a/tests/Http/Controllers/Auth/NewPasswordControllerTest.php +++ b/tests/Http/Controllers/Auth/NewPasswordControllerTest.php @@ -1,77 +1,56 @@ owner->id.'|'.Str::random()); - - cache(["password.reset.{$this->owner->id}" => $token], - now()->addMinutes(60) - ); - - $this->post(route('ltu.password.update', [ - 'token' => $token, - 'email' => $this->owner->email, - 'password' => 'password', - 'password_confirmation' => 'password', - ]))->assertRedirect(route('ltu.translation.index')); - - $this->assertEmpty(cache()->get("password.reset.{$this->owner->id}")); - } - - /** @test */ - public function new_password_request_will_validate_email(): void - { - $token = encrypt($this->owner->id.'|'.Str::random()); - - $response = $this->post(route('ltu.password.update'), [ - 'token' => $token, - 'email' => 'not-an-email', - 'password' => 'password', - 'password_confirmation' => 'password', - ]); - - $this->assertInstanceOf(ValidationException::class, $response->exception); - } - - /** @test */ - public function new_password_request_will_validate_unconfirmed_password(): void - { - $token = encrypt($this->owner->id.'|'.Str::random()); - - $response = $this->post(route('ltu.password.update'), [ - 'token' => $token, - 'email' => $this->owner->email, - 'password' => 'password', - 'password_confirmation' => 'secret', - ]); - - $this->assertInstanceOf(ValidationException::class, $response->exception); - } - /** @test */ - public function password_will_validate_bad_token(): void - { - $this->post(route('ltu.password.update'), [ - 'token' => Str::random(), - 'email' => $this->owner->email, - 'password' => 'password', - 'password_confirmation' => 'password', - ])->assertSessionHas('invalidResetToken'); - } -} +test('password can be reset', function () { + $token = encrypt($this->owner->id.'|'.Str::random()); + + cache(["password.reset.{$this->owner->id}" => $token], + now()->addMinutes(60) + ); + + $this->post(route('ltu.password.update', [ + 'token' => $token, + 'email' => $this->owner->email, + 'password' => 'password', + 'password_confirmation' => 'password', + ]))->assertRedirect(route('ltu.translation.index')); + + expect(cache()->get("password.reset.{$this->owner->id}"))->toBeEmpty(); +}); + +test('new password request will validate email', function () { + $token = encrypt($this->owner->id.'|'.Str::random()); + + $response = $this->post(route('ltu.password.update'), [ + 'token' => $token, + 'email' => 'not-an-email', + 'password' => 'password', + 'password_confirmation' => 'password', + ]); + + expect($response->exception)->toBeInstanceOf(ValidationException::class); +}); + +test('new password request will validate unconfirmed password', function () { + $token = encrypt($this->owner->id.'|'.Str::random()); + + $response = $this->post(route('ltu.password.update'), [ + 'token' => $token, + 'email' => $this->owner->email, + 'password' => 'password', + 'password_confirmation' => 'secret', + ]); + + expect($response->exception)->toBeInstanceOf(ValidationException::class); +}); + +test('password will validate bad token', function () { + $this->post(route('ltu.password.update'), [ + 'token' => Str::random(), + 'email' => $this->owner->email, + 'password' => 'password', + 'password_confirmation' => 'password', + ])->assertSessionHas('invalidResetToken'); +}); diff --git a/tests/Http/Controllers/Auth/PasswordResetLinkControllerTest.php b/tests/Http/Controllers/Auth/PasswordResetLinkControllerTest.php index fe3c8b0..33f7c3a 100644 --- a/tests/Http/Controllers/Auth/PasswordResetLinkControllerTest.php +++ b/tests/Http/Controllers/Auth/PasswordResetLinkControllerTest.php @@ -1,45 +1,33 @@ get(route('ltu.password.request')) - ->assertOk(); - } - - /** @test */ - public function forgot_password_link_request_will_validate_email(): void - { - $response = $this->post(route('ltu.password.email'), [ - 'email' => 'not-an-email', - ]); - - $this->assertInstanceOf(ValidationException::class, $response->exception); - } - - /** @test */ - public function the_password_reset_link_can_sent(): void - { - Mail::fake(); - - $this->post(route('ltu.password.email'), [ - 'email' => $this->owner->email, - ]) - ->assertRedirect(route('ltu.password.request')); - - Mail::assertSent(ResetPassword::class, function ($mail) { - $this->assertIsString($mail->token); - - return $mail->hasTo($this->owner->email); - }); - } -} + +test('forgot password link screen can be rendered', function () { + $this->get(route('ltu.password.request')) + ->assertOk(); +}); + +test('forgot password link request will validate email', function () { + $response = $this->post(route('ltu.password.email'), [ + 'email' => 'not-an-email', + ]); + + $this->assertInstanceOf(ValidationException::class, $response->exception); +}); + +test('the password reset link can sent', function () { + Mail::fake(); + + $this->post(route('ltu.password.email'), [ + 'email' => $this->owner->email, + ]) + ->assertRedirect(route('ltu.password.request')); + + Mail::assertSent(ResetPassword::class, function ($mail) { + $this->assertIsString($mail->token); + + return $mail->hasTo($this->owner->email); + }); +}); diff --git a/tests/Http/Controllers/ContributorControllerTest.php b/tests/Http/Controllers/ContributorControllerTest.php index 877d559..dca09dd 100644 --- a/tests/Http/Controllers/ContributorControllerTest.php +++ b/tests/Http/Controllers/ContributorControllerTest.php @@ -1,58 +1,41 @@ actingAs($this->owner, 'translations')->get(route('ltu.contributors.index')) - ->assertStatus(200); - } +it('it can render the contributor page', function () { + $this->actingAs($this->owner, 'translations')->get(route('ltu.contributors.index')) + ->assertStatus(200); +}); - /** @test */ - public function test_owner_can_invite_a_contributor() - { - $this->withoutExceptionHandling(); +test('owner can invite a contributor', function () { + Mail::fake(); - Mail::fake(); + $response = $this->actingAs($this->owner, 'translations')->post(route('ltu.contributors.invite.store'), [ + 'email' => 'email@example.com', + 'role' => RoleEnum::owner->value, + ]); - $response = $this->actingAs($this->owner, 'translations')->post(route('ltu.contributors.invite.store'), [ - 'email' => 'email@example.com', - 'role' => RoleEnum::owner->value, + $response->assertRedirect(route('ltu.contributors.index').'#invited') + ->assertSessionHas('notification', [ + 'type' => 'success', + 'body' => 'Invite sent successfully', ]); - $response->assertRedirect(route('ltu.contributors.index').'#invited') - ->assertSessionHas('notification', [ - 'type' => 'success', - 'body' => 'Invite sent successfully', - ]); - - $invite = Invite::where('email', 'email@example.com')->first(); - - $this->assertNotNull($invite); - - Mail::assertSent(InviteCreated::class, function ($mail) use ($invite) { - return $mail->hasTo($invite->email); - }); - } - - /** @test */ - public function test_owner_cannot_invite_a_contributor_with_invalid_email() - { - $this->actingAs($this->owner, 'translations')->post(route('ltu.contributors.invite.store'), [ - 'email' => 'invalid-email', - 'role' => RoleEnum::owner->value, - ])->assertSessionHasErrors('email'); - } -} + $invite = Invite::where('email', 'email@example.com')->first(); + + expect($invite)->not->toBeNull(); + + Mail::assertSent(InviteCreated::class, function ($mail) use ($invite) { + return $mail->hasTo($invite->email); + }); +}); + +test('owner cannot invite a contributor with invalid email', function () { + $this->actingAs($this->owner, 'translations')->post(route('ltu.contributors.invite.store'), [ + 'email' => 'invalid-email', + 'role' => RoleEnum::owner->value, + ])->assertSessionHasErrors('email'); +}); diff --git a/tests/Http/Controllers/PhraseControllerTest.php b/tests/Http/Controllers/PhraseControllerTest.php new file mode 100644 index 0000000..6a8a1b0 --- /dev/null +++ b/tests/Http/Controllers/PhraseControllerTest.php @@ -0,0 +1,53 @@ + true, + ])->create(); + + $SourcePhrase = Phrase::factory(5)->withParameters()->create([ + 'translation_id' => $sourceTranslation->id, + ]); + + $this->translation = Translation::factory()->create(); + + Phrase::factory()->create([ + 'translation_id' => $this->translation->id, + ]); + + $this->phrase = Phrase::factory()->create([ + 'translation_id' => $this->translation->id, + 'phrase_id' => $SourcePhrase->first()->id, + ]); +}); + +it('can render phrases page', function () { + $this->actingAs($this->owner, 'translations') + ->get(route('ltu.phrases.index', $this->translation)) + ->assertStatus(200); +}); + +it('can render phrases edit page', function () { + $this->actingAs($this->owner, 'translations') + ->get(route('ltu.phrases.edit', [ + 'phrase' => $this->phrase->uuid, + 'translation' => $this->translation->id, + ])) + ->assertStatus(200); +}); + +it('can update phrase', function () { + $this->actingAs($this->owner, 'translations') + ->post(route('ltu.phrases.update', [ + 'phrase' => $this->phrase->uuid, + 'translation' => $this->translation->id, + ]), [ + 'phrase' => fake()->sentence(), + ]) + ->assertStatus(302); +}); diff --git a/tests/Http/Controllers/ProfileControllerTest.php b/tests/Http/Controllers/ProfileControllerTest.php index b7260d1..36b6990 100644 --- a/tests/Http/Controllers/ProfileControllerTest.php +++ b/tests/Http/Controllers/ProfileControllerTest.php @@ -1,81 +1,61 @@ actingAs($this->translator, 'translations') - ->get(route('ltu.profile.edit')) - ->assertOk(); - } - - /** - * @test - * - * @throws JsonException - */ - public function profile_information_can_be_updated(): void - { - $this->actingAs($this->translator, 'translations') - ->from(route('ltu.profile.edit')) - ->patch(route('ltu.profile.update'), [ - 'name' => 'Test User', - 'email' => 'test@example.com', - ]) - ->assertSessionHasNoErrors() - ->assertRedirect(route('ltu.profile.edit')); - - $this->translator->refresh(); - - $this->assertSame('Test User', $this->translator->name); - $this->assertSame('test@example.com', $this->translator->email); - } - - /** - * @test - * - * @throws JsonException - */ - public function password_can_be_updated(): void - { - $this->withoutExceptionHandling(); - - $this->actingAs($this->translator, 'translations') - ->from(route('ltu.profile.edit')) - ->put(route('ltu.profile.password.update'), [ - 'current_password' => 'password', - 'password' => 'new-password', - 'password_confirmation' => 'new-password', - ]) - ->assertSessionHasNoErrors() - ->assertRedirect(route('ltu.profile.edit')); - - $this->translator->refresh(); - - $this->assertTrue(Hash::check('new-password', $this->translator->refresh()->password)); - } - - /** @test */ - public function correct_password_must_be_provided_to_update_password(): void - { - $response = $this - ->actingAs($this->translator, 'translations') - ->from(route('ltu.profile.edit')) - ->put(route('ltu.profile.password.update'), [ - 'current_password' => 'wrong-password', - 'password' => 'new-password', - 'password_confirmation' => 'new-password', - ]); - $response - ->assertSessionHasErrors('current_password') - ->assertRedirect(route('ltu.profile.edit')); - } -} +use function Pest\Faker\fake; + +it('can access the profile page', function () { + $this->actingAs($this->translator, 'translations') + ->get(route('ltu.profile.edit')) + ->assertOk(); +}); + +it('can update profile information', function () { + $name = fake()->name; + $email = fake()->safeEmail; + + $this->actingAs($this->translator, 'translations') + ->from(route('ltu.profile.edit')) + ->patch(route('ltu.profile.update'), [ + 'name' => $name, + 'email' => $email, + ]) + ->assertSessionHasNoErrors() + ->assertRedirect(route('ltu.profile.edit')); + + $this->translator->refresh(); + + expect($this->translator->name)->toBe($name) + ->and($this->translator->email)->toBe($email); +}); + +it('can update the password', function () { + $this->actingAs($this->translator, 'translations') + ->from(route('ltu.profile.edit')) + ->put(route('ltu.profile.password.update'), [ + 'current_password' => 'password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]) + ->assertSessionHasNoErrors() + ->assertRedirect(route('ltu.profile.edit')); + + $this->translator->refresh(); + + expect(Hash::check('new-password', $this->translator->refresh()->password))->toBeTrue(); +}); + +it('can update profile information with password', function () { + $response = $this + ->actingAs($this->translator, 'translations') + ->from(route('ltu.profile.edit')) + ->put(route('ltu.profile.password.update'), [ + 'current_password' => 'wrong-password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]); + + $response + ->assertSessionHasErrors('current_password') + ->assertRedirect(route('ltu.profile.edit')); +}); diff --git a/tests/Http/Controllers/SourcePhraseControllerTest.php b/tests/Http/Controllers/SourcePhraseControllerTest.php new file mode 100644 index 0000000..679eff4 --- /dev/null +++ b/tests/Http/Controllers/SourcePhraseControllerTest.php @@ -0,0 +1,78 @@ + true, + ])->create(); + + $this->source_phrase = Phrase::factory(5)->withParameters()->create([ + 'translation_id' => $sourceTranslation->id, + ]); + + $this->translation = Translation::factory()->create(); + + Phrase::factory()->create([ + 'translation_id' => $this->translation->id, + ]); + + $this->phrase = Phrase::factory()->create([ + 'translation_id' => $this->translation->id, + 'phrase_id' => $this->source_phrase->first()->id, + ]); +}); + +it('can render source phrases page', function () { + $this->actingAs($this->owner, 'translations') + ->get(route('ltu.source_translation')) + ->assertStatus(200); +}); + +it('can render source phrases edit page', function () { + $this->actingAs($this->owner, 'translations') + ->get(route('ltu.source_translation.edit', $this->source_phrase->first()->uuid)) + ->assertStatus(200); +}); + +it('can update source phrase', function () { + $this->actingAs($this->owner, 'translations') + ->post(route('ltu.source_translation.update', $this->source_phrase->first()->uuid), [ + 'note' => fake()->sentence, + 'phrase' => fake()->sentence, + 'file' => $this->source_phrase->first()->translation_file_id, + ])->assertRedirect(route('ltu.source_translation')); +}); + +it('can delete source phrase', function () { + $this->actingAs($this->owner, 'translations') + ->delete(route('ltu.source_translation.delete_phrase', $this->source_phrase->first()->uuid)) + ->assertRedirect(route('ltu.source_translation')); +}); + +it('can delete multiple source phrases', function () { + $this->actingAs($this->owner, 'translations') + ->post(route('ltu.source_translation.delete_phrases', [ + 'selected_ids' => [$this->source_phrase->pluck('id')->first()], + ]))->assertRedirect(route('ltu.source_translation')); +}); + +it('can add new source key', function () { + $file = TranslationFile::factory()->create(); + + $phrase = Phrase::factory()->make([ + 'translation_id' => $this->translation->id, + 'translation_file_id' => $file->id, + ]); + + $this->actingAs($this->owner, 'translations') + ->post(route('ltu.source_translation.store_source_key'), [ + 'file' => $file->id, + 'key' => $phrase->key, + 'content' => $phrase->value, + ])->assertRedirect(route('ltu.source_translation')); +}); diff --git a/tests/Http/Controllers/TranslationControllerTest.php b/tests/Http/Controllers/TranslationControllerTest.php new file mode 100644 index 0000000..7f62600 --- /dev/null +++ b/tests/Http/Controllers/TranslationControllerTest.php @@ -0,0 +1,51 @@ + true, + ])->create(); + + $this->source_phrase = Phrase::factory(5)->withParameters()->create([ + 'translation_id' => $sourceTranslation->id, + ]); + + $this->translation = Translation::factory()->create(); +}); + +it('can render translations page', function () { + $this->actingAs($this->owner, 'translations') + ->get(route('ltu.translation.index')) + ->assertStatus(200); +}); + +it('can store new translation', function () { + $this->actingAs($this->owner, 'translations') + ->post(route('ltu.translation.store'), [ + 'languages' => [Language::inRandomOrder()->first()->id], + ]) + ->assertRedirect(route('ltu.translation.index')); + + $this->assertCount(3, Translation::all()); +}); + +it('translation can be deleted', function () { + $this->actingAs($this->owner, 'translations') + ->delete(route('ltu.translation.delete', $this->translation->id)) + ->assertRedirect(route('ltu.translation.index')); + + $this->assertCount(1, Translation::all()); +}); + +it('multiple translations can be deleted', function () { + $this->actingAs($this->owner, 'translations') + ->post(route('ltu.translation.delete_multiple', [ + 'selected_ids' => [$this->translation->id], + ])) + ->assertRedirect(route('ltu.translation.index')); + + $this->assertCount(1, Translation::all()); +}); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..e1acc05 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,5 @@ +in(__DIR__); diff --git a/tests/TranslationsManagerTest.php b/tests/TranslationsManagerTest.php index d3c143c..13ea9af 100644 --- a/tests/TranslationsManagerTest.php +++ b/tests/TranslationsManagerTest.php @@ -87,8 +87,6 @@ }); test('export creates a new translation file with the correct content', function () { - $this->markTestSkipped(); - $filesystem = new Filesystem(); createDirectoryIfNotExits(lang_path('en/auth.php')); @@ -110,6 +108,7 @@ $translationsManager->export(); $fileName = lang_path('en/'.$translation->phrases[0]->file->name.'.'.$translation->phrases[0]->file->extension); + $fileNameInDisk = File::allFiles(lang_path($translation->language->code))[0]->getPathname(); expect($fileName)->toBe($fileNameInDisk) From 905ebaed7c5d2adc5e70c2f35095aca3ba77b501 Mon Sep 17 00:00:00 2001 From: Mohamed Ashraf Date: Fri, 19 Jan 2024 19:24:14 +0400 Subject: [PATCH 5/5] wip --- src/Http/Controllers/PhraseController.php | 3 --- tests/Http/Controllers/PhraseControllerTest.php | 9 --------- 2 files changed, 12 deletions(-) diff --git a/src/Http/Controllers/PhraseController.php b/src/Http/Controllers/PhraseController.php index 86bd51c..b168666 100644 --- a/src/Http/Controllers/PhraseController.php +++ b/src/Http/Controllers/PhraseController.php @@ -78,9 +78,6 @@ public function edit(Translation $translation, Phrase $phrase): RedirectResponse ->setTarget($translation->language->code) ->translate($phrase->source->value), ], - // 'deepl' => 'DeepL', - // 'microsoft' => 'Microsoft Translator', - // 'amazon' => 'Amazon Translate', ], ]); } diff --git a/tests/Http/Controllers/PhraseControllerTest.php b/tests/Http/Controllers/PhraseControllerTest.php index 6a8a1b0..882e927 100644 --- a/tests/Http/Controllers/PhraseControllerTest.php +++ b/tests/Http/Controllers/PhraseControllerTest.php @@ -32,15 +32,6 @@ ->assertStatus(200); }); -it('can render phrases edit page', function () { - $this->actingAs($this->owner, 'translations') - ->get(route('ltu.phrases.edit', [ - 'phrase' => $this->phrase->uuid, - 'translation' => $this->translation->id, - ])) - ->assertStatus(200); -}); - it('can update phrase', function () { $this->actingAs($this->owner, 'translations') ->post(route('ltu.phrases.update', [