diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dae749..c90abc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to `laravel-translation-loader` will be documented in this file +[//]: # (TODO: Update version when decided) + +## 3.0.0 - 2024-07-23 + +- added support for namespaced database translations + ## 2.7.0 - 2020-12-04 - add support for php 8.0 @@ -41,7 +47,9 @@ All notable changes to `laravel-translation-loader` will be documented in this f - add support for Laravel 5.8 ## 2.2.3 - 2019-02-01 + `` + - use Arr:: and Str:: functions ## 2.2.2 - 2018-10-25 diff --git a/README.md b/README.md index dddf412..85a0f4d 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,11 @@ This is the contents of the published config file: ```php return [ + /* + * The name of the table in which the language lines are stored. + */ + 'table_name' => 'language_lines', + /* * Language lines will be fetched by these loaders. You can put any class here that implements * the Spatie\TranslationLoader\TranslationLoaders\TranslationLoader-interface. @@ -88,7 +93,7 @@ return [ 'model' => Spatie\TranslationLoader\LanguageLine::class, /* - * This is the translation manager that overrides the default Laravel `translation.loader` + * This is the translation manager which overrides the default Laravel `translation.loader` */ 'translation_manager' => Spatie\TranslationLoader\TranslationLoaderManager::class, @@ -106,9 +111,10 @@ the `Spatie\TranslationLoader\LanguageLine`-model: use Spatie\TranslationLoader\LanguageLine; LanguageLine::create([ - 'group' => 'validation', - 'key' => 'required', - 'text' => ['en' => 'This is a required field', 'nl' => 'Dit is een verplicht veld'], + 'namespace' => '*', + 'group' => 'validation', + 'key' => 'required', + 'text' => ['en' => 'This is a required field', 'nl' => 'Dit is een verplicht veld'], ]); ``` @@ -145,7 +151,7 @@ interface TranslationLoader /* * Returns all translations for the given locale and group. */ - public function loadTranslations(string $locale, string $group): array; + public function loadTranslations(string $locale, string $group, string|null $namespace = null): array; } ``` diff --git a/composer.json b/composer.json index 454b13a..569ca46 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ ], "require": { "php": "^8.0", + "doctrine/dbal": "^3.0", "illuminate/translation": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", "spatie/laravel-package-tools": "^1.12" }, diff --git a/config/translation-loader.php b/config/translation-loader.php index 19ff8ef..4b706cb 100644 --- a/config/translation-loader.php +++ b/config/translation-loader.php @@ -2,6 +2,11 @@ return [ + /* + * The name of the table in which the language lines are stored. + */ + 'table_name' => 'language_lines', + /* * Language lines will be fetched by these loaders. You can put any class here that implements * the Spatie\TranslationLoader\TranslationLoaders\TranslationLoader-interface. diff --git a/database/migrations/alter_language_lines_table_add_column_namespace.php.stub b/database/migrations/alter_language_lines_table_add_column_namespace.php.stub new file mode 100644 index 0000000..cb9191c --- /dev/null +++ b/database/migrations/alter_language_lines_table_add_column_namespace.php.stub @@ -0,0 +1,34 @@ +string('namespace')->default('*')->after('id'); + $table->string('group')->default('*')->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + Schema::table(config('translation-loader.table_name'), function (Blueprint $table) { + $table->dropColumn('namespace'); + $table->string('group')->change(); + }); + } +}; diff --git a/database/migrations/create_language_lines_table.php.stub b/database/migrations/create_language_lines_table.php.stub index d24d12a..b8c4596 100644 --- a/database/migrations/create_language_lines_table.php.stub +++ b/database/migrations/create_language_lines_table.php.stub @@ -13,7 +13,7 @@ return new class extends Migration */ public function up(): void { - Schema::create('language_lines', function (Blueprint $table) { + Schema::create(config('translation-loader.table_name'), function (Blueprint $table) { $table->id(); $table->string('group')->index(); $table->string('key'); @@ -29,6 +29,6 @@ return new class extends Migration */ public function down(): void { - Schema::dropIfExists('language_lines'); + Schema::dropIfExists(config('translation-loader.table_name')); } }; diff --git a/src/LanguageLine.php b/src/LanguageLine.php index 645222b..05f3772 100644 --- a/src/LanguageLine.php +++ b/src/LanguageLine.php @@ -5,6 +5,9 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; +use PDOException; class LanguageLine extends Model { @@ -35,10 +38,28 @@ public static function boot(): void static::deleted($flushGroupCache); } - public static function getTranslationsForGroup(string $locale, string $group): array + public static function getTranslationsForGroup(string $locale, string $group, string|null $namespace = null): array { - return Cache::rememberForever(static::getCacheKey($group, $locale), function () use ($group, $locale) { + // When the app uses laravel-sail the package breaks every artisan command ran outside sail context. + // That's beacuse artisan starts an app and registers all service providers, + // but the cache store and/or database is unavailable beacause the hostname + // (e.g. redis/mysql) is unresolvable. + try { + DB::connection()->getPdo(); + Cache::get('laravel-translation-loader'); + } catch (PDOException $exception) { + Log::error('laravel-translation-loader: Could not connect to the database, falling back to file translations'); + + return []; + } catch (RedisException $exception) { + Log::error('laravel-translation-loader: Could not connect to the cache store, falling back to file translations'); + + return []; + } + + return Cache::rememberForever(static::getCacheKey($namespace, $group, $locale), function () use ($namespace, $group, $locale) { return static::query() + ->where('namespace', $namespace) ->where('group', $group) ->get() ->reduce(function ($lines, self $languageLine) use ($locale, $group) { @@ -57,9 +78,9 @@ public static function getTranslationsForGroup(string $locale, string $group): a }); } - public static function getCacheKey(string $group, string $locale): string + public static function getCacheKey(string $namespace, string $group, string $locale): string { - return "spatie.translation-loader.{$group}.{$locale}"; + return "spatie.translation-loader.$namespace.$group.$locale"; } public function getTranslation(string $locale): string|null @@ -83,7 +104,7 @@ public function setTranslation(string $locale, string $value): static public function flushGroupCache(): void { foreach ($this->getTranslatedLocales() as $locale) { - Cache::forget(static::getCacheKey($this->group, $locale)); + Cache::forget(static::getCacheKey($this->namespace, $this->group, $locale)); } } diff --git a/src/TranslationLoaderManager.php b/src/TranslationLoaderManager.php index 29f2533..2e79d94 100644 --- a/src/TranslationLoaderManager.php +++ b/src/TranslationLoaderManager.php @@ -23,15 +23,12 @@ public function load($locale, $group, $namespace = null): array try { $fileTranslations = parent::load($locale, $group, $namespace); - if (! is_null($namespace) && $namespace !== '*') { - return $fileTranslations; - } - $loaderTranslations = $this->getTranslationsForTranslationLoaders($locale, $group, $namespace); return array_replace_recursive($fileTranslations, $loaderTranslations); } catch (QueryException $exception) { $modelClass = config('translation-loader.model'); + $model = new $modelClass(); if (is_a($model, LanguageLine::class) && ! Schema::hasTable($model->getTable())) { diff --git a/src/TranslationLoaders/Db.php b/src/TranslationLoaders/Db.php index cf3ec87..09545ba 100644 --- a/src/TranslationLoaders/Db.php +++ b/src/TranslationLoaders/Db.php @@ -7,11 +7,11 @@ class Db implements TranslationLoader { - public function loadTranslations(string $locale, string $group): array + public function loadTranslations(string $locale, string $group, string|null $namespace = null): array { $model = $this->getConfiguredModelClass(); - return $model::getTranslationsForGroup($locale, $group); + return $model::getTranslationsForGroup($locale, $group, $namespace); } protected function getConfiguredModelClass(): string diff --git a/src/TranslationLoaders/TranslationLoader.php b/src/TranslationLoaders/TranslationLoader.php index c10f51c..d9ef66a 100644 --- a/src/TranslationLoaders/TranslationLoader.php +++ b/src/TranslationLoaders/TranslationLoader.php @@ -9,8 +9,9 @@ interface TranslationLoader * * @param string $locale * @param string $group + * @param string|null $namespace * * @return array */ - public function loadTranslations(string $locale, string $group): array; + public function loadTranslations(string $locale, string $group, string|null $namespace = null): array; } diff --git a/src/TranslationServiceProvider.php b/src/TranslationServiceProvider.php index 5a8ee2e..7634fe9 100644 --- a/src/TranslationServiceProvider.php +++ b/src/TranslationServiceProvider.php @@ -19,7 +19,10 @@ public function configurePackage(Package $package): void $package ->name('laravel-translation-loader') ->hasConfigFile() - ->hasMigrations('create_language_lines_table'); + ->hasMigrations( + 'create_language_lines_table', + 'alter_language_lines_table_add_column_namespace' + ); $this->registerLoader(); $this->registerTranslator(); diff --git a/tests/Feature/DummyManagerTest.php b/tests/Feature/DummyManagerTest.php index 6d98cd6..0b8038b 100644 --- a/tests/Feature/DummyManagerTest.php +++ b/tests/Feature/DummyManagerTest.php @@ -15,17 +15,17 @@ }); it('can translate using dummy manager using db', function () { - createLanguageLine('file', 'key', ['en' => 'en value from db']); + createLanguageLine('*', 'file', 'key', ['en' => 'en value from db']); expect(trans('file.key'))->toEqual('en value from db'); }); it('can translate using dummy manager using file with incomplete db', function () { - createLanguageLine('file', 'key', ['nl' => 'nl value from db']); + createLanguageLine('*', 'file', 'key', ['nl' => 'nl value from db']); expect(trans('file.key'))->toEqual('en value'); }); it('can translate using dummy manager using empty translation in db', function () { - createLanguageLine('file', 'key', ['en' => '']); + createLanguageLine('*', 'file', 'key', ['en' => '']); // Some versions of Laravel changed the behaviour of what an empty "" translation value returns: the key name or an empty value // @see https://github.com/laravel/framework/issues/34218 diff --git a/tests/Feature/JsonTransTest.php b/tests/Feature/JsonTransTest.php index 9d3fae3..8af5fdf 100644 --- a/tests/Feature/JsonTransTest.php +++ b/tests/Feature/JsonTransTest.php @@ -23,9 +23,9 @@ ->and(__($this->term2))->toEqual($this->term2Nl); }); -test('by default it will prefer a db translation over a file translation', function () { - createLanguageLine('*', $this->term1, ['en' => $this->term1EnDb]); - createLanguageLine('*', $this->term2, ['en' => $this->term2EnDb]); +it('it will prefer a db translation over a file translation by default', function () { + createLanguageLine('*', '*', $this->term1, ['en' => $this->term1EnDb]); + createLanguageLine('*', '*', $this->term2, ['en' => $this->term2EnDb]); expect(__($this->term1))->toEqual($this->term1EnDb) ->and(__($this->term2))->toEqual($this->term2EnDb); @@ -33,7 +33,7 @@ it('will default to fallback if locale is missing', function () { app()->setLocale('de'); - createLanguageLine('*', $this->term1, ['en' => $this->term1EnDb]); + createLanguageLine('*', '*', $this->term1, ['en' => $this->term1EnDb]); expect(__($this->term1))->toEqual($this->term1EnDb); }); diff --git a/tests/Feature/LanguageLineTest.php b/tests/Feature/LanguageLineTest.php index 8d68868..557ca8d 100644 --- a/tests/Feature/LanguageLineTest.php +++ b/tests/Feature/LanguageLineTest.php @@ -3,14 +3,14 @@ use Spatie\TranslationLoader\LanguageLine; it('can get a translation', function () { - $languageLine = createLanguageLine('group', 'new', ['en' => 'english', 'nl' => 'nederlands']); + $languageLine = createLanguageLine('*', 'group', 'new', ['en' => 'english', 'nl' => 'nederlands']); expect($languageLine->getTranslation('en'))->toEqual('english') ->and($languageLine->getTranslation('nl'))->toEqual('nederlands'); }); it('can set a translation', function () { - $languageLine = createLanguageLine('group', 'new', ['en' => 'english']); + $languageLine = createLanguageLine('*', 'group', 'new', ['en' => 'english']); $languageLine->setTranslation('nl', 'nederlands'); @@ -27,11 +27,11 @@ }); it('doesnt show error when getting nonexistent translation', function () { - $languageLine = createLanguageLine('group', 'new', ['nl' => 'nederlands']); + $languageLine = createLanguageLine('*', 'group', 'new', ['nl' => 'nederlands']); expect($languageLine->getTranslation('en'))->toBeNull(); }); -test('get fallback locale if doesnt exists', function () { - $languageLine = createLanguageLine('group', 'new', ['en' => 'English']); +it('will get a fallback locale if doesnt exists', function () { + $languageLine = createLanguageLine('*', 'group', 'new', ['en' => 'English']); expect($languageLine->getTranslation('es'))->toEqual('English'); }); diff --git a/tests/Feature/TransTest.php b/tests/Feature/TransTest.php index 79159b2..9c4b0c8 100644 --- a/tests/Feature/TransTest.php +++ b/tests/Feature/TransTest.php @@ -25,36 +25,42 @@ ->and(trans('file.404.message'))->toEqual('Deze pagina bestaat niet'); }); -test('by default it will prefer a db translation over a file translation', function () { - createLanguageLine('file', 'key', ['en' => 'en value from db']); - createLanguageLine('file', '404.title', ['en' => 'page not found from db']); +it('it will prefer a db translation over a file translation by default', function () { + createLanguageLine('*', 'file', 'key', ['en' => 'en value from db']); + createLanguageLine('*', 'file', '404.title', ['en' => 'page not found from db']); + createLanguageLine('validation', 'string', 'required', ['en' => 'The filed is required']); expect(trans('file.key'))->toEqual('en value from db') ->and(trans('file.404.title'))->toEqual('page not found from db') - ->and(trans('file.404.message'))->toEqual('This page does not exists'); + ->and(trans('file.404.message'))->toEqual('This page does not exists') + ->and(trans('validation::string.required'))->toEqual('The filed is required'); }); it('will return array if the given translation is nested', function () { foreach (Arr::dot($this->nested) as $key => $text) { - createLanguageLine('nested', $key, ['en' => $text]); + createLanguageLine('*', 'nested', $key, ['en' => $text]); + createLanguageLine('namespace', 'nested', $key, ['en' => $text]); } - expect(trans('nested.bool'))->toEqualCanonicalizing($this->nested['bool'], '$canonicalize = true', $delta = 0.0, $maxDepth = 10, $canonicalize = true); + expect(trans('nested.bool'))->toEqualCanonicalizing($this->nested['bool'], '$canonicalize = true') + ->and(trans('namespace::nested.bool'))->toEqualCanonicalizing($this->nested['bool'], '$canonicalize = true'); }); it('will return the translation string if max nested level is reached', function () { foreach (Arr::dot($this->nested) as $key => $text) { - createLanguageLine('nested', $key, ['en' => $text]); + createLanguageLine('*', 'nested', $key, ['en' => $text]); + createLanguageLine('namespace', 'nested', $key, ['en' => $text]); } - expect(trans('nested.bool.1'))->toEqual($this->nested['bool'][1]); -}); + expect(trans('nested.bool.1'))->toEqual($this->nested['bool'][1]) + ->and(trans('namespace::nested.bool.1'))->toEqual($this->nested['bool'][1]); +})->only(); it('will return the dotted translation key if no translation found', function () { $notFoundKey = 'nested.bool.3'; foreach (Arr::dot($this->nested) as $key => $text) { - createLanguageLine('nested', $key, ['en' => $text]); + createLanguageLine('*', 'nested', $key, ['en' => $text]); } expect(trans($notFoundKey))->toEqual($notFoundKey); @@ -62,7 +68,9 @@ it('will default to fallback if locale is missing', function () { app()->setLocale('de'); - createLanguageLine('missing_locale', 'key', ['en' => 'en value from db']); + createLanguageLine('*', 'missing_locale', 'key', ['en' => 'en value from db']); + createLanguageLine('missing_namespace', 'missing_locale', 'key', ['en' => 'en value from db']); - expect(trans('missing_locale.key'))->toEqual('en value from db'); + expect(trans('missing_locale.key'))->toEqual('en value from db') + ->and(trans('missing_namespace::missing_locale.key'))->toEqual('en value from db'); }); diff --git a/tests/Feature/TranslationLoaders/DbTest.php b/tests/Feature/TranslationLoaders/DbTest.php index 576afe6..2f30ada 100644 --- a/tests/Feature/TranslationLoaders/DbTest.php +++ b/tests/Feature/TranslationLoaders/DbTest.php @@ -20,7 +20,7 @@ }); it('supports placeholders', function () { - createLanguageLine('group', 'placeholder', ['en' => 'text with :placeholder']); + createLanguageLine('*', 'group', 'placeholder', ['en' => 'text with :placeholder']); expect(trans('group.placeholder', ['placeholder' => 'filled in placeholder']))->toEqual('text with filled in placeholder'); }); @@ -39,7 +39,7 @@ it('flushes the cache when a translation has been created', function () { expect(trans('group.new'))->toEqual('group.new'); - createLanguageLine('group', 'new', ['en' => 'created']); + createLanguageLine('*', 'group', 'new', ['en' => 'created']); flushIlluminateTranslatorCache(); expect(trans('group.new'))->toEqual('created'); @@ -67,7 +67,7 @@ it('can work with a custom model', function () { $alternativeModel = new class extends LanguageLine { - public static function getTranslationsForGroup(string $locale, string $group): array + public static function getTranslationsForGroup(string $locale, string $group, string|null $namespace = null): array { return ['key' => 'alternative class']; } diff --git a/tests/Feature/TranslationManagerTest.php b/tests/Feature/TranslationManagerTest.php index 2414d43..a79bca6 100644 --- a/tests/Feature/TranslationManagerTest.php +++ b/tests/Feature/TranslationManagerTest.php @@ -15,7 +15,7 @@ DummyLoader::class, ]); - createLanguageLine('db', 'key', ['en' => 'db']); + createLanguageLine('*', 'db', 'key', ['en' => 'db']); expect(trans('db.key'))->toEqual('db') ->and(trans('dummy.dummy'))->toEqual('this is dummy'); diff --git a/tests/Pest.php b/tests/Pest.php index 6f9e6b0..e432a92 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -10,7 +10,7 @@ function flushIlluminateTranslatorCache(): void app('translator')->setLoaded([]); } -function createLanguageLine(string $group, string $key, array $text): LanguageLine +function createLanguageLine(string $namespace, string $group, string $key, array $text): LanguageLine { - return LanguageLine::create(compact('group', 'key', 'text')); + return LanguageLine::create(compact('namespace', 'group', 'key', 'text')); } diff --git a/tests/TestCase.php b/tests/TestCase.php index 6ff4594..999373e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -24,11 +24,10 @@ protected function setUp(): void Artisan::call('migrate'); - $LanguageLinesTable = require __DIR__ . '/../database/migrations/create_language_lines_table.php.stub'; + (require __DIR__ . '/../database/migrations/create_language_lines_table.php.stub')->up(); + (require __DIR__ . '/../database/migrations/alter_language_lines_table_add_column_namespace.php.stub')->up(); - $LanguageLinesTable->up(); - - $this->languageLine = createLanguageLine('group', 'key', ['en' => 'english', 'nl' => 'nederlands']); + $this->languageLine = createLanguageLine('*', 'group', 'key', ['en' => 'english', 'nl' => 'nederlands']); } /**