├── .editorconfig ├── .php-cs-fixer.php ├── LICENSE.md ├── README.md ├── composer.json ├── config └── translation-loader.php ├── database └── migrations │ └── create_language_lines_table.php.stub ├── phpunit.xml └── src ├── Exceptions └── InvalidConfiguration.php ├── LanguageLine.php ├── TranslationLoaderManager.php ├── TranslationLoaders ├── Db.php └── TranslationLoader.php └── TranslationServiceProvider.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | notPath('bootstrap/*') 5 | ->notPath('storage/*') 6 | ->notPath('vendor') 7 | ->in([ 8 | __DIR__ . '/src', 9 | __DIR__ . '/tests', 10 | ]) 11 | ->name('*.php') 12 | ->notName('*.blade.php') 13 | ->ignoreDotFiles(true) 14 | ->ignoreVCS(true); 15 | 16 | return (new PhpCsFixer\Config()) 17 | ->setRules([ 18 | '@PSR2' => true, 19 | 'array_syntax' => [ 20 | 'syntax' => 'short', 21 | ], 22 | 'ordered_imports' => [ 23 | 'sort_algorithm' => 'alpha', 24 | ], 25 | 'no_unused_imports' => true, 26 | 'not_operator_with_successor_space' => true, 27 | 'trailing_comma_in_multiline' => [ 28 | 'elements' => [ 29 | 'arrays', 30 | ], 31 | ], 32 | 'phpdoc_scalar' => true, 33 | 'unary_operator_spaces' => true, 34 | 'binary_operator_spaces' => true, 35 | 'blank_line_before_statement' => [ 36 | 'statements' => [ 37 | 'break', 38 | 'continue', 39 | 'declare', 40 | 'return', 41 | 'throw', 42 | 'try', 43 | ], 44 | ], 45 | 'phpdoc_single_line_var_spacing' => true, 46 | 'phpdoc_var_without_name' => true, 47 | 'class_attributes_separation' => [ 48 | 'elements' => [ 49 | 'method' => 'one', 50 | 'property' => 'one', 51 | ], 52 | ], 53 | 'method_argument_space' => [ 54 | 'on_multiline' => 'ensure_fully_multiline', 55 | 'keep_multiple_spaces_after_comma' => true, 56 | ], 57 | ]) 58 | ->setFinder($finder); 59 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Store your language lines in the database 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-translation-loader.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-translation-loader) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 5 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/spatie/laravel-translation-loader/run-tests.yml?branch=main&label=Tests&style=flat-square)](https://github.com/spatie/laravel-translation-loader/actions/workflows/run-tests.yml) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-translation-loader.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-translation-loader) 7 | 8 | In a vanilla Laravel or Lumen installation, you can use [language files](https://laravel.com/docs/localization) to 9 | localize your app. This package will enable the translations to be stored in the database. You can still use all the 10 | features of [the `__` helper function](https://laravel.com/docs/localization#retrieving-translation-strings) you know 11 | and love. 12 | 13 | ```php 14 | __('messages.welcome', ['name' => 'dayle']); 15 | ``` 16 | 17 | You can even mix using language files and the database. If a translation is present in both a file and the database, the 18 | database version will be returned. 19 | 20 | Want to use a different source for your translations? No problem! The package 21 | is [easily extendable](https://github.com/spatie/laravel-translation-loader#creating-your-own-translation-providers). 22 | 23 | Spatie is a web design agency based in Antwerp, Belgium. 24 | You'll find an overview of all our open source 25 | projects [on our website](https://spatie.be/opensource). 26 | 27 | ## Support us 28 | 29 | [](https://spatie.be/github-ad-click/laravel-translation-loader) 30 | 31 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can 32 | support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 33 | 34 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. 35 | You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards 36 | on [our virtual postcard wall](https://spatie.be/open-source/postcards). 37 | 38 | ## Installation 39 | 40 | You can install the package via composer: 41 | 42 | ``` bash 43 | composer require spatie/laravel-translation-loader 44 | ``` 45 | 46 | In `config/app.php` (Laravel) or `bootstrap/app.php` (Lumen) you should replace Laravel's translation service provider 47 | 48 | ```php 49 | Illuminate\Translation\TranslationServiceProvider::class, 50 | ``` 51 | 52 | by the one included in this package: 53 | 54 | ```php 55 | Spatie\TranslationLoader\TranslationServiceProvider::class, 56 | ``` 57 | 58 | You must publish and run the migrations to create the `language_lines` table: 59 | 60 | ```bash 61 | php artisan vendor:publish --provider="Spatie\TranslationLoader\TranslationServiceProvider" --tag="translation-loader-migrations" 62 | php artisan migrate 63 | ``` 64 | 65 | Optionally, you could publish the config file using this command. 66 | 67 | ```bash 68 | php artisan vendor:publish --provider="Spatie\TranslationLoader\TranslationServiceProvider" --tag="translation-loader-config" 69 | ``` 70 | 71 | This is the contents of the published config file: 72 | 73 | ```php 74 | return [ 75 | 76 | /* 77 | * Language lines will be fetched by these loaders. You can put any class here that implements 78 | * the Spatie\TranslationLoader\TranslationLoaders\TranslationLoader-interface. 79 | */ 80 | 'translation_loaders' => [ 81 | Spatie\TranslationLoader\TranslationLoaders\Db::class, 82 | ], 83 | 84 | /* 85 | * This is the model used by the Db Translation loader. You can put any model here 86 | * that extends Spatie\TranslationLoader\LanguageLine. 87 | */ 88 | 'model' => Spatie\TranslationLoader\LanguageLine::class, 89 | 90 | /* 91 | * This is the translation manager that overrides the default Laravel `translation.loader` 92 | */ 93 | 'translation_manager' => Spatie\TranslationLoader\TranslationLoaderManager::class, 94 | 95 | ]; 96 | ``` 97 | 98 | > **Note:** publishing assets doesn't work out of the box in Lumen. Instead, you have to copy the files from the repo. 99 | 100 | ## Usage 101 | 102 | You can create a translation in the database by creating and saving an instance of 103 | the `Spatie\TranslationLoader\LanguageLine`-model: 104 | 105 | ```php 106 | use Spatie\TranslationLoader\LanguageLine; 107 | 108 | LanguageLine::create([ 109 | 'group' => 'validation', 110 | 'key' => 'required', 111 | 'text' => ['en' => 'This is a required field', 'nl' => 'Dit is een verplicht veld'], 112 | ]); 113 | ``` 114 | 115 | You can fetch the translation 116 | with [Laravel's default `__` function](https://laravel.com/docs/localization#retrieving-translation-strings): 117 | 118 | ```php 119 | __('validation.required'); // returns 'This is a required field' 120 | 121 | app()->setLocale('nl'); 122 | 123 | __('validation.required'); // returns 'Dit is een verplicht veld' 124 | ``` 125 | 126 | You can still keep using the default language files as well. If a requested translation is present in both the database 127 | and the language files, the database version will be returned. 128 | 129 | If you need to store/override JSON translation lines, just create a normal LanguageLine with `group => '*'`. 130 | 131 | ## Creating your own translation providers 132 | 133 | This package ships with a translation provider that can fetch translations from the database. 134 | If you're storing your translations in a yaml-file, a csv-file, etc., 135 | you can easily extend this package by creating your own translation provider. 136 | 137 | A translation provider can be any class that implements 138 | the `Spatie\TranslationLoader\TranslationLoaders\TranslationLoader`-interface. It contains only one method: 139 | 140 | ```php 141 | namespace Spatie\TranslationLoader\TranslationLoaders; 142 | 143 | interface TranslationLoader 144 | { 145 | /* 146 | * Returns all translations for the given locale and group. 147 | */ 148 | public function loadTranslations(string $locale, string $group): array; 149 | } 150 | ``` 151 | 152 | Translation providers can be registered in the `translation_loaders` key of the config file. 153 | 154 | ## Changelog 155 | 156 | Please see [CHANGELOG](CHANGELOG.md) for more information about what has changed recently. 157 | 158 | ## Testing 159 | 160 | ``` bash 161 | composer test 162 | ``` 163 | 164 | ## Contributing 165 | 166 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 167 | 168 | ## Security 169 | 170 | If you've found a bug regarding security, please mail [security@spatie.be](mailto:security@spatie.be) instead of using 171 | the issue tracker. 172 | 173 | ## Credits 174 | 175 | - [Freek Van der Herten](https://github.com/freekmurze) 176 | - [All Contributors](../../contributors) 177 | 178 | ## License 179 | 180 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 181 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/laravel-translation-loader", 3 | "description": "Store your language lines in the database, yaml or other sources", 4 | "keywords": [ 5 | "spatie", 6 | "laravel", 7 | "laravel-translation-loader", 8 | "translate", 9 | "database", 10 | "db", 11 | "language", 12 | "i8n" 13 | ], 14 | "homepage": "https://github.com/spatie/laravel-translation-loader", 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Freek Van der Herten", 19 | "email": "freek@spatie.be", 20 | "homepage": "https://spatie.be", 21 | "role": "Developer" 22 | } 23 | ], 24 | "require": { 25 | "php": "^8.0", 26 | "illuminate/translation": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 27 | "spatie/laravel-package-tools": "^1.12" 28 | }, 29 | "require-dev": { 30 | "friendsofphp/php-cs-fixer": "^3.59", 31 | "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0|^10.0", 32 | "pestphp/pest": "^1.23|^2.0|^3.7" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Spatie\\TranslationLoader\\": "src" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "Tests\\": "tests" 42 | } 43 | }, 44 | "scripts": { 45 | "test": "vendor/bin/pest", 46 | "lint": "vendor/bin/php-cs-fixer fix" 47 | }, 48 | "config": { 49 | "sort-packages": true, 50 | "allow-plugins": { 51 | "pestphp/pest-plugin": true 52 | } 53 | }, 54 | "extra": { 55 | "laravel": { 56 | "providers": [ 57 | "Spatie\\TranslationLoader\\TranslationServiceProvider" 58 | ] 59 | } 60 | }, 61 | "minimum-stability": "dev", 62 | "prefer-stable": true 63 | } 64 | -------------------------------------------------------------------------------- /config/translation-loader.php: -------------------------------------------------------------------------------- 1 | [ 10 | Spatie\TranslationLoader\TranslationLoaders\Db::class, 11 | ], 12 | 13 | /* 14 | * This is the model used by the Db Translation loader. You can put any model here 15 | * that extends Spatie\TranslationLoader\LanguageLine. 16 | */ 17 | 'model' => Spatie\TranslationLoader\LanguageLine::class, 18 | 19 | /* 20 | * This is the translation manager which overrides the default Laravel `translation.loader` 21 | */ 22 | 'translation_manager' => Spatie\TranslationLoader\TranslationLoaderManager::class, 23 | 24 | ]; 25 | -------------------------------------------------------------------------------- /database/migrations/create_language_lines_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('group')->index(); 19 | $table->string('key'); 20 | $table->json('text'); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('language_lines'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ./tests 21 | 22 | 23 | 24 | 25 | ./app 26 | ./src 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidConfiguration.php: -------------------------------------------------------------------------------- 1 | */ 17 | public $guarded = [ 18 | 'id', 19 | ]; 20 | 21 | /** @var array */ 22 | protected $casts = [ 23 | 'text' => 'array', 24 | ]; 25 | 26 | public static function boot(): void 27 | { 28 | parent::boot(); 29 | 30 | $flushGroupCache = function (self $languageLine) { 31 | $languageLine->flushGroupCache(); 32 | }; 33 | 34 | static::saved($flushGroupCache); 35 | static::deleted($flushGroupCache); 36 | } 37 | 38 | public static function getTranslationsForGroup(string $locale, string $group): array 39 | { 40 | return Cache::rememberForever(static::getCacheKey($group, $locale), function () use ($group, $locale) { 41 | return static::query() 42 | ->where('group', $group) 43 | ->get() 44 | ->reduce(function ($lines, self $languageLine) use ($locale, $group) { 45 | $translation = $languageLine->getTranslation($locale); 46 | 47 | if ($translation !== null && $group === '*') { 48 | // Make a flat array when returning json translations 49 | $lines[$languageLine->key] = $translation; 50 | } elseif ($translation !== null && $group !== '*') { 51 | // Make a nested array when returning normal translations 52 | Arr::set($lines, $languageLine->key, $translation); 53 | } 54 | 55 | return $lines; 56 | }) ?? []; 57 | }); 58 | } 59 | 60 | public static function getCacheKey(string $group, string $locale): string 61 | { 62 | return "spatie.translation-loader.{$group}.{$locale}"; 63 | } 64 | 65 | public function getTranslation(string $locale): string|null 66 | { 67 | if (! isset($this->text[$locale])) { 68 | $fallback = config('app.fallback_locale'); 69 | 70 | return $this->text[$fallback] ?? null; 71 | } 72 | 73 | return $this->text[$locale]; 74 | } 75 | 76 | public function setTranslation(string $locale, string $value): static 77 | { 78 | $this->text = array_merge($this->text ?? [], [$locale => $value]); 79 | 80 | return $this; 81 | } 82 | 83 | public function flushGroupCache(): void 84 | { 85 | foreach ($this->getTranslatedLocales() as $locale) { 86 | Cache::forget(static::getCacheKey($this->group, $locale)); 87 | } 88 | } 89 | 90 | protected function getTranslatedLocales(): array 91 | { 92 | return array_keys($this->text); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/TranslationLoaderManager.php: -------------------------------------------------------------------------------- 1 | getTranslationsForTranslationLoaders($locale, $group, $namespace); 31 | 32 | return array_replace_recursive($fileTranslations, $loaderTranslations); 33 | } catch (QueryException $exception) { 34 | $modelClass = config('translation-loader.model'); 35 | $model = new $modelClass(); 36 | 37 | if (is_a($model, LanguageLine::class) && ! Schema::hasTable($model->getTable())) { 38 | return parent::load($locale, $group, $namespace); 39 | } 40 | 41 | throw $exception; 42 | } 43 | } 44 | 45 | protected function getTranslationsForTranslationLoaders( 46 | string $locale, 47 | string $group, 48 | string|null $namespace = null 49 | ): array { 50 | return collect(config('translation-loader.translation_loaders')) 51 | ->map(function (string $className) { 52 | return app($className); 53 | }) 54 | ->mapWithKeys(function (TranslationLoader $translationLoader) use ($locale, $group, $namespace) { 55 | return $translationLoader->loadTranslations($locale, $group, $namespace); 56 | }) 57 | ->toArray(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/TranslationLoaders/Db.php: -------------------------------------------------------------------------------- 1 | getConfiguredModelClass(); 13 | 14 | return $model::getTranslationsForGroup($locale, $group); 15 | } 16 | 17 | protected function getConfiguredModelClass(): string 18 | { 19 | $modelClass = config('translation-loader.model'); 20 | 21 | if (! is_a(new $modelClass(), LanguageLine::class)) { 22 | throw InvalidConfiguration::invalidModel($modelClass); 23 | } 24 | 25 | return $modelClass; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/TranslationLoaders/TranslationLoader.php: -------------------------------------------------------------------------------- 1 | name('laravel-translation-loader') 21 | ->hasConfigFile() 22 | ->hasMigrations('create_language_lines_table'); 23 | 24 | $this->registerLoader(); 25 | $this->registerTranslator(); 26 | } 27 | 28 | /** 29 | * Register the translation line loader. This method registers a 30 | * `TranslationLoaderManager` instead of a simple `FileLoader` as the 31 | * applications `translation.loader` instance. 32 | */ 33 | protected function registerLoader(): void 34 | { 35 | $this->app->singleton('translation.loader', function ($app) { 36 | $class = config('translation-loader.translation_manager'); 37 | 38 | return new $class($app['files'], $app['path.lang']); 39 | }); 40 | } 41 | 42 | /** 43 | * @return void 44 | */ 45 | protected function registerTranslator(): void 46 | { 47 | $this->app->singleton('translator', function ($app) { 48 | $loader = $app['translation.loader']; 49 | 50 | // When registering the translator component, we'll need to set the default 51 | // locale as well as the fallback locale. So, we'll grab the application 52 | // configuration so we can easily get both of these values from there. 53 | $locale = $app->getLocale(); 54 | 55 | $trans = new Translator($loader, $locale); 56 | 57 | $trans->setFallback($app->getFallbackLocale()); 58 | 59 | return $trans; 60 | }); 61 | } 62 | 63 | /** 64 | * Get the services provided by the provider. 65 | * 66 | * @return array 67 | */ 68 | public function provides(): array 69 | { 70 | return ['translator', 'translation.loader']; 71 | } 72 | } 73 | --------------------------------------------------------------------------------