├── .gitignore ├── .travis.yml ├── LICENSE.md ├── composer.json ├── config ├── .gitkeep └── translator.php ├── database └── migrations │ ├── .gitkeep │ ├── 2013_07_25_145943_create_languages_table.php │ ├── 2013_07_25_145958_create_translations_table.php │ └── 2016_06_02_124154_increase_locale_length.php ├── phpunit.xml ├── readme.md ├── src ├── Cache │ ├── CacheRepositoryInterface.php │ ├── RepositoryFactory.php │ ├── SimpleRepository.php │ └── TaggedRepository.php ├── Commands │ ├── CacheFlushCommand.php │ └── FileLoaderCommand.php ├── Facades │ ├── TranslationCache.php │ └── UriLocalizer.php ├── Loaders │ ├── CacheLoader.php │ ├── DatabaseLoader.php │ ├── FileLoader.php │ ├── Loader.php │ └── MixedLoader.php ├── Middleware │ └── TranslationMiddleware.php ├── Models │ ├── Language.php │ └── Translation.php ├── Repositories │ ├── LanguageRepository.php │ ├── Repository.php │ └── TranslationRepository.php ├── Routes │ └── ResourceRegistrar.php ├── Traits │ ├── Translatable.php │ └── TranslatableObserver.php ├── TranslationServiceProvider.php └── UriLocalizer.php └── tests ├── .gitkeep ├── Cache ├── RepositoryFactoryTest.php ├── SimpleRepositoryTest.php ├── TaggedRepositoryTest.php └── TranslationCacheTest.php ├── Commands ├── FlushTest.php └── LoadTest.php ├── Loaders ├── CacheLoaderTest.php ├── DatabaseLoaderTest.php ├── FileLoaderTest.php ├── LoadTest.php └── MixedLoaderTest.php ├── Localizer ├── CleanUrlTest.php ├── GetLocaleFromUrlTest.php └── LocalizeUriTest.php ├── Middleware └── TranslationMiddlewareTest.php ├── Repositories ├── LanguageRepositoryTest.php └── TranslationRepositoryTest.php ├── Routes └── ResourceRouteTest.php ├── TestCase.php ├── Traits └── TranslatableTest.php └── lang ├── ca └── test.php ├── en ├── auth.php ├── empty.php └── welcome │ └── page.php ├── es ├── auth.php └── welcome │ └── page.php └── vendor └── package ├── en └── example.php └── es └── example.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /tests/temp 3 | composer.phar 4 | composer.lock 5 | .DS_Store 6 | tests/temp/database.sqlite 7 | .idea 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.2 5 | - 7.3 6 | 7 | before_script: 8 | - travis_retry composer self-update 9 | - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-source 10 | 11 | script: 12 | - phpunit 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 WAAVI STUDIO SL 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "waavi/translation", 3 | "description": "A Translation package for Laravel 5 with database and cache support", 4 | "keywords": [ 5 | "waavi", 6 | "laravel-translator", 7 | "laravel", 8 | "translator", 9 | "translation", 10 | "localization" 11 | ], 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Waavi", 16 | "email": "info@waavi.com", 17 | "homepage": "http://waavi.com" 18 | } 19 | ], 20 | "require": { 21 | "laravel/framework": "~6.0|~7.0", 22 | "doctrine/dbal": "^2.5" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit" : "~8.3", 26 | "orchestra/testbench": "~4.0", 27 | "mockery/mockery": "^1.2.3" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Waavi\\Translation\\": "src/" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Waavi\\Translation\\Test\\": "tests" 37 | } 38 | }, 39 | "minimum-stability": "dev", 40 | "scripts": { 41 | "test": "vendor/bin/phpunit" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Waavi/translation/30b095364ea5d6701377d53f5eadec0e6d0b517d/config/.gitkeep -------------------------------------------------------------------------------- /config/translator.php: -------------------------------------------------------------------------------- 1 | env('TRANSLATION_SOURCE', 'files'), 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Default Translation Connection 24 | |-------------------------------------------------------------------------- 25 | | 26 | | This option controls the translation's connection. By default is use Laravel default connection. In most cases 27 | | you don't need to change it. 28 | */ 29 | 'connection' => config('database.default', env('TRANSLATOR_CONNECTION', 'mysql')), 30 | 31 | // In case the files source is selected, please enter here the supported locales for your app. 32 | // Ex: ['en', 'es', 'fr'] 33 | 'available_locales' => [], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Default Translation Cache 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Choose whether to leverage Laravel's cache module and how to do so. 41 | | 42 | | 'enabled' Boolean value. 43 | | 'timeout' In minutes. 44 | | 45 | */ 46 | 'cache' => [ 47 | 'enabled' => env('TRANSLATION_CACHE_ENABLED', true), 48 | 'timeout' => env('TRANSLATION_CACHE_TIMEOUT', 60), 49 | 'suffix' => env('TRANSLATION_CACHE_SUFFIX', 'translation'), 50 | ], 51 | ]; 52 | -------------------------------------------------------------------------------- /database/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Waavi/translation/30b095364ea5d6701377d53f5eadec0e6d0b517d/database/migrations/.gitkeep -------------------------------------------------------------------------------- /database/migrations/2013_07_25_145943_create_languages_table.php: -------------------------------------------------------------------------------- 1 | create('translator_languages', function ($table) { 16 | $table->increments('id'); 17 | $table->string('locale', 6)->unique(); 18 | $table->string('name', 60)->unique(); 19 | $table->timestamps(); 20 | $table->softDeletes(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::drop('translator_languages'); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2013_07_25_145958_create_translations_table.php: -------------------------------------------------------------------------------- 1 | create('translator_translations', function ($table) { 16 | $table->increments('id'); 17 | $table->string('locale', 6); 18 | $table->string('namespace', 150)->default('*'); 19 | $table->string('group', 150); 20 | $table->string('item', 150); 21 | $table->text('text'); 22 | $table->boolean('unstable')->default(false); 23 | $table->boolean('locked')->default(false); 24 | $table->timestamps(); 25 | $table->foreign('locale')->references('locale')->on('translator_languages'); 26 | $table->unique(['locale', 'namespace', 'group', 'item']); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::drop('translator_translations'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /database/migrations/2016_06_02_124154_increase_locale_length.php: -------------------------------------------------------------------------------- 1 | table('translator_languages', function ($table) { 16 | $table->string('locale', 10)->change(); 17 | }); 18 | Schema::connection(config('translator.connection'))->table('translator_translations', function ($table) { 19 | $table->string('locale', 10)->change(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | // 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | /temp 17 | /lang 18 | 19 | 20 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Better localization management for Laravel 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/waavi/translation.svg?style=flat-square)](https://packagist.org/packages/waavi/translation) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 5 | [![Build Status](https://img.shields.io/travis/Waavi/translation/master.svg?style=flat-square)](https://travis-ci.org/Waavi/translation) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/waavi/translation.svg?style=flat-square)](https://packagist.org/packages/waavi/translation) 7 | 8 | ## Introduction 9 | 10 | Keeping a project's translations properly updated is cumbersome. Usually translators do not have access to the codebase, and even when they do it's hard to keep track of which translations are missing for each language or when updates to the original text require that translations be revised. 11 | 12 | This package allows developers to leverage their database and cache to manage multilanguage sites, while still working on language files during development and benefiting from all the features Laravel's Translation bundle has, like pluralization or replacement. 13 | 14 | WAAVI is a web development studio based in Madrid, Spain. You can learn more about us at [waavi.com](http://waavi.com) 15 | 16 | ## Table of contents 17 | 18 | - [Laravel compatibility](#laravel-compatibility) 19 | - [Features overview](#features-overview) 20 | - [Installation](#installation) 21 | - [Set source for translations](#translations-source) 22 | - [Load translations from files](#load-translations-from-files) 23 | - [Load translations from the database](#load-translations-from-the-database) 24 | - [Mixed mode](#mixed-mode) 25 | - [Loading your files into the database](#loading-your-files-into-the-database) 26 | - [Cache translations](#cache-translations) 27 | - [Managing languages and translations in the Database](#managing-languages-and-translations-in-the-database) 28 | - [Managing Languages](#managing-languages) 29 | - [Managing Translations](#managing-translations) 30 | - [Model attributes translation](#model-attributes-translation) 31 | - [Uri localization](#uri-localization) 32 | 33 | ## Laravel compatibility 34 | 35 | Laravel | translation 36 | :---------|:---------- 37 | 4.x | 1.0.x 38 | 5.0.x | 2.0.x 39 | 5.1.x\|5.3.x | 2.1.x 40 | 5.4.x | 2.2.x 41 | 5.5.x | 2.3.x and higher 42 | 5.6.x | 2.3.x and higher 43 | 6.x\|7.x | 2.4.x and higher 44 | ## Features overview 45 | 46 | - Allow dynamic changes to the site's text and translations. 47 | - Cache your localization entries. 48 | - Load your translation files into the database. 49 | - Force your urls to be localized (ex: /home -> /es/home) and set the locale automatically through the browser's config. 50 | - Localize your model attributes. 51 | 52 | ## Installation 53 | 54 | Require through composer 55 | 56 | 57 | composer require waavi/translation 2.3.x 58 | 59 | Or manually edit your composer.json file: 60 | 61 | "require": { 62 | "waavi/translation": "2.3.x" 63 | } 64 | 65 | Once installed, in your project's config/app.php file replace the following entry from the providers array: 66 | 67 | Illuminate\Translation\TranslationServiceProvider::class 68 | 69 | with: 70 | 71 | Waavi\Translation\TranslationServiceProvider::class 72 | 73 | Remove your config cache: 74 | 75 | php artisan config:clear 76 | 77 | Publish both the configuration file and the migrations: 78 | 79 | php artisan vendor:publish --provider="Waavi\Translation\TranslationServiceProvider" 80 | 81 | Execute the database migrations: 82 | 83 | php artisan migrate 84 | 85 | You may check the package's configuration file at: 86 | 87 | config/translator.php 88 | 89 | ## Translations source 90 | 91 | This package allows you to load translation from the regular Laravel localization files (in /resources/lang), from the database, from cache or in a mix of the previous for development. You may configure the desired mode of operation through the translator.php config file and/or the TRANSLATION_SOURCE environment variable. Accepted values are: 92 | 93 | - 'files' To load translations from Laravel's language files (default) 94 | - 'database' To load translations from the database 95 | - 'mixed' To load translations both from the filesystem and the database, with the filesystem having priority. 96 | - 'mixed_db' To load translations both from the filesystem and the database, with the database having priority. [v2.1.5.3] 97 | 98 | NOTE: When adding the package to an existing Laravel project, 'files' must be used until migrations have been executed. 99 | 100 | For cache configuration, please go to [cache configuration](#cache-translations) 101 | 102 | ### Load translations from files 103 | 104 | If you do not wish to leverage your database for translations, you may choose to load language lines exclusively through language files. This mode differs from Laravel in that, in case a line is not found in the specified locale, instead of returning the key right away, we first check the default language for an entry. In case you wish to use this mode exclusively, you will need to set the 'available_locales' config file: 105 | 106 | config/translator.php 107 | 'available_locales' => ['en', 'es', 'fr'], 108 | 109 | Example: 110 | 111 | The content in en/validations.php, where 'en' is the default locale, is: 112 | ```php 113 | [ 114 | 'missing_name' => 'Name is missing', 115 | 'missing_surname' => 'Surname is missing', 116 | ]; 117 | ``` 118 | The content in es/validations.php is: 119 | ```php 120 | [ 121 | 'missing_name' => 'Falta el nombre', 122 | ]; 123 | ``` 124 | Output for different keys with 'es' locale: 125 | ```php 126 | trans('validations.missing_name'); // 'Falta el nombre' 127 | trans('validations.missing_surname'); // 'Surname is missing' 128 | trans('validations.missing_email'); // 'validations.missing_email' 129 | ``` 130 | 131 | ### Load translations from the database 132 | 133 | You may choose to load translations exclusively from the database. This is very useful if you intend to allow users or administrators to live edit the site's text and translations. In a live production environment, you will usually want this source mode to be activated with the translation's cache. Please see [Loading your files into the database](#loading-your-files-into-the-database) for details on the steps required to use this source mode. 134 | 135 | Example: 136 | 137 | The content in the languages table is: 138 | 139 | | id | locale | name | 140 | ------------------------- 141 | | 1 | en | english | 142 | | 2 | es | spanish | 143 | 144 | The relevant content in the language_entries table is: 145 | 146 | | id | locale | namespace | group | item | text | 147 | ------------------------------------------------------------------------------------- 148 | | 1 | en | * | validations | missing.name | Name is missing | 149 | | 2 | en | * | validations | missing.surname | Surname is missing | 150 | | 3 | en | * | validations | min_number | Number is too small | 151 | | 4 | es | * | validations | missing.name | Falta nombre | 152 | | 5 | es | * | validations | missing.surname | Falta apellido | 153 | 154 | Output for different keys with es locale: 155 | 156 | ```php 157 | trans('validations.missing.name'); // 'Falta nombre' 158 | trans('validations.min_number'); // 'Number is too small' 159 | trans('validations.missing.email'); // 'missing_email' 160 | ``` 161 | 162 | ### Mixed mode 163 | 164 | In mixed mode, both the language files and the database are queried when looking for a group of language lines. Entries found in the filesystem take precedence over the database. This source mode is useful when in development, so that both the filesystem and the user entries are taken into consideration. 165 | 166 | Example: 167 | 168 | When files and database are set like in the previous examples: 169 | ```php 170 | trans('validations.missing_name'); // 'Falta el nombre' 171 | trans('validations.missing_surname'); // 'Falta apellido' 172 | trans('validations.min_number'); // 'Number is too small' 173 | trans('validations.missing_email'); // 'missing_email' 174 | ``` 175 | 176 | ### Loading your files into the database 177 | 178 | When using either the database or mixed translation sources, you will need to first load your translations into the database. To do so, follow these steps: 179 | 180 | * Run the migrations detailed in the installation instructions. 181 | * Add your languages of choice to the database (see [Managing Database Languages](#managing-database-languages)) 182 | * Load your language files into the database using the provided Artisan command: 183 | 184 | ` php artisan translator:load ` 185 | 186 | When executing the artisan command, the following will happen: 187 | 188 | - Non existing entries will be created. 189 | - Existing entries will be updated **except if they're locked**. When allowing users to live edit the translations, it is recommended you do it throught the updateAndLock method provided in the [Translations repository](#managing-translations). This prevents entries being overwritten when reloading translations from files. 190 | - When an entry in the default locale is edited, all of its translations will be flagged as **pending review**. This gives translators the oportunity to review translations that might not be correct, but doesn't delete them so as to avoid minor errata changes in the source text from erasing all translations. See [Managing translations](#managing-translations) for details on how to work with unstable translations. 191 | 192 | Both vendor files and subdirectories are supported. Please keep in mind that when loading an entry inside a subdirectory, Laravel 5 has changed the syntax to: 193 | ```php 194 | trans('subdir/file.entry') 195 | trans('package::subdir/file.entry') 196 | ``` 197 | 198 | ## Cache translations 199 | 200 | Since querying the database everytime a language group must be loaded is grossly inefficient, you may choose to leverage Laravel's cache system. This module will use the same cache configuration as defined by you in app/config/cache.php. 201 | 202 | You may enable or disable the cache through the translator.php config file or the 'TRANSLATION_CACHE_ENABLED' environment variable. Config options are: 203 | 204 | Env key | type |description 205 | :---------|:--------|:----------- 206 | TRANSLATION_CACHE_ENABLED | boolean| Enable / disable the translations cache 207 | TRANSLATION_CACHE_TIMEOUT | integer| Minutes translation items should be kept in the cache. 208 | TRANSLATION_CACHE_SUFFIX | string | Default is 'translation'. This will be the cache suffix applied to all translation cache entries. 209 | 210 | ### Cache tags 211 | 212 | Available since version 2.1.3.8, if the cache store in use allows for tags, the TRANSLATION_CACHE_SUFFIX will be used as the common tag to all cache entries. This is recommended to be able to invalidate only the translation cache, or even just a given locale, namespace and group configuration. 213 | 214 | ### Clearing the cache 215 | 216 | Available since version 2.1.3.8, you may clear the translation cache through both an Artisan Command and a Facade. If cache tags are in use, only the translation cache will be cleared. All of your application cache will however be cleared if you cache tags are not available. 217 | 218 | Cache flush command: 219 | 220 | php artisan translator:flush 221 | 222 | In order to access the translation cache, add to your config/app.php files, the following alias: 223 | ```php 224 | 'aliases' => [ 225 | /* ... */ 226 | 'TranslationCache' => \Waavi\Translation\Facades\TranslationCache::class, 227 | ] 228 | ``` 229 | Once done, you may clear the whole translation cache by calling: 230 | ```php 231 | \TranslationCache::flushAll(); 232 | ``` 233 | 234 | You may also choose to invalidate only a given locale, namespace and group combination. 235 | ```php 236 | \TranslationCache::flush($locale, $group, $namespace); 237 | ``` 238 | 239 | - The locale is the language locale you wish to clear. 240 | - The namespace is either '*' for your application translation files, or 'package' for vendor translation files. 241 | - The group variable is the path to the translation file you wish to clear. 242 | 243 | For example, say we have the following file in our resources/lang directory: en/auth.php, en/auth/login.php and en/vendor/waavi/login.php. To clear the cache entries for each of them you would call: 244 | ```php 245 | \TranslationCache::flush('en', 'auth', '*'); 246 | \TranslationCache::flush('en', 'auth/login', '*'); 247 | \TranslationCache::flush('en', 'login', 'waavi'); 248 | ``` 249 | 250 | ## Managing languages and translations in the Database 251 | 252 | The recommended way of managing both languages and translations is through the provided repositories. You may circumvent this by saving changes directly through the Language and Translation models, however validation is no longer executed automatically on model save and could lead to instability and errors. 253 | 254 | Both the Language and the Translation repositories provide the following methods: 255 | 256 | Method | Description 257 | :---------|:-------- 258 | hasTable(); | Returns true if the corresponding table exists in the database, false otherwise 259 | all($related = [], $perPage = 0); | Retrieve all records from the DB. A paginated record will be return if the second argument is > 0, with $perPage items returned per page 260 | find($id); | Find a record by id 261 | create($attributes); | Validates the given attributes and inserts a new record. Returns false if validation errors occured 262 | delete($id); | Delete a record by id 263 | restore($id); | Restore a record by id 264 | count(); | Return the total number of entries 265 | validate(array $attributes); | Checks if the given attributes are valid 266 | validationErrors(); | Get validation errors for create and update methods 267 | 268 | ### Managing Languages 269 | 270 | Language management should be done through the **\Waavi\Translation\Repositories\LanguageRepository** to ensure proper data validation before inserts and updates. It is recommended that you instantiate this class through Dependency Injection. 271 | 272 | A valid Language record requires both its name and locale to be unique. It is recommended you use the native name for each language (Ex: English, Español, Français) 273 | 274 | The provided methods are: 275 | 276 | Method | Description 277 | :---------|:-------- 278 | update(array $attributes); | Updates a Language entry [id, name, locale] 279 | trashed($related = [], $perPage = 0); | Retrieve all trashed records from the DB. 280 | findTrashed($id, $related = []); | Find a trashed record by id 281 | findByLocale($locale); | Find a record by locale 282 | findTrashedByLocale($locale); | Finds a trashed record by locale 283 | allExcept($locale); | Returns a list of all languages excluding the given locale 284 | availableLocales(); | Returns a list of all available locales 285 | isValidLocale($locale); | Checks if a language exists with the given locale 286 | percentTranslated($locale); | Returns the percent translated for the given locale 287 | 288 | 289 | ### Managing Translations 290 | 291 | Translation management should be done through the **\Waavi\Translation\Repositories\TranslationRepository** to ensure proper data validation before inserts and updates. It is recommended that you instantiate this class through Dependency Injection. 292 | 293 | A valid translation entry cannot have the same locale and language code than another. 294 | 295 | The provided methods are: 296 | 297 | Method | Description 298 | :---------|:-------- 299 | update($id, $text); | Update an unlocked entry 300 | updateAndLock($id, $text); | Update and lock an entry (locked or not) 301 | allByLocale($locale, $perPage = 0); | Get all by locale 302 | untranslated($locale, $perPage = 0, $text = null); | Get all untranslated entries. If $text is set, entries will be filtered by partial matches to translation value. 303 | pendingReview($locale, $perPage = 0); | List all entries pending review 304 | search($locale, $term, $perPage = 0); | Search by all entries by locale and a partial match to both the text value and the translation code. 305 | randomUntranslated($locale); | Get a random untranslated entry 306 | translateText($text, $textLocale, $targetLocale); | Translate text to another locale 307 | flagAsReviewed($id); | Flag entry as reviewed 308 | 309 | Things to consider: 310 | 311 | - You may lock translations so that they can only be updated through updateAndLock. The language file loader uses the update method and will not be able to override locked translations. 312 | - When a text entry belonging to the default locale is updated, all of its siblings are marked as pending review. 313 | - When deleting an entry, if it belongs to the default locale its translations will also be deleted. 314 | 315 | ## Model attributes translation 316 | 317 | You can also use the translation management system to manage your model attributes translations. To do this, you only need to: 318 | 319 | - Make sure either the database or mixed source are set. 320 | - Make sure your models use the Waavi\Translation\Translatable\Trait 321 | - In your model, add a translatableAttributes array with the names of the attributes you wish to be available for translation. 322 | - For every field you wish to translate, make sure there is a corresponding attributeName_translation field in your database. 323 | 324 | Example: 325 | ```php 326 | \Schema::create('examples', function ($table) { 327 | $table->increments('id'); 328 | $table->string('slug')->nullable(); 329 | $table->string('title')->nullable(); 330 | $table->string('title_translation')->nullable(); 331 | $table->string('text')->nullable(); 332 | $table->string('text_translation')->nullable(); 333 | $table->timestamps(); 334 | }); 335 | 336 | class Example extends Model 337 | { 338 | use \Waavi\Translation\Traits\Translatable; 339 | protected $translatableAttributes = ['title', 'text']; 340 | } 341 | ``` 342 | 343 | ## Uri localization 344 | 345 | You may use Waavi\Translation\Middleware\TranslationMiddleware to make sure all of your urls are properly localized. The TranslationMiddleware will only redirect GET requests that do not have a locale in them. 346 | 347 | For example, if a user visits the url /home, the following would happen: 348 | 349 | - The middleware will check if a locale is present. 350 | - If a valid locale is present: 351 | - it will globally set the language for that locale 352 | - the following data will be available in your views: 353 | - currentLanguage: current selected Language instance. 354 | - selectableLanguages: list of all languages the visitor can switch to (except the current one) 355 | - altLocalizedUrls: a list of all localized urls for the current resource except this one, formatted as ['locale' => 'en', 'name' => 'English', 'url' => '/en/home'] 356 | - If no locale is present: 357 | - Check the first two letters of the brower's accepted locale HTTP_ACCEPT_LANGUAGE (for example 'en-us' => 'en') 358 | - If this is a valid locale, redirect the visitor to that locale => /es/home 359 | - If not, redirect to default locale => /en/home 360 | - Redirects will keep input data in the url, if any 361 | 362 | You may choose to activate this Middleware globally by adding the middleware to your App\Http\Kernel file: 363 | ```php 364 | protected $middleware = [ 365 | /* ... */ 366 | \Waavi\Translation\Middleware\TranslationMiddleware::class, 367 | ] 368 | ``` 369 | Or to apply it selectively through the **'localize'** route middleware, which is already registered when installing the package through the ServiceProvider. 370 | 371 | It is recommended you add the following alias to your config/app.php aliases: 372 | 373 | ```php 374 | 'aliases' => [ 375 | /* ... */ 376 | 'UriLocalizer' => Waavi\Translation\Facades\UriLocalizer::class, 377 | ]; 378 | ``` 379 | 380 | Every localized route must be prefixed with the current locale: 381 | 382 | ```php 383 | // If the middleware is globally applied: 384 | Route::group(['prefix' => \UriLocalizer::localeFromRequest()], function(){ 385 | /* Your routes here */ 386 | }); 387 | 388 | // For selectively chosen routes: 389 | Route::group(['prefix' => \UriLocalizer::localeFromRequest(), 'middleware' => 'localize')], function () { 390 | /* Your routes here */ 391 | }); 392 | ``` 393 | 394 | Starting on v2.1.6, you may also specify a custom position for the locale segment in your url. For example, if the locale info is the third segment in a URL (/api/v1/es/my_resource), you may use: 395 | 396 | ```php 397 | // For selectively chosen routes: 398 | Route::group(['prefix' => 'api/v1'], function() { 399 | /** ... Non localized urls here **/ 400 | 401 | Route::group(['prefix' => \UriLocalizer::localeFromRequest(2), 'middleware' => 'localize:2')], function () { 402 | /* Your localized routes here */ 403 | }); 404 | }); 405 | ``` 406 | 407 | In your views, for routes where the Middleware is active, you may present the user with a menu to switch from the current language to another by using the shared variables. For example: 408 | 409 | ```php 410 | 418 | ``` 419 | -------------------------------------------------------------------------------- /src/Cache/CacheRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | getParentClass(); 12 | $parentName = $storeParent ? $storeParent->name : ''; 13 | return $parentName == 'Illuminate\Cache\TaggableStore' ? new TaggedRepository($store, $cacheTag) : new SimpleRepository($store, $cacheTag); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Cache/SimpleRepository.php: -------------------------------------------------------------------------------- 1 | store = $store; 23 | $this->cacheTag = $cacheTag; 24 | } 25 | 26 | /** 27 | * Checks if an entry with the given key exists in the cache. 28 | * 29 | * @param string $locale 30 | * @param string $group 31 | * @param string $namespace 32 | * @return boolean 33 | */ 34 | public function has($locale, $group, $namespace) 35 | { 36 | return !is_null($this->get($locale, $group, $namespace)); 37 | } 38 | 39 | /** 40 | * Get an item from the cache 41 | * 42 | * @param string $locale 43 | * @param string $group 44 | * @param string $namespace 45 | * @return mixed 46 | */ 47 | public function get($locale, $group, $namespace) 48 | { 49 | $key = $this->getKey($locale, $group, $namespace); 50 | return $this->store->get($key); 51 | } 52 | 53 | /** 54 | * Put an item into the cache store 55 | * 56 | * @param string $locale 57 | * @param string $group 58 | * @param string $namespace 59 | * @param mixed $content 60 | * @param integer $minutes 61 | * @return void 62 | */ 63 | public function put($locale, $group, $namespace, $content, $minutes) 64 | { 65 | $key = $this->getKey($locale, $group, $namespace); 66 | $this->store->put($key, $content, $minutes); 67 | } 68 | 69 | /** 70 | * Flush the cache for the given entries 71 | * 72 | * @param string $locale 73 | * @param string $group 74 | * @param string $namespace 75 | * @return void 76 | */ 77 | public function flush($locale, $group, $namespace) 78 | { 79 | $this->flushAll(); 80 | } 81 | 82 | /** 83 | * Completely flush the cache 84 | * 85 | * @param string $locale 86 | * @param string $group 87 | * @param string $namespace 88 | * @return void 89 | */ 90 | public function flushAll() 91 | { 92 | $this->store->flush(); 93 | } 94 | 95 | /** 96 | * Returns a unique cache key. 97 | * 98 | * @param string $locale 99 | * @param string $group 100 | * @param string $namespace 101 | * @return string 102 | */ 103 | protected function getKey($locale, $group, $namespace) 104 | { 105 | return md5("{$this->cacheTag}-{$locale}-{$group}-{$namespace}"); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/Cache/TaggedRepository.php: -------------------------------------------------------------------------------- 1 | store = $store; 30 | $this->cacheTag = $cacheTag; 31 | } 32 | 33 | /** 34 | * Checks if an entry with the given key exists in the cache. 35 | * 36 | * @param string $locale 37 | * @param string $group 38 | * @param string $namespace 39 | * @return boolean 40 | */ 41 | public function has($locale, $group, $namespace) 42 | { 43 | return !is_null($this->get($locale, $group, $namespace)); 44 | } 45 | 46 | /** 47 | * Get an item from the cache 48 | * 49 | * @param string $locale 50 | * @param string $group 51 | * @param string $namespace 52 | * @return mixed 53 | */ 54 | public function get($locale, $group, $namespace) 55 | { 56 | $key = $this->getKey($locale, $group, $namespace); 57 | return $this->store->tags([$this->cacheTag, $key])->get($key); 58 | } 59 | 60 | /** 61 | * Put an item into the cache store 62 | * 63 | * @param string $locale 64 | * @param string $group 65 | * @param string $namespace 66 | * @param mixed $content 67 | * @param integer $minutes 68 | * @return void 69 | */ 70 | public function put($locale, $group, $namespace, $content, $minutes) 71 | { 72 | $key = $this->getKey($locale, $group, $namespace); 73 | $this->store->tags([$this->cacheTag, $key])->put($key, $content, $minutes); 74 | } 75 | 76 | /** 77 | * Flush the cache for the given entries 78 | * 79 | * @param string $locale 80 | * @param string $group 81 | * @param string $namespace 82 | * @return void 83 | */ 84 | public function flush($locale, $group, $namespace) 85 | { 86 | $key = $this->getKey($locale, $group, $namespace); 87 | $this->store->tags([$key])->flush(); 88 | } 89 | 90 | /** 91 | * Completely flush the cache 92 | * 93 | * @param string $locale 94 | * @param string $group 95 | * @param string $namespace 96 | * @return void 97 | */ 98 | public function flushAll() 99 | { 100 | $this->store->tags([$this->cacheTag])->flush(); 101 | } 102 | 103 | /** 104 | * Returns a unique cache key. 105 | * 106 | * @param string $locale 107 | * @param string $group 108 | * @param string $namespace 109 | * @return string 110 | */ 111 | protected function getKey($locale, $group, $namespace) 112 | { 113 | return md5("{$this->cacheTag}-{$locale}-{$group}-{$namespace}"); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Commands/CacheFlushCommand.php: -------------------------------------------------------------------------------- 1 | cacheRepository = $cacheRepository; 33 | $this->cacheEnabled = $cacheEnabled; 34 | } 35 | 36 | /** 37 | * Execute the console command. 38 | * 39 | * @return void 40 | */ 41 | public function fire() 42 | { 43 | if (!$this->cacheEnabled) { 44 | $this->info('The translation cache is disabled.'); 45 | } else { 46 | $this->cacheRepository->flushAll(); 47 | $this->info('Translation cache cleared.'); 48 | } 49 | } 50 | 51 | /** 52 | * Execute the console command for Laravel 5.5 53 | * this laravel version call handle intead of fire 54 | */ 55 | public function handle() 56 | { 57 | $this->fire(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Commands/FileLoaderCommand.php: -------------------------------------------------------------------------------- 1 | languageRepository = $languageRepository; 35 | $this->translationRepository = $translationRepository; 36 | $this->path = $translationsPath; 37 | $this->files = $files; 38 | $this->defaultLocale = $defaultLocale; 39 | } 40 | 41 | public function handle() 42 | { 43 | return $this->fire(); 44 | } 45 | 46 | /** 47 | * Execute the console command. 48 | * 49 | * @return void 50 | */ 51 | public function fire() 52 | { 53 | $this->loadLocaleDirectories($this->path); 54 | } 55 | 56 | /** 57 | * Loads all locale directories in the given path (/en, /es, /fr) as long as the locale corresponds to a language in the database. 58 | * If a vendor directory is found not inside another vendor directory, the files within it will be loaded with the corresponding namespace. 59 | * 60 | * @param string $path Full path to the root directory of the locale directories. Usually /path/to/laravel/resources/lang 61 | * @param string $namespace Namespace where the language files should be inserted. 62 | * @return void 63 | */ 64 | public function loadLocaleDirectories($path, $namespace = '*') 65 | { 66 | $availableLocales = $this->languageRepository->availableLocales(); 67 | $directories = $this->files->directories($path); 68 | foreach ($directories as $directory) { 69 | $locale = basename($directory); 70 | if (in_array($locale, $availableLocales)) { 71 | $this->loadDirectory($directory, $locale, $namespace); 72 | } 73 | if ($locale === 'vendor' && $namespace === '*') { 74 | $this->loadVendor($directory); 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * Load all vendor overriden localization packages. Calls loadLocaleDirectories with the appropriate namespace. 81 | * 82 | * @param string $path Path to vendor locale root, usually /path/to/laravel/resources/lang/vendor. 83 | * @see http://laravel.com/docs/5.1/localization#overriding-vendor-language-files 84 | * @return void 85 | */ 86 | public function loadVendor($path) 87 | { 88 | $directories = $this->files->directories($path); 89 | foreach ($directories as $directory) { 90 | $namespace = basename($directory); 91 | $this->loadLocaleDirectories($directory, $namespace); 92 | } 93 | } 94 | 95 | /** 96 | * Load all files inside a locale directory and its subdirectories. 97 | * 98 | * @param string $path Path to locale root. Ex: /path/to/laravel/resources/lang/en 99 | * @param string $locale Locale to apply when loading the localization files. 100 | * @param string $namespace Namespace to apply when loading the localization files ('*' by default, or the vendor package name if not) 101 | * @param string $group When loading from a subdirectory, the subdirectory's name must be prepended. For example: trans('subdir/file.entry'). 102 | * @return void 103 | */ 104 | public function loadDirectory($path, $locale, $namespace = '*', $group = '') 105 | { 106 | // Load all files inside subdirectories: 107 | $directories = $this->files->directories($path); 108 | foreach ($directories as $directory) { 109 | $directoryName = str_replace($path . '/', '', $directory); 110 | $dirGroup = $group . basename($directory) . '/'; 111 | $this->loadDirectory($directory, $locale, $namespace, $dirGroup); 112 | } 113 | 114 | // Load all files in root: 115 | $files = $this->files->files($path); 116 | foreach ($files as $file) { 117 | $this->loadFile($file, $locale, $namespace, $group); 118 | } 119 | } 120 | 121 | /** 122 | * Loads the given file into the database 123 | * 124 | * @param string $path Full path to the localization file. For example: /path/to/laravel/resources/lang/en/auth.php 125 | * @param string $locale 126 | * @param string $namespace 127 | * @param string $group Relative from the locale directory's root. For example subdirectory/subdir2/ 128 | * @return void 129 | */ 130 | public function loadFile($file, $locale, $namespace = '*', $group = '') 131 | { 132 | $group = $group . basename($file, '.php'); 133 | $translations = $this->files->getRequire($file); 134 | $this->translationRepository->loadArray($translations, $locale, $group, $namespace, $locale == $this->defaultLocale); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Facades/TranslationCache.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 47 | $this->fallback = $fallback; 48 | $this->cacheTimeout = $cacheTimeout; 49 | } 50 | 51 | /** 52 | * Load the messages for the given locale. 53 | * 54 | * @param string $locale 55 | * @param string $group 56 | * @param string $namespace 57 | * @return array 58 | */ 59 | public function loadSource($locale, $group, $namespace = '*') 60 | { 61 | if ($this->cache->has($locale, $group, $namespace)) { 62 | return $this->cache->get($locale, $group, $namespace); 63 | } else { 64 | $source = $this->fallback->load($locale, $group, $namespace); 65 | $this->cache->put($locale, $group, $namespace, $source, $this->cacheTimeout); 66 | return $source; 67 | } 68 | } 69 | 70 | /** 71 | * Add a new namespace to the loader. 72 | * 73 | * @param string $namespace 74 | * @param string $hint 75 | * @return void 76 | */ 77 | public function addNamespace($namespace, $hint) 78 | { 79 | $this->fallback->addNamespace($namespace, $hint); 80 | } 81 | 82 | /** 83 | * Add a new JSON path to the loader. 84 | * 85 | * @param string $path 86 | * @return void 87 | */ 88 | public function addJsonPath($path) 89 | { 90 | // 91 | } 92 | 93 | /** 94 | * Get an array of all the registered namespaces. 95 | * 96 | * @return array 97 | */ 98 | public function namespaces() 99 | { 100 | return $this->fallback->namespaces(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Loaders/DatabaseLoader.php: -------------------------------------------------------------------------------- 1 | translationRepository = $translationRepository; 30 | } 31 | 32 | /** 33 | * Load the messages strictly for the given locale. 34 | * 35 | * @param string $locale 36 | * @param string $group 37 | * @param string $namespace 38 | * @return array 39 | */ 40 | public function loadSource($locale, $group, $namespace = '*') 41 | { 42 | $dotArray = $this->translationRepository->loadSource($locale, $namespace, $group); 43 | $undot = []; 44 | foreach ($dotArray as $item => $text) { 45 | Arr::set($undot, $item, $text); 46 | } 47 | return $undot; 48 | } 49 | 50 | /** 51 | * Add a new namespace to the loader. 52 | * 53 | * @param string $namespace 54 | * @param string $hint 55 | * @return void 56 | */ 57 | public function addNamespace($namespace, $hint) 58 | { 59 | $this->hints[$namespace] = $hint; 60 | } 61 | 62 | /** 63 | * Add a new JSON path to the loader. 64 | * 65 | * @param string $path 66 | * @return void 67 | */ 68 | public function addJsonPath($path) 69 | { 70 | // 71 | } 72 | 73 | /** 74 | * Get an array of all the registered namespaces. 75 | * 76 | * @return array 77 | */ 78 | public function namespaces() 79 | { 80 | return $this->hints; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Loaders/FileLoader.php: -------------------------------------------------------------------------------- 1 | laravelFileLoader = $laravelFileLoader; 32 | } 33 | 34 | /** 35 | * Load the messages strictly for the given locale without checking the cache or in case of a cache miss. 36 | * 37 | * @param string $locale 38 | * @param string $group 39 | * @param string $namespace 40 | * @return array 41 | */ 42 | public function loadSource($locale, $group, $namespace = '*') 43 | { 44 | return $this->laravelFileLoader->load($locale, $group, $namespace); 45 | } 46 | 47 | /** 48 | * Add a new namespace to the loader. 49 | * 50 | * @param string $namespace 51 | * @param string $hint 52 | * @return void 53 | */ 54 | public function addNamespace($namespace, $hint) 55 | { 56 | $this->hints[$namespace] = $hint; 57 | $this->laravelFileLoader->addNamespace($namespace, $hint); 58 | } 59 | 60 | /** 61 | * Add a new JSON path to the loader. 62 | * 63 | * @param string $path 64 | * @return void 65 | */ 66 | public function addJsonPath($path) 67 | { 68 | $this->laravelFileLoader->addJsonPath($path); 69 | } 70 | 71 | /** 72 | * Get an array of all the registered namespaces. 73 | * 74 | * @return array 75 | */ 76 | public function namespaces() 77 | { 78 | return $this->hints; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Loaders/Loader.php: -------------------------------------------------------------------------------- 1 | defaultLocale = $defaultLocale; 27 | } 28 | 29 | /** 30 | * Load the messages for the given locale. 31 | * 32 | * @param string $locale 33 | * @param string $group 34 | * @param string $namespace 35 | * @return array 36 | */ 37 | public function load($locale, $group, $namespace = null) 38 | { 39 | if ($locale != $this->defaultLocale) { 40 | return array_replace_recursive( 41 | $this->loadSource($this->defaultLocale, $group, $namespace), 42 | $this->loadSource($locale, $group, $namespace) 43 | ); 44 | } 45 | return $this->loadSource($locale, $group, $namespace); 46 | } 47 | 48 | /** 49 | * Load the messages for the given locale from the loader source (cache, file, database, etc...) 50 | * 51 | * @param string $locale 52 | * @param string $group 53 | * @param string $namespace 54 | * @return array 55 | */ 56 | abstract public function loadSource($locale, $group, $namespace = null); 57 | 58 | /** 59 | * Add a new namespace to the loader. 60 | * 61 | * @param string $namespace 62 | * @param string $hint 63 | * @return void 64 | */ 65 | abstract public function addNamespace($namespace, $hint); 66 | 67 | /** 68 | * Add a new JSON path to the loader. 69 | * 70 | * @param string $path 71 | * @return void 72 | **/ 73 | abstract public function addJsonPath($path); 74 | 75 | /** 76 | * Get an array of all the registered namespaces. 77 | * 78 | * @return array 79 | */ 80 | abstract public function namespaces(); 81 | } 82 | -------------------------------------------------------------------------------- /src/Loaders/MixedLoader.php: -------------------------------------------------------------------------------- 1 | primaryLoader = $primaryLoader; 34 | $this->secondaryLoader = $secondaryLoader; 35 | } 36 | 37 | /** 38 | * Load the messages strictly for the given locale. 39 | * 40 | * @param string $locale 41 | * @param string $group 42 | * @param string $namespace 43 | * @return array 44 | */ 45 | public function loadSource($locale, $group, $namespace = '*') 46 | { 47 | return array_replace_recursive( 48 | $this->secondaryLoader->loadSource($locale, $group, $namespace), 49 | $this->primaryLoader->loadSource($locale, $group, $namespace) 50 | ); 51 | } 52 | 53 | /** 54 | * Add a new namespace to the loader. 55 | * 56 | * @param string $namespace 57 | * @param string $hint 58 | * @return void 59 | */ 60 | public function addNamespace($namespace, $hint) 61 | { 62 | $this->hints[$namespace] = $hint; 63 | $this->primaryLoader->addNamespace($namespace, $hint); 64 | $this->secondaryLoader->addNamespace($namespace, $hint); 65 | } 66 | 67 | /** 68 | * Add a new JSON path to the loader. 69 | * 70 | * @param string $path 71 | * @return void 72 | */ 73 | public function addJsonPath($path) 74 | { 75 | // 76 | } 77 | 78 | /** 79 | * Get an array of all the registered namespaces. 80 | * 81 | * @return array 82 | */ 83 | public function namespaces() 84 | { 85 | return $this->hints; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Middleware/TranslationMiddleware.php: -------------------------------------------------------------------------------- 1 | uriLocalizer = $uriLocalizer; 25 | $this->languageRepository = $languageRepository; 26 | $this->config = $config; 27 | $this->viewFactory = $viewFactory; 28 | $this->app = $app; 29 | } 30 | 31 | /** 32 | * Handle an incoming request. 33 | * 34 | * @param \Illuminate\Http\Request $request 35 | * @param \Closure $next 36 | * @param integer $segment Index of the segment containing locale info 37 | * @return mixed 38 | */ 39 | public function handle($request, Closure $next, $segment = 0) 40 | { 41 | // Ignores all non GET requests: 42 | if ($request->method() !== 'GET') { 43 | return $next($request); 44 | } 45 | 46 | $currentUrl = $request->getUri(); 47 | $uriLocale = $this->uriLocalizer->getLocaleFromUrl($currentUrl, $segment); 48 | $defaultLocale = $this->config->get('app.locale'); 49 | 50 | // If a locale was set in the url: 51 | if ($uriLocale) { 52 | $currentLanguage = $this->languageRepository->findByLocale($uriLocale); 53 | $selectableLanguages = $this->languageRepository->allExcept($uriLocale); 54 | $altLocalizedUrls = []; 55 | foreach ($selectableLanguages as $lang) { 56 | $altLocalizedUrls[] = [ 57 | 'locale' => $lang->locale, 58 | 'name' => $lang->name, 59 | 'url' => $this->uriLocalizer->localize($currentUrl, $lang->locale, $segment), 60 | ]; 61 | } 62 | 63 | // Set app locale 64 | $this->app->setLocale($uriLocale); 65 | 66 | // Share language variable with views: 67 | $this->viewFactory->share('currentLanguage', $currentLanguage); 68 | $this->viewFactory->share('selectableLanguages', $selectableLanguages); 69 | $this->viewFactory->share('altLocalizedUrls', $altLocalizedUrls); 70 | 71 | // Set locale in session: 72 | if ($request->hasSession() && $request->session()->get('waavi.translation.locale') !== $uriLocale) { 73 | $request->session()->put('waavi.translation.locale', $uriLocale); 74 | } 75 | return $next($request); 76 | } 77 | 78 | // If no locale was set in the url, check the session locale 79 | if ($request->hasSession() && $sessionLocale = $request->session()->get('waavi.translation.locale')) { 80 | if ($this->languageRepository->isValidLocale($sessionLocale)) { 81 | return redirect()->to($this->uriLocalizer->localize($currentUrl, $sessionLocale, $segment)); 82 | } 83 | } 84 | 85 | // If no locale was set in the url, check the browser's locale: 86 | $browserLocale = substr($request->server('HTTP_ACCEPT_LANGUAGE'), 0, 2); 87 | if ($this->languageRepository->isValidLocale($browserLocale)) { 88 | return redirect()->to($this->uriLocalizer->localize($currentUrl, $browserLocale, $segment)); 89 | } 90 | 91 | // If not, redirect to the default locale: 92 | // Keep flash data. 93 | if ($request->hasSession()) { 94 | $request->session()->reflash(); 95 | } 96 | return redirect()->to($this->uriLocalizer->localize($currentUrl, $defaultLocale, $segment)); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Models/Language.php: -------------------------------------------------------------------------------- 1 | setConnection(config('translator.connection')); 30 | } 31 | 32 | /** 33 | * Each language may have several translations. 34 | */ 35 | public function translations() 36 | { 37 | return $this->hasMany(Translation::class, 'locale', 'locale'); 38 | } 39 | 40 | /** 41 | * Returns the name of this language in the current selected language. 42 | * 43 | * @return string 44 | */ 45 | public function getLanguageCodeAttribute() 46 | { 47 | return "languages.{$this->locale}"; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/Models/Translation.php: -------------------------------------------------------------------------------- 1 | setConnection(config('translator.connection')); 24 | } 25 | 26 | /** 27 | * Each translation belongs to a language. 28 | */ 29 | public function language() 30 | { 31 | return $this->belongsTo(Language::class, 'locale', 'locale'); 32 | } 33 | 34 | /** 35 | * Returns the full translation code for an entry: namespace.group.item 36 | * @return string 37 | */ 38 | public function getCodeAttribute() 39 | { 40 | return $this->namespace === '*' ? "{$this->group}.{$this->item}" : "{$this->namespace}::{$this->group}.{$this->item}"; 41 | } 42 | 43 | /** 44 | * Flag this entry as Reviewed 45 | * @return void 46 | */ 47 | public function flagAsReviewed() 48 | { 49 | $this->unstable = 0; 50 | } 51 | 52 | /** 53 | * Set the translation to the locked state 54 | * @return void 55 | */ 56 | public function lock() 57 | { 58 | $this->locked = 1; 59 | } 60 | 61 | /** 62 | * Check if the translation is locked 63 | * @return boolean 64 | */ 65 | public function isLocked() 66 | { 67 | return (boolean) $this->locked; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Repositories/LanguageRepository.php: -------------------------------------------------------------------------------- 1 | model = $model; 62 | $this->validator = $app['validator']; 63 | $config = $app['config']; 64 | $this->defaultLocale = $config->get('app.locale'); 65 | $this->defaultAvailableLocales = $config->get('translator.available_locales', []); 66 | $this->config = $config; 67 | } 68 | 69 | /** 70 | * Insert a new language entry into the database. 71 | * If the attributes are not valid, a null response is given and the errors can be retrieved through validationErrors() 72 | * 73 | * @param array $attributes Model attributes 74 | * @return boolean 75 | */ 76 | public function create(array $attributes) 77 | { 78 | return $this->validate($attributes) ? Language::create($attributes) : null; 79 | } 80 | 81 | /** 82 | * Insert a new language entry into the database. 83 | * If the attributes are not valid, a null response is given and the errors can be retrieved through validationErrors() 84 | * 85 | * @param array $attributes Model attributes 86 | * @return boolean 87 | */ 88 | public function update(array $attributes) 89 | { 90 | return $this->validate($attributes) ? (boolean) Language::where('id', $attributes['id'])->update($attributes) : false; 91 | } 92 | 93 | /** 94 | * Find a Language by its locale 95 | * 96 | * @return Language | null 97 | */ 98 | public function findByLocale($locale) 99 | { 100 | return $this->model->where('locale', $locale)->first(); 101 | } 102 | 103 | /** 104 | * Find a deleted Language by its locale 105 | * 106 | * @return Language | null 107 | */ 108 | public function findTrashedByLocale($locale) 109 | { 110 | return $this->model->onlyTrashed()->where('locale', $locale)->first(); 111 | } 112 | 113 | /** 114 | * Find all Languages except the one with the specified locale. 115 | * 116 | * @return Language | null 117 | */ 118 | public function allExcept($locale) 119 | { 120 | return $this->model->where('locale', '!=', $locale)->get(); 121 | } 122 | 123 | /** 124 | * Returns a list of all available locales. 125 | * 126 | * @return array 127 | */ 128 | public function availableLocales() 129 | { 130 | if ($this->config->has('translator.locales')) { 131 | return $this->config->get('translator.locales'); 132 | } 133 | 134 | if ($this->config->get('translator.source') !== 'files') { 135 | if ($this->tableExists()) { 136 | $locales = $this->model->distinct()->get()->pluck('locale')->toArray(); 137 | $this->config->set('translator.locales', $locales); 138 | return $locales; 139 | } 140 | } 141 | 142 | return $this->defaultAvailableLocales; 143 | } 144 | 145 | /** 146 | * Checks if a language with the given locale exists. 147 | * 148 | * @return boolean 149 | */ 150 | public function isValidLocale($locale) 151 | { 152 | return $this->model->whereLocale($locale)->count() > 0; 153 | } 154 | 155 | /** 156 | * Compute percentage translate of the given language. 157 | * 158 | * @param string $locale 159 | * @param string $referenceLocale 160 | * @return int 161 | */ 162 | public function percentTranslated($locale) 163 | { 164 | $lang = $this->findByLocale($locale); 165 | $referenceLang = $this->findByLocale($this->defaultLocale); 166 | 167 | $langEntries = $lang->translations()->count(); 168 | $referenceEntries = $referenceLang->translations()->count(); 169 | 170 | return $referenceEntries > 0 ? (int) round($langEntries * 100 / $referenceEntries) : 0; 171 | } 172 | 173 | /** 174 | * Validate the given attributes 175 | * 176 | * @param array $attributes 177 | * @return boolean 178 | */ 179 | public function validate(array $attributes) 180 | { 181 | $id = Arr::get($attributes, 'id', 'NULL'); 182 | $table = $this->model->getTable(); 183 | $rules = [ 184 | 'locale' => "required|unique:{$table},locale,{$id}", 185 | 'name' => "required|unique:{$table},name,{$id}", 186 | ]; 187 | $validator = $this->validator->make($attributes, $rules); 188 | if ($validator->fails()) { 189 | $this->errors = $validator->errors(); 190 | return false; 191 | } 192 | return true; 193 | } 194 | 195 | /** 196 | * Returns the validations errors of the last action executed. 197 | * 198 | * @return \Illuminate\Support\MessageBag 199 | */ 200 | public function validationErrors() 201 | { 202 | return $this->errors; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/Repositories/Repository.php: -------------------------------------------------------------------------------- 1 | model; 13 | } 14 | 15 | /** 16 | * Check if the model's table exists 17 | * 18 | * @return boolean 19 | */ 20 | public function tableExists() 21 | { 22 | return $this->model->getConnection()->getSchemaBuilder()->hasTable($this->model->getTable()); 23 | } 24 | 25 | /** 26 | * Retrieve all records. 27 | * 28 | * @param array $related Related object to include. 29 | * @param integer $perPage Number of records to retrieve per page. If zero the whole result set is returned. 30 | * @return \Illuminate\Database\Eloquent\Model 31 | */ 32 | public function all($related = [], $perPage = 0) 33 | { 34 | $results = $this->model->with($related)->orderBy('created_at', 'DESC'); 35 | return $perPage ? $results->paginate($perPage) : $results->get(); 36 | } 37 | 38 | /** 39 | * Retrieve all trashed. 40 | * 41 | * @param array $related Related object to include. 42 | * @param integer $perPage Number of records to retrieve per page. If zero the whole result set is returned. 43 | * @return \Illuminate\Database\Eloquent\Model 44 | */ 45 | public function trashed($related = [], $perPage = 0) 46 | { 47 | $trashed = $this->model->onlyTrashed()->with($related); 48 | return $perPage ? $trashed->paginate($perPage) : $trashed->get(); 49 | } 50 | 51 | /** 52 | * Retrieve a single record by id. 53 | * 54 | * @param integer $id 55 | * @return \Illuminate\Database\Eloquent\Model 56 | */ 57 | public function find($id, $related = []) 58 | { 59 | return $this->model->with($related)->find($id); 60 | } 61 | 62 | /** 63 | * Retrieve a single record by id. 64 | * 65 | * @param integer $id 66 | * @return \Illuminate\Database\Eloquent\Model 67 | */ 68 | public function findTrashed($id, $related = []) 69 | { 70 | return $this->model->onlyTrashed()->with($related)->find($id); 71 | } 72 | 73 | /** 74 | * Remove a record. 75 | * 76 | * @param \Illuminate\Database\Eloquent\Model $model 77 | * @return boolean 78 | */ 79 | public function delete($id) 80 | { 81 | $model = $this->model->where('id', $id)->first(); 82 | if (!$model) { 83 | return false; 84 | } 85 | return $model->delete(); 86 | } 87 | 88 | /** 89 | * Restore a record. 90 | * 91 | * @param int $id 92 | * @return boolean 93 | */ 94 | public function restore($id) 95 | { 96 | $model = $this->findTrashed($id); 97 | if ($model) { 98 | $model->restore(); 99 | } 100 | return $model; 101 | } 102 | 103 | /** 104 | * Returns total number of entries in DB. 105 | * 106 | * @return integer 107 | */ 108 | public function count() 109 | { 110 | return $this->model->count(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Repositories/TranslationRepository.php: -------------------------------------------------------------------------------- 1 | model = $model; 46 | $this->app = $app; 47 | $this->defaultLocale = $app['config']->get('app.locale'); 48 | $this->database = $app['db']; 49 | } 50 | 51 | /** 52 | * Insert a new translation into the database. 53 | * If the attributes are not valid, a null response is given and the errors can be retrieved through validationErrors() 54 | * 55 | * @param array $attributes Model attributes 56 | * @return boolean 57 | */ 58 | public function create(array $attributes) 59 | { 60 | return $this->validate($attributes) ? Translation::create($attributes) : null; 61 | } 62 | 63 | /** 64 | * Update a translation. 65 | * If the translation is locked, no update will be made. 66 | * 67 | * @param array $attributes Model attributes 68 | * @return boolean 69 | */ 70 | public function update($id, $text) 71 | { 72 | $translation = $this->find($id); 73 | if (!$translation || $translation->isLocked()) { 74 | return false; 75 | } 76 | $translation->text = $text; 77 | $saved = $translation->save(); 78 | if ($saved && $translation->locale === $this->defaultLocale) { 79 | $this->flagAsUnstable($translation->namespace, $translation->group, $translation->item); 80 | } 81 | return $saved; 82 | } 83 | 84 | /** 85 | * Update and lock translation. Locked translations will not be ovewritten when loading translation files into the database. 86 | * This will force and update if the translation is locked. 87 | * If the attributes are not valid, a null response is given and the errors can be retrieved through validationErrors() 88 | * 89 | * @param array $attributes Model attributes 90 | * @return boolean 91 | */ 92 | public function updateAndLock($id, $text) 93 | { 94 | $translation = $this->find($id); 95 | if (!$translation) { 96 | return false; 97 | } 98 | $translation->text = $text; 99 | $translation->lock(); 100 | $saved = $translation->save(); 101 | if ($saved && $translation->locale === $this->defaultLocale) { 102 | $this->flagAsUnstable($translation->namespace, $translation->group, $translation->item); 103 | } 104 | return $saved; 105 | } 106 | 107 | /** 108 | * Insert or Update entry by translation code for the default locale. 109 | * 110 | * @param string $code 111 | * @param string $text 112 | * @return boolean 113 | */ 114 | public function updateDefaultByCode($code, $text) 115 | { 116 | list($namespace, $group, $item) = $this->parseCode($code); 117 | $locale = $this->defaultLocale; 118 | $translation = $this->model->whereLocale($locale)->whereNamespace($namespace)->whereGroup($group)->whereItem($item)->first(); 119 | if (!$translation) { 120 | return $this->create(compact('locale', 'namespace', 'group', 'item', 'text')); 121 | } 122 | return $this->update($translation->id, $text); 123 | } 124 | 125 | /** 126 | * Delete a translation. If the translation is of the default language, delete all translations with the same namespace, group and item 127 | * 128 | * @param integer $id 129 | * @return boolean 130 | */ 131 | public function delete($id) 132 | { 133 | $translation = $this->find($id); 134 | if (!$translation) { 135 | return false; 136 | } 137 | 138 | if ($translation->locale === $this->defaultLocale) { 139 | return $this->model->whereNamespace($translation->namespace)->whereGroup($translation->group)->whereItem($translation->item)->delete(); 140 | } else { 141 | return $translation->delete(); 142 | } 143 | } 144 | 145 | /** 146 | * Delete all entries by code 147 | * 148 | * @param string $code 149 | * @return boolean 150 | */ 151 | public function deleteByCode($code) 152 | { 153 | list($namespace, $group, $item) = $this->parseCode($code); 154 | $this->model->whereNamespace($namespace)->whereGroup($group)->whereItem($item)->delete(); 155 | } 156 | 157 | /** 158 | * Loads a localization array from a localization file into the databas. 159 | * 160 | * @param array $lines 161 | * @param string $locale 162 | * @param string $group 163 | * @param string $namespace 164 | * @return void 165 | */ 166 | public function loadArray(array $lines, $locale, $group, $namespace = '*') 167 | { 168 | // Transform the lines into a flat dot array: 169 | $lines = Arr::dot($lines); 170 | foreach ($lines as $item => $text) { 171 | if (is_string($text)) { 172 | // Check if the entry exists in the database: 173 | $translation = Translation::whereLocale($locale) 174 | ->whereNamespace($namespace) 175 | ->whereGroup($group) 176 | ->whereItem($item) 177 | ->first(); 178 | 179 | // If the translation already exists, we update the text: 180 | if ($translation && !$translation->isLocked()) { 181 | $translation->text = $text; 182 | $saved = $translation->save(); 183 | if ($saved && $translation->locale === $this->defaultLocale) { 184 | $this->flagAsUnstable($namespace, $group, $item); 185 | } 186 | } 187 | // If no entry was found, create it: 188 | else { 189 | $this->create(compact('locale', 'namespace', 'group', 'item', 'text')); 190 | } 191 | } 192 | } 193 | } 194 | 195 | /** 196 | * Return a list of translations for the given language. If perPage is > 0 a paginated list is returned with perPage items per page. 197 | * 198 | * @param string $locale 199 | * @return Translation 200 | */ 201 | public function allByLocale($locale, $perPage = 0) 202 | { 203 | $translations = $this->model->where('locale', $locale); 204 | return $perPage ? $translations->paginate($perPage) : $translations->get(); 205 | } 206 | 207 | /** 208 | * Return all items for a given locale, namespace and group 209 | * 210 | * @param string $locale 211 | * @param string $namespace 212 | * @param string $group 213 | * @return array 214 | */ 215 | public function getItems($locale, $namespace, $group) 216 | { 217 | return $this->model 218 | ->whereLocale($locale) 219 | ->whereNamespace($namespace) 220 | ->whereGroup($group) 221 | ->get() 222 | ->toArray(); 223 | } 224 | 225 | /** 226 | * Return all items formatted as if coming from a PHP language file. 227 | * 228 | * @param string $locale 229 | * @param string $namespace 230 | * @param string $group 231 | * @return array 232 | */ 233 | public function loadSource($locale, $namespace, $group) 234 | { 235 | return $this->model 236 | ->whereLocale($locale) 237 | ->whereNamespace($namespace) 238 | ->whereGroup($group) 239 | ->get() 240 | ->keyBy('item') 241 | ->map(function ($translation) { 242 | return $translation['text']; 243 | }) 244 | ->toArray(); 245 | } 246 | 247 | /** 248 | * Retrieve translations pending review for the given locale. 249 | * 250 | * @param string $locale 251 | * @param int $perPage Number of elements per page. 0 if all are wanted. 252 | * @return Translation 253 | */ 254 | public function pendingReview($locale, $perPage = 0) 255 | { 256 | $underReview = $this->model->whereLocale($locale)->whereUnstable(1); 257 | return $perPage ? $underReview->paginate($perPage) : $underReview->get(); 258 | } 259 | 260 | /** 261 | * Search for entries given a partial code and a locale 262 | * 263 | * @param string $locale 264 | * @param string $partialCode 265 | * @param integer $perPage 0 if all, > 0 if paginated list with that number of elements per page. 266 | * @return Translation 267 | */ 268 | public function search($locale, $partialCode, $perPage = 0) 269 | { 270 | // Get the namespace, if any: 271 | $colonIndex = stripos($partialCode, '::'); 272 | $query = $this->model->whereLocale($locale); 273 | if ($colonIndex === 0) { 274 | $query = $query->where('namespace', '!=', '*'); 275 | } elseif ($colonIndex > 0) { 276 | $namespace = substr($partialCode, 0, $colonIndex); 277 | $query = $query->where('namespace', 'like', "%{$namespace}%"); 278 | $partialCode = substr($partialCode, $colonIndex + 2); 279 | } 280 | 281 | // Divide the code in segments by . 282 | $elements = explode('.', $partialCode); 283 | foreach ($elements as $element) { 284 | if ($element) { 285 | $query = $query->where(function ($query) use ($element) { 286 | $query->where('group', 'like', "%{$element}%")->orWhere('item', 'like', "%{$element}%")->orWhere('text', 'like', "%{$element}%"); 287 | }); 288 | } 289 | } 290 | 291 | return $perPage ? $query->paginate($perPage) : $query->get(); 292 | } 293 | 294 | /** 295 | * List all entries in the default locale that do not exist for the target locale. 296 | * 297 | * @param string $locale Language to translate to. 298 | * @param integer $perPage If greater than zero, return a paginated list with $perPage items per page. 299 | * @param string $text [optional] Show only entries with the given text in them in the reference language. 300 | * @return Collection 301 | */ 302 | public function untranslated($locale, $perPage = 0, $text = null) 303 | { 304 | $ids = $this->untranslatedQuery($locale)->pluck('id'); 305 | 306 | $untranslated = $text ? $this->model->whereIn('id', $ids)->where('text', 'like', "%$text%") : $this->model->whereIn('id', $ids); 307 | 308 | return $perPage ? $untranslated->paginate($perPage) : $untranslated->get(); 309 | } 310 | 311 | /** 312 | * Find a random entry that is present in the default locale but not in the given one. 313 | * 314 | * @param string $locale Locale to translate to. 315 | * @return Translation 316 | */ 317 | public function randomUntranslated($locale) 318 | { 319 | return $this->untranslatedQuery($locale)->inRandomOrder()->take(1)->pluck('id'); 320 | } 321 | 322 | /** 323 | * Find a translation per namespace, group and item values 324 | * 325 | * @param string $locale 326 | * @param string $namespace 327 | * @param string $group 328 | * @param string $item 329 | * @return Translation 330 | */ 331 | public function findByLangCode($locale, $code) 332 | { 333 | list($namespace, $group, $item) = $this->parseCode($code); 334 | return $this->model->whereLocale($locale)->whereNamespace($namespace)->whereGroup($group)->whereItem($item)->first(); 335 | } 336 | 337 | /** 338 | * Find a translation per namespace, group and item values 339 | * 340 | * @param string $locale 341 | * @param string $namespace 342 | * @param string $group 343 | * @param string $item 344 | * @return Translation 345 | */ 346 | public function findByCode($locale, $namespace, $group, $item) 347 | { 348 | return $this->model->whereLocale($locale)->whereNamespace($namespace)->whereGroup($group)->whereItem($item)->first(); 349 | } 350 | 351 | /** 352 | * Check if there are existing translations for the given text in the given locale for the target locale. 353 | * 354 | * @param string $text 355 | * @param string $textLocale 356 | * @param string $targetLocale 357 | * @return array 358 | */ 359 | public function translateText($text, $textLocale, $targetLocale) 360 | { 361 | $table = $this->model->getTable(); 362 | 363 | return $this->model 364 | ->newQuery() 365 | ->select($table . '.text') 366 | ->from($table) 367 | ->leftJoin("{$table} as e", function ($join) use ($table, $text, $textLocale) { 368 | $join->on('e.namespace', '=', "{$table}.namespace") 369 | ->on('e.group', '=', "{$table}.group") 370 | ->on('e.item', '=', "{$table}.item"); 371 | }) 372 | ->where("{$table}.locale", $targetLocale) 373 | ->where('e.locale', $textLocale) 374 | ->where('e.text', $text) 375 | ->get() 376 | ->pluck('text') 377 | ->unique() 378 | ->toArray(); 379 | } 380 | 381 | /** 382 | * Flag all entries with the given namespace, group and item and locale other than default as pending review. 383 | * This is used when an entry for the default locale is updated. 384 | * 385 | * @param Translation $entry 386 | * @return boolean 387 | */ 388 | public function flagAsUnstable($namespace, $group, $item) 389 | { 390 | $this->model->whereNamespace($namespace)->whereGroup($group)->whereItem($item)->where('locale', '!=', $this->defaultLocale)->update(['unstable' => '1']); 391 | } 392 | 393 | /** 394 | * Flag the entry with the given id as reviewed. 395 | * 396 | * @param integer $id 397 | * @return boolean 398 | */ 399 | public function flagAsReviewed($id) 400 | { 401 | $this->model->where('id', $id)->update(['unstable' => '0']); 402 | } 403 | 404 | /** 405 | * Validate the given attributes 406 | * 407 | * @param array $attributes 408 | * @return boolean 409 | */ 410 | public function validate(array $attributes) 411 | { 412 | $table = $this->model->getTable(); 413 | $locale = Arr::get($attributes, 'locale', ''); 414 | $namespace = Arr::get($attributes, 'namespace', ''); 415 | $group = Arr::get($attributes, 'group', ''); 416 | $rules = [ 417 | 'locale' => 'required', 418 | 'namespace' => 'required', 419 | 'group' => 'required', 420 | 'item' => "required|unique:{$table},item,NULL,id,locale,{$locale},namespace,{$namespace},group,{$group}", 421 | 'text' => '', // Translations may be empty 422 | ]; 423 | $validator = $this->app['validator']->make($attributes, $rules); 424 | if ($validator->fails()) { 425 | $this->errors = $validator->errors(); 426 | return false; 427 | } 428 | return true; 429 | } 430 | 431 | /** 432 | * Returns the validations errors of the last action executed. 433 | * 434 | * @return \Illuminate\Support\MessageBag 435 | */ 436 | public function validationErrors() 437 | { 438 | return $this->errors; 439 | } 440 | 441 | /** 442 | * Parse a translation code into its components 443 | * 444 | * @param string $code 445 | * @return boolean 446 | */ 447 | public function parseCode($code) 448 | { 449 | $segments = (new NamespacedItemResolver)->parseKey($code); 450 | 451 | if (is_null($segments[0])) { 452 | $segments[0] = '*'; 453 | } 454 | 455 | return $segments; 456 | } 457 | 458 | /** 459 | * Create and return a new query to identify untranslated records. 460 | * 461 | * @param string $locale 462 | * @return \Illuminate\Database\Query\Builder 463 | */ 464 | protected function untranslatedQuery($locale) 465 | { 466 | $table = $this->model->getTable(); 467 | 468 | return $this->database->table("$table as $table") 469 | ->select("$table.id") 470 | ->leftJoin("$table as e", function (JoinClause $query) use ($table, $locale) { 471 | $query->on('e.namespace', '=', "$table.namespace") 472 | ->on('e.group', '=', "$table.group") 473 | ->on('e.item', '=', "$table.item") 474 | ->where('e.locale', '=', $locale); 475 | }) 476 | ->where("$table.locale", $this->defaultLocale) 477 | ->whereNull("e.id"); 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /src/Routes/ResourceRegistrar.php: -------------------------------------------------------------------------------- 1 | languageRepository = $languageRepository; 28 | } 29 | 30 | /** 31 | * Get the resource name for a grouped resource. 32 | * 33 | * @param string $prefix 34 | * @param string $resource 35 | * @param string $method 36 | * @return string 37 | */ 38 | protected function getGroupResourceName($prefix, $resource, $method) 39 | { 40 | $availableLocales = $this->languageRepository->availableLocales(); 41 | 42 | // Remove segments from group prefix that are equal to one of the available locales: 43 | $groupSegments = explode('/', $this->router->getLastGroupPrefix()); 44 | $groupSegments = array_filter($groupSegments, function ($segment) use ($availableLocales) { 45 | return !in_array($segment, $availableLocales); 46 | }); 47 | $group = trim(implode('.', $groupSegments), '.'); 48 | 49 | if (empty($group)) { 50 | return trim("{$prefix}{$resource}.{$method}", '.'); 51 | } 52 | 53 | return trim("{$prefix}{$group}.{$resource}.{$method}", '.'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Traits/Translatable.php: -------------------------------------------------------------------------------- 1 | rawValueRequested($attribute)) { 29 | $rawAttribute = snake_case(str_replace('raw', '', $attribute)); 30 | return $this->attributes[$rawAttribute]; 31 | } 32 | // Return the translation for the given attribute if available 33 | if ($this->isTranslated($attribute)) { 34 | return $this->translate($attribute); 35 | } 36 | // Return parent 37 | return parent::getAttribute($attribute); 38 | } 39 | 40 | /** 41 | * Hijack Eloquent's setAttribute to create a Language Entry, or update the existing one, when setting the value of this attribute. 42 | * 43 | * @param string $attribute Attribute name 44 | * @param string $value Text value in default locale. 45 | * @return void 46 | */ 47 | public function setAttribute($attribute, $value) 48 | { 49 | if ($this->isTranslatable($attribute) && !empty($value)) { 50 | // If a translation code has not yet been set, generate one: 51 | if (!$this->translationCodeFor($attribute)) { 52 | $reflected = new \ReflectionClass($this); 53 | $group = 'translatable'; 54 | $item = strtolower($reflected->getShortName()) . '.' . strtolower($attribute) . '.' . Str::random(); 55 | $this->attributes["{$attribute}_translation"] = "$group.$item"; 56 | } 57 | } 58 | return parent::setAttribute($attribute, $value); 59 | } 60 | 61 | /** 62 | * Extend parent's attributesToArray so that _translation attributes do not appear in array, and translatable attributes are translated. 63 | * 64 | * @return array 65 | */ 66 | public function attributesToArray() 67 | { 68 | $attributes = parent::attributesToArray(); 69 | 70 | foreach ($this->translatableAttributes as $translatableAttribute) { 71 | if (isset($attributes[$translatableAttribute])) { 72 | $attributes[$translatableAttribute] = $this->translate($translatableAttribute); 73 | } 74 | unset($attributes["{$translatableAttribute}_translation"]); 75 | } 76 | 77 | return $attributes; 78 | } 79 | 80 | /** 81 | * Get the set translation code for the give attribute 82 | * 83 | * @param string $attribute 84 | * @return string 85 | */ 86 | public function translationCodeFor($attribute) 87 | { 88 | return Arr::get($this->attributes, "{$attribute}_translation", false); 89 | } 90 | 91 | /** 92 | * Check if the attribute being queried is the raw value of a translatable attribute. 93 | * 94 | * @param string $attribute 95 | * @return boolean 96 | */ 97 | public function rawValueRequested($attribute) 98 | { 99 | if (strrpos($attribute, 'raw') === 0) { 100 | $rawAttribute = snake_case(str_replace('raw', '', $attribute)); 101 | return $this->isTranslatable($rawAttribute); 102 | } 103 | return false; 104 | } 105 | 106 | /** 107 | * @param $attribute 108 | */ 109 | public function getRawAttribute($attribute) 110 | { 111 | return Arr::get($this->attributes, $attribute, ''); 112 | } 113 | 114 | /** 115 | * Return the translation related to a translatable attribute. 116 | * 117 | * @param string $attribute 118 | * @return Translation 119 | */ 120 | public function translate($attribute) 121 | { 122 | $translationCode = $this->translationCodeFor($attribute); 123 | $translation = $translationCode ? trans($translationCode) : false; 124 | return $translation ?: parent::getAttribute($attribute); 125 | } 126 | 127 | /** 128 | * Check if an attribute is translatable. 129 | * 130 | * @return boolean 131 | */ 132 | public function isTranslatable($attribute) 133 | { 134 | return in_array($attribute, $this->translatableAttributes); 135 | } 136 | 137 | /** 138 | * Check if a translation exists for the given attribute. 139 | * 140 | * @param string $attribute 141 | * @return boolean 142 | */ 143 | public function isTranslated($attribute) 144 | { 145 | return $this->isTranslatable($attribute) && isset($this->attributes["{$attribute}_translation"]); 146 | } 147 | 148 | /** 149 | * Return the translatable attributes array 150 | * 151 | * @return array 152 | */ 153 | public function translatableAttributes() 154 | { 155 | return $this->translatableAttributes; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Traits/TranslatableObserver.php: -------------------------------------------------------------------------------- 1 | translatableAttributes() as $attribute) { 19 | // If the value of the translatable attribute has changed: 20 | if ($model->isDirty($attribute)) { 21 | $translationRepository->updateDefaultByCode($model->translationCodeFor($attribute), $model->getRawAttribute($attribute)); 22 | } 23 | } 24 | $cacheRepository->flush(config('app.locale'), 'translatable', '*'); 25 | } 26 | 27 | /** 28 | * Delete translations when model is deleted. 29 | * 30 | * @param Model $model 31 | * @return void 32 | */ 33 | public function deleted($model) 34 | { 35 | $translationRepository = \App::make(TranslationRepository::class); 36 | foreach ($model->translatableAttributes() as $attribute) { 37 | $translationRepository->deleteByCode($model->translationCodeFor($attribute)); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/TranslationServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 29 | __DIR__ . '/../config/translator.php' => config_path('translator.php'), 30 | ]); 31 | $this->loadMigrationsFrom(__DIR__ . '/../database/migrations/'); 32 | } 33 | 34 | /** 35 | * Register the service provider. 36 | * 37 | * @return void 38 | */ 39 | public function register() 40 | { 41 | $this->mergeConfigFrom(__DIR__ . '/../config/translator.php', 'translator'); 42 | 43 | parent::register(); 44 | $this->registerCacheRepository(); 45 | $this->registerFileLoader(); 46 | $this->registerCacheFlusher(); 47 | $this->app->singleton('translation.uri.localizer', UriLocalizer::class); 48 | $this->app[\Illuminate\Routing\Router::class]->aliasMiddleware('localize', TranslationMiddleware::class); 49 | // Fix issue with laravel prepending the locale to localize resource routes: 50 | $this->app->bind('Illuminate\Routing\ResourceRegistrar', ResourceRegistrar::class); 51 | } 52 | 53 | /** 54 | * IOC alias provided by this Service Provider. 55 | * 56 | * @return array 57 | */ 58 | public function provides() 59 | { 60 | return array_merge(parent::provides(), ['translation.cache.repository', 'translation.uri.localizer', 'translation.loader']); 61 | } 62 | 63 | /** 64 | * Register the translation line loader. 65 | * 66 | * @return void 67 | */ 68 | protected function registerLoader() 69 | { 70 | $app = $this->app; 71 | $this->app->singleton('translation.loader', function ($app) { 72 | $defaultLocale = $app['config']->get('app.locale'); 73 | $loader = null; 74 | $source = $app['config']->get('translator.source'); 75 | 76 | switch ($source) { 77 | case 'mixed': 78 | $laravelFileLoader = new LaravelFileLoader($app['files'], $app->basePath() . '/resources/lang'); 79 | $fileLoader = new FileLoader($defaultLocale, $laravelFileLoader); 80 | $databaseLoader = new DatabaseLoader($defaultLocale, $app->make(TranslationRepository::class)); 81 | $loader = new MixedLoader($defaultLocale, $fileLoader, $databaseLoader); 82 | break; 83 | case 'mixed_db': 84 | $laravelFileLoader = new LaravelFileLoader($app['files'], $app->basePath() . '/resources/lang'); 85 | $fileLoader = new FileLoader($defaultLocale, $laravelFileLoader); 86 | $databaseLoader = new DatabaseLoader($defaultLocale, $app->make(TranslationRepository::class)); 87 | $loader = new MixedLoader($defaultLocale, $databaseLoader, $fileLoader); 88 | break; 89 | case 'database': 90 | $loader = new DatabaseLoader($defaultLocale, $app->make(TranslationRepository::class)); 91 | break; 92 | default:case 'files': 93 | $laravelFileLoader = new LaravelFileLoader($app['files'], $app->basePath() . '/resources/lang'); 94 | $loader = new FileLoader($defaultLocale, $laravelFileLoader); 95 | break; 96 | } 97 | if ($app['config']->get('translator.cache.enabled')) { 98 | $loader = new CacheLoader($defaultLocale, $app['translation.cache.repository'], $loader, $app['config']->get('translator.cache.timeout')); 99 | } 100 | return $loader; 101 | }); 102 | } 103 | 104 | /** 105 | * Register the translation cache repository 106 | * 107 | * @return void 108 | */ 109 | public function registerCacheRepository() 110 | { 111 | $this->app->singleton('translation.cache.repository', function ($app) { 112 | $cacheStore = $app['cache']->getStore(); 113 | return CacheRepositoryFactory::make($cacheStore, $app['config']->get('translator.cache.suffix')); 114 | }); 115 | } 116 | 117 | /** 118 | * Register the translator:load language file loader. 119 | * 120 | * @return void 121 | */ 122 | protected function registerFileLoader() 123 | { 124 | $app = $this->app; 125 | $defaultLocale = $app['config']->get('app.locale'); 126 | $languageRepository = $app->make(LanguageRepository::class); 127 | $translationRepository = $app->make(TranslationRepository::class); 128 | $translationsPath = $app->basePath() . '/resources/lang'; 129 | $command = new FileLoaderCommand($languageRepository, $translationRepository, $app['files'], $translationsPath, $defaultLocale); 130 | 131 | $this->app['command.translator:load'] = $command; 132 | $this->commands('command.translator:load'); 133 | } 134 | 135 | /** 136 | * Flushes the translation cache 137 | * 138 | * @return void 139 | */ 140 | public function registerCacheFlusher() 141 | { 142 | //$cacheStore = $this->app['cache']->getStore(); 143 | //$cacheRepository = CacheRepositoryFactory::make($cacheStore, $this->app['config']->get('translator.cache.suffix')); 144 | $command = new CacheFlushCommand($this->app['translation.cache.repository'], $this->app['config']->get('translator.cache.enabled')); 145 | 146 | $this->app['command.translator:flush'] = $command; 147 | $this->commands('command.translator:flush'); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/UriLocalizer.php: -------------------------------------------------------------------------------- 1 | request = $request; 16 | $this->availableLocales = $languageRepository->availableLocales(); 17 | } 18 | 19 | /** 20 | * Returns the locale present in the current url, if any. 21 | * 22 | * @param integer $segment Index of the segment containing locale info 23 | * @return string 24 | */ 25 | public function localeFromRequest($segment = 0) 26 | { 27 | $url = $this->request->getUri(); 28 | return $this->getLocaleFromUrl($url, $segment); 29 | } 30 | 31 | /** 32 | * Localizes the given url to the given locale. Removes domain if present. 33 | * Ex: /home => /es/home, /en/home => /es/home, http://www.domain.com/en/home => /en/home, https:://domain.com/ => /en 34 | * If a non zero segment index is given, and the url doesn't have enought segments, the url is unchanged. 35 | * 36 | * @param string $url 37 | * @param string $locale 38 | * @param integer $segment Index of the segment containing locale info 39 | * @return string 40 | */ 41 | public function localize($url, $locale, $segment = 0) 42 | { 43 | $cleanUrl = $this->cleanUrl($url, $segment); 44 | $parsedUrl = $this->parseUrl($cleanUrl, $segment); 45 | 46 | // Check if there are enough segments, if not return url unchanged: 47 | if (count($parsedUrl['segments']) >= $segment) { 48 | array_splice($parsedUrl['segments'], $segment, 0, $locale); 49 | } 50 | return $this->pathFromParsedUrl($parsedUrl); 51 | } 52 | 53 | /** 54 | * Extract the first valid locale from a url 55 | * 56 | * @param string $url 57 | * @param integer $segment Index of the segment containing locale info 58 | * @return string|null $locale 59 | */ 60 | public function getLocaleFromUrl($url, $segment = 0) 61 | { 62 | return $this->parseUrl($url, $segment)['locale']; 63 | } 64 | 65 | /** 66 | * Removes the domain and locale (if present) of a given url. 67 | * Ex: http://www.domain.com/locale/random => /random, https://www.domain.com/random => /random, http://domain.com/random?param=value => /random?param=value 68 | * 69 | * @param string $url 70 | * @param integer $segment Index of the segment containing locale info 71 | * @return string 72 | */ 73 | public function cleanUrl($url, $segment = 0) 74 | { 75 | $parsedUrl = $this->parseUrl($url, $segment); 76 | // Remove locale from segments: 77 | if ($parsedUrl['locale']) { 78 | unset($parsedUrl['segments'][$segment]); 79 | $parsedUrl['locale'] = false; 80 | } 81 | return $this->pathFromParsedUrl($parsedUrl); 82 | } 83 | 84 | /** 85 | * Parses the given url in a similar way to PHP's parse_url, with the following differences: 86 | * Forward and trailling slashed are removed from the path value. 87 | * A new "segments" key replaces 'path', with the uri segments in array form ('/es/random/thing' => ['es', 'random', 'thing']) 88 | * A 'locale' key is added, with the value of the locale found in the current url 89 | * 90 | * @param string $url 91 | * @param integer $segment Index of the segment containing locale info 92 | * @return mixed 93 | */ 94 | protected function parseUrl($url, $segment = 0) 95 | { 96 | $parsedUrl = parse_url($url); 97 | $parsedUrl['segments'] = array_values(array_filter(explode('/', $parsedUrl['path']), 'strlen')); 98 | $localeCandidate = Arr::get($parsedUrl['segments'], $segment, false); 99 | $parsedUrl['locale'] = in_array($localeCandidate, $this->availableLocales) ? $localeCandidate : null; 100 | $parsedUrl['query'] = Arr::get($parsedUrl, 'query', false); 101 | $parsedUrl['fragment'] = Arr::get($parsedUrl, 'fragment', false); 102 | unset($parsedUrl['path']); 103 | return $parsedUrl; 104 | } 105 | 106 | /** 107 | * Returns the uri for the given parsed url based on its segments, query and fragment 108 | * 109 | * @return string 110 | */ 111 | protected function pathFromParsedUrl($parsedUrl) 112 | { 113 | $path = '/' . implode('/', $parsedUrl['segments']); 114 | if ($parsedUrl['query']) { 115 | $path .= "?{$parsedUrl['query']}"; 116 | } 117 | if ($parsedUrl['fragment']) { 118 | $path .= "#{$parsedUrl['fragment']}"; 119 | } 120 | return $path; 121 | } 122 | 123 | /** 124 | * Remove the front slash from a string 125 | * 126 | * @param string $path 127 | * @return string 128 | */ 129 | protected function removeFrontSlash($path) 130 | { 131 | return strlen($path) > 0 && substr($path, 0, 1) === '/' ? substr($path, 1) : $path; 132 | } 133 | 134 | /** 135 | * Remove the trailing slash from a string 136 | * 137 | * @param string $path 138 | * @return string 139 | */ 140 | protected function removeTrailingSlash($path) 141 | { 142 | return strlen($path) > 0 && substr($path, -1) === '/' ? substr($path, 0, -1) : $path; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Waavi/translation/30b095364ea5d6701377d53f5eadec0e6d0b517d/tests/.gitkeep -------------------------------------------------------------------------------- /tests/Cache/RepositoryFactoryTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(SimpleRepository::class, get_class($repo)); 26 | } 27 | 28 | /** 29 | * @test 30 | */ 31 | public function test_returns_simple_cache_if_taggable_store() 32 | { 33 | $store = new ArrayStore; 34 | $repo = RepositoryFactory::make($store, 'translation'); 35 | $this->assertEquals(TaggedRepository::class, get_class($repo)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Cache/SimpleRepositoryTest.php: -------------------------------------------------------------------------------- 1 | repo = new SimpleRepository(new ArrayStore, 'translation'); 14 | } 15 | 16 | /** 17 | * @test 18 | */ 19 | public function test_has_with_no_entry() 20 | { 21 | $this->assertFalse($this->repo->has('en', 'namespace', 'group')); 22 | } 23 | 24 | /** 25 | * @test 26 | */ 27 | public function test_has_returns_true_if_entry() 28 | { 29 | $this->repo->put('en', 'namespace', 'group', 'key', 'value'); 30 | $this->assertTrue($this->repo->has('en', 'namespace', 'group')); 31 | } 32 | 33 | /** 34 | * @test 35 | */ 36 | public function test_get_returns_null_if_empty() 37 | { 38 | $this->assertNull($this->repo->get('en', 'namespace', 'group')); 39 | } 40 | 41 | /** 42 | * @test 43 | */ 44 | public function test_get_return_content_if_hit() 45 | { 46 | $this->repo->put('en', 'namespace', 'group', 'value', 60); 47 | $this->assertEquals('value', $this->repo->get('en', 'namespace', 'group')); 48 | } 49 | 50 | /** 51 | * @test 52 | */ 53 | public function test_flush_removes_all() 54 | { 55 | $this->repo->put('en', 'namespace', 'group', 'value', 60); 56 | $this->repo->put('es', 'namespace', 'group', 'valor', 60); 57 | $this->repo->flush('en', 'namespace', 'group'); 58 | $this->assertNull($this->repo->get('en', 'namespace', 'group')); 59 | $this->assertNull($this->repo->get('es', 'namespace', 'group')); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Cache/TaggedRepositoryTest.php: -------------------------------------------------------------------------------- 1 | repo = new TaggedRepository(new ArrayStore, 'translation'); 14 | } 15 | 16 | /** 17 | * @test 18 | */ 19 | public function has_returns_false_when_no_entry_present() 20 | { 21 | $this->assertFalse($this->repo->has('en', 'namespace', 'group')); 22 | } 23 | 24 | /** 25 | * @test 26 | */ 27 | public function has_returns_true_if_entry_present() 28 | { 29 | $this->repo->put('en', 'namespace', 'group', 'value', 60); 30 | $this->assertTrue($this->repo->has('en', 'namespace', 'group')); 31 | } 32 | 33 | /** 34 | * @test 35 | */ 36 | public function get_returns_null_if_empty() 37 | { 38 | $this->assertNull($this->repo->get('en', 'namespace', 'group')); 39 | } 40 | 41 | /** 42 | * @test 43 | */ 44 | public function get_return_content_if_hit() 45 | { 46 | $this->repo->put('en', 'namespace', 'group', 'value', 60); 47 | $this->assertEquals('value', $this->repo->get('en', 'namespace', 'group')); 48 | } 49 | 50 | /** 51 | * @test 52 | */ 53 | public function test_flush_removes_just_the_group() 54 | { 55 | $this->repo->put('en', 'namespace', 'group', 'value', 60); 56 | $this->repo->put('es', 'namespace', 'group', 'valor', 60); 57 | $this->repo->flush('en', 'namespace', 'group'); 58 | $this->assertNull($this->repo->get('en', 'namespace', 'group')); 59 | $this->assertEquals('valor', $this->repo->get('es', 'namespace', 'group')); 60 | } 61 | 62 | /** 63 | * @test 64 | */ 65 | public function test_flush_all_removes_all() 66 | { 67 | $this->repo->put('en', 'namespace', 'group', 'value', 60); 68 | $this->repo->put('es', 'namespace', 'group', 'value', 60); 69 | $this->repo->flushAll(); 70 | $this->assertNull($this->repo->get('en', 'namespace', 'group')); 71 | $this->assertNull($this->repo->get('es', 'namespace', 'group')); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Cache/TranslationCacheTest.php: -------------------------------------------------------------------------------- 1 | assertFalse(\TranslationCache::has('en', 'namespace', 'group')); 19 | } 20 | 21 | /** 22 | * @test 23 | */ 24 | public function test_has_returns_true_if_entry() 25 | { 26 | \TranslationCache::put('en', 'namespace', 'group', 'value', 60); 27 | $this->assertTrue(\TranslationCache::has('en', 'namespace', 'group')); 28 | } 29 | 30 | /** 31 | * @test 32 | */ 33 | public function test_get_returns_null_if_empty() 34 | { 35 | $this->assertNull(\TranslationCache::get('en', 'namespace', 'group')); 36 | } 37 | 38 | /** 39 | * @test 40 | */ 41 | public function test_get_return_content_if_hit() 42 | { 43 | \TranslationCache::put('en', 'namespace', 'group', 'value', 60); 44 | $this->assertEquals('value', \TranslationCache::get('en', 'namespace', 'group')); 45 | } 46 | 47 | /** 48 | * @test 49 | */ 50 | public function test_flush_removes_just_the_group() 51 | { 52 | \TranslationCache::put('en', 'namespace', 'group', 'value', 60); 53 | \TranslationCache::put('es', 'namespace', 'group', 'valor', 60); 54 | \TranslationCache::flush('en', 'namespace', 'group'); 55 | $this->assertNull(\TranslationCache::get('en', 'namespace', 'group')); 56 | $this->assertEquals('valor', \TranslationCache::get('es', 'namespace', 'group')); 57 | } 58 | 59 | /** 60 | * @test 61 | */ 62 | public function test_flush_all_removes_all() 63 | { 64 | \TranslationCache::put('en', 'namespace', 'group', 'value', 60); 65 | \TranslationCache::put('es', 'namespace', 'group', 'value', 60); 66 | \TranslationCache::flushAll(); 67 | $this->assertNull(\TranslationCache::get('en', 'namespace', 'group')); 68 | $this->assertNull(\TranslationCache::get('es', 'namespace', 'group')); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Commands/FlushTest.php: -------------------------------------------------------------------------------- 1 | cacheRepository = \App::make('translation.cache.repository'); 12 | } 13 | 14 | public function tearDown(): void 15 | { 16 | parent::tearDown(); 17 | Mockery::close(); 18 | } 19 | 20 | /** 21 | * @test 22 | */ 23 | public function it_does_nothing_if_cache_disabled() 24 | { 25 | $this->cacheRepository->put('en', 'group', 'namespace', 'value', 60); 26 | $this->assertTrue($this->cacheRepository->has('en', 'group', 'namespace')); 27 | $command = Mockery::mock('Waavi\Translation\Commands\CacheFlushCommand[info]', [$this->cacheRepository, false]); 28 | $command->shouldReceive('info')->with('The translation cache is disabled.')->once(); 29 | $command->handle(); 30 | $this->assertTrue($this->cacheRepository->has('en', 'group', 'namespace')); 31 | } 32 | 33 | /** 34 | * @test 35 | */ 36 | public function it_flushes_the_cache() 37 | { 38 | $this->cacheRepository->put('en', 'group', 'namespace', 'value', 60); 39 | $this->assertTrue($this->cacheRepository->has('en', 'group', 'namespace')); 40 | $command = Mockery::mock('Waavi\Translation\Commands\CacheFlushCommand[info]', [$this->cacheRepository, true]); 41 | $command->shouldReceive('info')->with('Translation cache cleared.')->once(); 42 | $command->handle(); 43 | $this->assertFalse($this->cacheRepository->has('en', 'group', 'namespace')); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Commands/LoadTest.php: -------------------------------------------------------------------------------- 1 | languageRepository = \App::make(LanguageRepository::class); 14 | $this->translationRepository = \App::make(TranslationRepository::class); 15 | $translationsPath = realpath(__DIR__ . '/../lang'); 16 | $this->command = new FileLoaderCommand($this->languageRepository, $this->translationRepository, \App::make('files'), $translationsPath, 'en'); 17 | } 18 | 19 | /** 20 | * @test 21 | */ 22 | public function it_loads_files_into_database() 23 | { 24 | $file = realpath(__DIR__ . '/../lang/en/auth.php'); 25 | $this->command->loadFile($file, 'en'); 26 | $translations = $this->translationRepository->all(); 27 | 28 | $this->assertEquals(3, $translations->count()); 29 | 30 | $this->assertEquals('en', $translations[0]->locale); 31 | $this->assertEquals('*', $translations[0]->namespace); 32 | $this->assertEquals('auth', $translations[0]->group); 33 | $this->assertEquals('login.label', $translations[0]->item); 34 | $this->assertEquals('Enter your credentials', $translations[0]->text); 35 | 36 | $this->assertEquals('en', $translations[1]->locale); 37 | $this->assertEquals('*', $translations[1]->namespace); 38 | $this->assertEquals('auth', $translations[1]->group); 39 | $this->assertEquals('login.action', $translations[1]->item); 40 | $this->assertEquals('Login', $translations[1]->text); 41 | 42 | $this->assertEquals('en', $translations[2]->locale); 43 | $this->assertEquals('*', $translations[2]->namespace); 44 | $this->assertEquals('auth', $translations[2]->group); 45 | $this->assertEquals('simple', $translations[2]->item); 46 | $this->assertEquals('Simple', $translations[2]->text); 47 | } 48 | 49 | /** 50 | * @test 51 | */ 52 | public function it_loads_files_in_subdirectories_into_database() 53 | { 54 | $directory = realpath(__DIR__ . '/../lang/es'); 55 | $this->command->loadDirectory($directory, 'es'); 56 | $translations = $this->translationRepository->all()->sortBy('id'); 57 | 58 | $this->assertEquals(2, $translations->count()); 59 | 60 | $this->assertEquals('es', $translations[0]->locale); 61 | $this->assertEquals('*', $translations[0]->namespace); 62 | $this->assertEquals('welcome/page', $translations[0]->group); 63 | $this->assertEquals('title', $translations[0]->item); 64 | $this->assertEquals('Bienvenido', $translations[0]->text); 65 | 66 | $this->assertEquals('es', $translations[1]->locale); 67 | $this->assertEquals('*', $translations[1]->namespace); 68 | $this->assertEquals('auth', $translations[1]->group); 69 | $this->assertEquals('login.action', $translations[1]->item); 70 | $this->assertEquals('Identifícate', $translations[1]->text); 71 | } 72 | 73 | /** 74 | * @test 75 | */ 76 | public function it_doesnt_load_undefined_locales() 77 | { 78 | $this->command->handle(); 79 | $locales = $this->translationRepository->all()->pluck('locale')->toArray(); 80 | $this->assertTrue(in_array('en', $locales)); 81 | $this->assertTrue(in_array('es', $locales)); 82 | $this->assertFalse(in_array('ca', $locales)); 83 | } 84 | 85 | /** 86 | * @test 87 | */ 88 | public function it_loads_overwritten_vendor_files_correctly() 89 | { 90 | $this->command->handle(); 91 | 92 | $translations = $this->translationRepository->all(); 93 | 94 | $this->assertEquals(9, $translations->count()); 95 | 96 | $this->assertEquals('Texto proveedor', $translations->where('locale', 'es')->where('namespace', 'package')->where('group', 'example')->where('item', 'entry')->first()->text); 97 | $this->assertEquals('Vendor text', $translations->where('locale', 'en')->where('namespace', 'package')->where('group', 'example')->where('item', 'entry')->first()->text); 98 | } 99 | 100 | /** 101 | * @test 102 | */ 103 | public function it_doesnt_overwrite_locked_translations() 104 | { 105 | $trans = $this->translationRepository->create([ 106 | 'locale' => 'en', 107 | 'namespace' => '*', 108 | 'group' => 'auth', 109 | 'item' => 'login.label', 110 | 'text' => 'No override', 111 | ]); 112 | $trans->locked = true; 113 | $trans->save(); 114 | 115 | $file = realpath(__DIR__ . '/../lang/en/auth.php'); 116 | $this->command->loadFile($file, 'en'); 117 | $translations = $this->translationRepository->all(); 118 | 119 | $this->assertEquals(3, $translations->count()); 120 | 121 | $this->assertEquals('en', $translations[0]->locale); 122 | $this->assertEquals('*', $translations[0]->namespace); 123 | $this->assertEquals('auth', $translations[0]->group); 124 | $this->assertEquals('login.label', $translations[0]->item); 125 | $this->assertEquals('No override', $translations[0]->text); 126 | 127 | $this->assertEquals('en', $translations[1]->locale); 128 | $this->assertEquals('*', $translations[1]->namespace); 129 | $this->assertEquals('auth', $translations[1]->group); 130 | $this->assertEquals('login.action', $translations[1]->item); 131 | $this->assertEquals('Login', $translations[1]->text); 132 | } 133 | 134 | /** 135 | * @test 136 | */ 137 | public function it_doesnt_load_empty_arrays() 138 | { 139 | $file = realpath(__DIR__ . '/../lang/en/empty.php'); 140 | $this->command->loadFile($file, 'en'); 141 | $translations = $this->translationRepository->all(); 142 | 143 | $this->assertEquals(1, $translations->count()); 144 | 145 | $this->assertEquals('en', $translations[0]->locale); 146 | $this->assertEquals('*', $translations[0]->namespace); 147 | $this->assertEquals('empty', $translations[0]->group); 148 | $this->assertEquals('emptyString', $translations[0]->item); 149 | $this->assertEquals('', $translations[0]->text); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /tests/Loaders/CacheLoaderTest.php: -------------------------------------------------------------------------------- 1 | cache = Mockery::mock(Cache::class); 15 | $this->fallback = Mockery::mock(Loader::class); 16 | $this->cacheLoader = new CacheLoader('en', $this->cache, $this->fallback, 60, 'translation'); 17 | } 18 | 19 | public function tearDown(): void 20 | { 21 | Mockery::close(); 22 | parent::tearDown(); 23 | } 24 | 25 | /** 26 | * @test 27 | */ 28 | public function it_returns_from_cache_if_hit() 29 | { 30 | $this->cache->shouldReceive('has')->with('en', 'group', 'name')->once()->andReturn(true); 31 | $this->cache->shouldReceive('get')->with('en', 'group', 'name')->once()->andReturn('cache hit'); 32 | $this->assertEquals('cache hit', $this->cacheLoader->loadSource('en', 'group', 'name')); 33 | } 34 | 35 | /** 36 | * @test 37 | */ 38 | public function it_returns_from_fallback_and_stores_in_cache_if_miss() 39 | { 40 | $this->cache->shouldReceive('has')->with('en', 'group', 'name')->once()->andReturn(false); 41 | $this->fallback->shouldReceive('load')->with('en', 'group', 'name')->once()->andReturn('cache miss'); 42 | $this->cache->shouldReceive('put')->with('en', 'group', 'name', 'cache miss', 60)->once()->andReturn(true); 43 | $this->assertEquals('cache miss', $this->cacheLoader->loadSource('en', 'group', 'name')); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Loaders/DatabaseLoaderTest.php: -------------------------------------------------------------------------------- 1 | translationRepository = \App::make(TranslationRepository::class); 14 | $this->loader = new DatabaseLoader('es', $this->translationRepository); 15 | } 16 | 17 | public function tearDown():void 18 | { 19 | Mockery::close(); 20 | parent::tearDown(); 21 | } 22 | 23 | /** 24 | * @test 25 | */ 26 | public function it_returns_from_database() 27 | { 28 | $expected = [ 29 | 'simple' => 'text', 30 | 'array' => [ 31 | 'item' => 'item', 32 | 'nested' => [ 33 | 'item' => 'nested', 34 | ], 35 | ], 36 | ]; 37 | $translation = $this->translationRepository->create([ 38 | 'locale' => 'es', 39 | 'namespace' => '*', 40 | 'group' => 'group', 41 | 'item' => 'simple', 42 | 'text' => 'text', 43 | ]); 44 | $translation = $this->translationRepository->create([ 45 | 'locale' => 'es', 46 | 'namespace' => '*', 47 | 'group' => 'group', 48 | 'item' => 'array.item', 49 | 'text' => 'item', 50 | ]); 51 | $translation = $this->translationRepository->create([ 52 | 'locale' => 'es', 53 | 'namespace' => '*', 54 | 'group' => 'group', 55 | 'item' => 'array.nested.item', 56 | 'text' => 'nested', 57 | ]); 58 | $translations = $this->loader->loadSource('es', 'group'); 59 | $this->assertEquals($expected, $translations); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Loaders/FileLoaderTest.php: -------------------------------------------------------------------------------- 1 | laravelLoader = Mockery::mock(LaravelFileLoader::class); 14 | $this->fileLoader = new FileLoader('en', $this->laravelLoader); 15 | } 16 | 17 | public function tearDown(): void 18 | { 19 | Mockery::close(); 20 | parent::tearDown(); 21 | } 22 | 23 | /** 24 | * @test 25 | */ 26 | public function it_returns_from_file() 27 | { 28 | $data = [ 29 | 'simple' => 'Simple', 30 | 'nested' => [ 31 | 'one' => 'First', 32 | 'two' => 'Second', 33 | ], 34 | ]; 35 | $this->laravelLoader->shouldReceive('load')->with('en', 'group', 'name')->andReturn($data); 36 | $this->assertEquals($data, $this->fileLoader->loadSource('en', 'group', 'name')); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Loaders/LoadTest.php: -------------------------------------------------------------------------------- 1 | laravelLoader = Mockery::mock(LaravelFileLoader::class); 14 | // We will use the file loader: 15 | $this->fileLoader = new FileLoader('en', $this->laravelLoader); 16 | } 17 | 18 | public function tearDown(): void 19 | { 20 | Mockery::close(); 21 | parent::tearDown(); 22 | } 23 | 24 | /** 25 | * @test 26 | */ 27 | public function it_merges_default_and_target_locales() 28 | { 29 | $en = [ 30 | 'simple' => 'Simple', 31 | 'nested' => [ 32 | 'one' => 'First', 33 | 'two' => 'Second', 34 | ], 35 | ]; 36 | $es = [ 37 | 'simple' => 'OverSimple', 38 | 'nested' => [ 39 | 'one' => 'OverFirst', 40 | ], 41 | ]; 42 | $expected = [ 43 | 'simple' => 'OverSimple', 44 | 'nested' => [ 45 | 'one' => 'OverFirst', 46 | 'two' => 'Second', 47 | ], 48 | ]; 49 | $this->laravelLoader->shouldReceive('load')->with('en', 'group', 'name')->andReturn($en); 50 | $this->laravelLoader->shouldReceive('load')->with('es', 'group', 'name')->andReturn($es); 51 | $this->assertEquals($expected, $this->fileLoader->load('es', 'group', 'name')); 52 | } 53 | 54 | /** 55 | * @testLoadTest 56 | */ 57 | public function it_returns_translation_code_if_text_not_found() 58 | { 59 | $this->assertEquals('auth.code', trans('auth.code')); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/Loaders/MixedLoaderTest.php: -------------------------------------------------------------------------------- 1 | fileLoader = Mockery::mock(FileLoader::class); 15 | $this->dbLoader = Mockery::mock(DatabaseLoader::class); 16 | $this->mixedLoader = new MixedLoader('en', $this->fileLoader, $this->dbLoader); 17 | } 18 | 19 | public function tearDown(): void 20 | { 21 | Mockery::close(); 22 | parent::tearDown(); 23 | } 24 | 25 | /** 26 | * @test 27 | */ 28 | public function it_merges_file_and_db() 29 | { 30 | $file = [ 31 | 'in.file' => 'File', 32 | 'no.db' => 'No database', 33 | ]; 34 | $db = [ 35 | 'in.file' => 'Database', 36 | 'no.file' => 'No file', 37 | ]; 38 | $expected = [ 39 | 'in.file' => 'File', 40 | 'no.db' => 'No database', 41 | 'no.file' => 'No file', 42 | ]; 43 | $this->fileLoader->shouldReceive('loadSource')->with('en', 'group', 'name')->andReturn($file); 44 | $this->dbLoader->shouldReceive('loadSource')->with('en', 'group', 'name')->andReturn($db); 45 | $this->assertEquals($expected, $this->mixedLoader->load('en', 'group', 'name')); 46 | } 47 | 48 | /** 49 | * @test 50 | */ 51 | public function it_cascades_namespaces() 52 | { 53 | $this->fileLoader->shouldReceive('addNamespace')->with('package', '/some/path/to/package')->andReturnNull(); 54 | $this->dbLoader->shouldReceive('addNamespace')->with('package', '/some/path/to/package')->andReturnNull(); 55 | $this->assertNull($this->mixedLoader->addNamespace('package', '/some/path/to/package')); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Localizer/CleanUrlTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('/', UriLocalizer::cleanUrl('')); 14 | $this->assertEquals('/', UriLocalizer::cleanUrl('/')); 15 | } 16 | 17 | /** 18 | * @test 19 | */ 20 | public function it_cleans_uri() 21 | { 22 | $this->assertEquals('/random', UriLocalizer::cleanUrl('random/')); 23 | } 24 | 25 | /** 26 | * @test 27 | */ 28 | public function it_cleans_http_url() 29 | { 30 | $this->assertEquals('/random', UriLocalizer::cleanUrl('http://domain.com/random/')); 31 | } 32 | 33 | /** 34 | * @test 35 | */ 36 | public function it_cleans_https_url() 37 | { 38 | $this->assertEquals('/random', UriLocalizer::cleanUrl('https://domain.com/random/')); 39 | } 40 | 41 | /** 42 | * @test 43 | */ 44 | public function it_keeps_query_string() 45 | { 46 | $this->assertEquals('/random?param=value¶m=', UriLocalizer::cleanUrl('https://domain.com/random/?param=value¶m=')); 47 | } 48 | 49 | /** 50 | * @test 51 | */ 52 | public function it_removes_locale_string() 53 | { 54 | $this->assertEquals('/random?param=value¶m=', UriLocalizer::cleanUrl('https://domain.com/es/random/?param=value¶m=')); 55 | } 56 | 57 | /** 58 | * @test 59 | */ 60 | public function it_removes_locale_string_in_custom_position() 61 | { 62 | $this->assertEquals('/api/random?param=value¶m=', UriLocalizer::cleanUrl('https://domain.com/api/es/random/?param=value¶m=', 1)); 63 | } 64 | 65 | /** 66 | * @test 67 | */ 68 | public function it_keeps_invalid_locale_string() 69 | { 70 | $this->assertEquals('/ca/random?param=value¶m=', UriLocalizer::cleanUrl('https://domain.com/ca/random/?param=value¶m=')); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/Localizer/GetLocaleFromUrlTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('es', UriLocalizer::getLocaleFromUrl('http://domain.com/es/random/')); 14 | } 15 | 16 | /** 17 | * @test 18 | */ 19 | public function it_returns_locale_from_uri() 20 | { 21 | $this->assertEquals('es', UriLocalizer::getLocaleFromUrl('/es/random/')); 22 | $this->assertEquals('es', UriLocalizer::getLocaleFromUrl('es/random/')); 23 | } 24 | 25 | /** 26 | * @test 27 | */ 28 | public function it_return_null_if_no_locale_found() 29 | { 30 | $this->assertNull(UriLocalizer::getLocaleFromUrl('/random/')); 31 | $this->assertNull(UriLocalizer::getLocaleFromUrl('ca/random/')); 32 | } 33 | 34 | /** 35 | * @test 36 | */ 37 | public function it_returns_locale_from_url_in_custom_position() 38 | { 39 | $this->assertEquals('es', UriLocalizer::getLocaleFromUrl('http://domain.com/api/es/random/', 1)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Localizer/LocalizeUriTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('/es', UriLocalizer::localize('/', 'es')); 15 | $this->assertEquals('/es', UriLocalizer::localize('', 'es')); 16 | } 17 | 18 | /** 19 | * @test 20 | */ 21 | public function test_home_with_locale() 22 | { 23 | $this->assertEquals('/es', UriLocalizer::localize('/en', 'es')); 24 | $this->assertEquals('/es', UriLocalizer::localize('en', 'es')); 25 | } 26 | 27 | /** 28 | * @test 29 | */ 30 | public function test_random_page_no_locale() 31 | { 32 | $this->assertEquals('/es/random', UriLocalizer::localize('/random', 'es')); 33 | $this->assertEquals('/es/random', UriLocalizer::localize('random', 'es')); 34 | $this->assertEquals('/es/random', UriLocalizer::localize('/random/', 'es')); 35 | $this->assertEquals('/es/random', UriLocalizer::localize('random/', 'es')); 36 | } 37 | 38 | /** 39 | * @test 40 | */ 41 | public function test_random_page_with_locale() 42 | { 43 | $this->assertEquals('/es/random', UriLocalizer::localize('/en/random', 'es')); 44 | $this->assertEquals('/es/random', UriLocalizer::localize('en/random', 'es')); 45 | $this->assertEquals('/es/random', UriLocalizer::localize('/en/random/', 'es')); 46 | $this->assertEquals('/es/random', UriLocalizer::localize('en/random/', 'es')); 47 | } 48 | 49 | /** 50 | * @test 51 | */ 52 | public function it_ignores_unexesting_locales() 53 | { 54 | $this->assertEquals('/es/ca/random', UriLocalizer::localize('/ca/random', 'es')); 55 | } 56 | 57 | /** 58 | * @test 59 | */ 60 | public function it_maintains_get_parameters() 61 | { 62 | $this->assertEquals('/es/random?param1=value1¶m2=', UriLocalizer::localize('random?param1=value1¶m2=', 'es')); 63 | } 64 | 65 | /** 66 | * @test 67 | */ 68 | public function it_localizes_when_locale_is_not_first() 69 | { 70 | $this->assertEquals('/api/es/random', UriLocalizer::localize('api/random', 'es', 1)); 71 | $this->assertEquals('/api/es/random', UriLocalizer::localize('api/en/random', 'es', 1)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Middleware/TranslationMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | call('GET', '/'); 15 | $statusCode = $response->getStatusCode(); 16 | 17 | $this->assertEquals(302, $response->getStatusCode()); 18 | $this->assertTrue($response->headers->has('location')); 19 | $this->assertEquals('http://localhost/en', $response->headers->get('location')); 20 | } 21 | 22 | /** 23 | * @test 24 | */ 25 | public function it_will_redirect_to_browser_locale_before_default() 26 | { 27 | $response = $this->call('GET', '/', [], [], [], ['HTTP_ACCEPT_LANGUAGE' => 'es']); 28 | $statusCode = $response->getStatusCode(); 29 | 30 | $this->assertEquals(302, $response->getStatusCode()); 31 | $this->assertTrue($response->headers->has('location')); 32 | $this->assertEquals('http://localhost/es', $response->headers->get('location')); 33 | } 34 | 35 | /** 36 | * @test 37 | */ 38 | public function it_will_redirect_if_invalid_locale() 39 | { 40 | $response = $this->call('GET', '/ca'); 41 | $statusCode = $response->getStatusCode(); 42 | 43 | $this->assertEquals(302, $response->getStatusCode()); 44 | $this->assertTrue($response->headers->has('location')); 45 | $this->assertEquals('http://localhost/en/ca', $response->headers->get('location')); 46 | } 47 | 48 | /** 49 | * @test 50 | */ 51 | public function it_will_not_redirect_if_valid_locale() 52 | { 53 | $response = $this->call('GET', '/es'); 54 | $statusCode = $response->getStatusCode(); 55 | 56 | $this->assertEquals(200, $response->getStatusCode()); 57 | $this->assertEquals('Hola mundo', $response->getContent()); 58 | } 59 | 60 | /** 61 | * @test 62 | */ 63 | public function it_will_ignore_post_requests() 64 | { 65 | $response = $this->call('POST', '/'); 66 | $statusCode = $response->getStatusCode(); 67 | 68 | $this->assertEquals(200, $response->getStatusCode()); 69 | $this->assertEquals('POST answer', $response->getContent()); 70 | } 71 | 72 | /** 73 | * @test 74 | */ 75 | public function it_sets_the_app_locale() 76 | { 77 | $response = $this->call('GET', '/en/locale'); 78 | $this->assertEquals('en', $response->getContent()); 79 | $response = $this->call('GET', '/es/locale'); 80 | $this->assertEquals('es', $response->getContent()); 81 | } 82 | 83 | /** 84 | * @test 85 | */ 86 | public function it_detects_the_app_locale_in_custom_segment() 87 | { 88 | $response = $this->call('GET', '/api/v1/en/locale'); 89 | $this->assertEquals('en', $response->getContent()); 90 | $response = $this->call('GET', '/api/v1/es/locale'); 91 | $this->assertEquals('es', $response->getContent()); 92 | } 93 | 94 | /** 95 | * @test 96 | */ 97 | public function it_redirects_invalid_locale_in_custom_segment() 98 | { 99 | $response = $this->call('GET', '/api/v1/ca/locale'); 100 | $statusCode = $response->getStatusCode(); 101 | 102 | $this->assertEquals(302, $response->getStatusCode()); 103 | $this->assertTrue($response->headers->has('location')); 104 | $this->assertEquals('http://localhost/api/v1/en/ca/locale', $response->headers->get('location')); 105 | } 106 | 107 | /** 108 | * @test 109 | */ 110 | public function it_keeps_locale_in_post_requests_with_no_locale_set() 111 | { 112 | $translationRepository = \App::make(TranslationRepository::class); 113 | $trans = $translationRepository->create([ 114 | 'locale' => 'en', 115 | 'namespace' => '*', 116 | 'group' => 'welcome', 117 | 'item' => 'title', 118 | 'text' => 'Welcome', 119 | ]); 120 | 121 | $trans = $translationRepository->create([ 122 | 'locale' => 'es', 123 | 'namespace' => '*', 124 | 'group' => 'welcome', 125 | 'item' => 'title', 126 | 'text' => 'Bienvenido', 127 | ]); 128 | 129 | $this->call('GET', '/es'); 130 | $response = $this->call('POST', '/welcome'); 131 | $statusCode = $response->getStatusCode(); 132 | $this->assertEquals(200, $response->getStatusCode()); 133 | $this->assertEquals('Bienvenido', $response->getContent()); 134 | 135 | $this->call('GET', '/en'); 136 | $response = $this->call('POST', '/welcome'); 137 | $statusCode = $response->getStatusCode(); 138 | $this->assertEquals(200, $response->getStatusCode()); 139 | $this->assertEquals('Welcome', $response->getContent()); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /tests/Repositories/LanguageRepositoryTest.php: -------------------------------------------------------------------------------- 1 | languageRepository = \App::make(LanguageRepository::class); 14 | $this->translationRepository = \App::make(TranslationRepository::class); 15 | } 16 | 17 | /** 18 | * @test 19 | */ 20 | public function test_can_create() 21 | { 22 | $this->assertNotNull($this->languageRepository->create(['locale' => 'ca', 'name' => 'Catalan'])); 23 | } 24 | 25 | /** 26 | * @test 27 | */ 28 | public function test_has_table() 29 | { 30 | $this->assertTrue($this->languageRepository->tableExists()); 31 | } 32 | 33 | /** 34 | * @test 35 | */ 36 | public function test_create_disallows_duplicate_locale() 37 | { 38 | $this->assertNull($this->languageRepository->create(['locale' => 'en', 'name' => 'Catalan'])); 39 | } 40 | 41 | /** 42 | * @test 43 | */ 44 | public function test_create_disallows_duplicate_name() 45 | { 46 | $this->assertNull($this->languageRepository->create(['locale' => 'ca', 'name' => 'English'])); 47 | } 48 | 49 | /** 50 | * @test 51 | */ 52 | public function test_can_update() 53 | { 54 | $this->assertTrue($this->languageRepository->update(['id' => 1, 'locale' => 'ens', 'name' => 'Englishs'])); 55 | $lang = $this->languageRepository->find(1); 56 | $this->assertEquals('ens', $lang->locale); 57 | $this->assertEquals('Englishs', $lang->name); 58 | } 59 | 60 | /** 61 | * @test 62 | */ 63 | public function test_update_disallows_duplicate_locale() 64 | { 65 | $this->assertFalse($this->languageRepository->update(['id' => 1, 'locale' => 'es', 'name' => 'Englishs'])); 66 | } 67 | 68 | /** 69 | * @test 70 | */ 71 | public function test_update_disallows_duplicate_name() 72 | { 73 | $this->assertFalse($this->languageRepository->update(['id' => 1, 'locale' => 'ens', 'name' => 'Spanish'])); 74 | } 75 | 76 | /** 77 | * @test 78 | */ 79 | public function it_can_delete() 80 | { 81 | $this->languageRepository->delete(2); 82 | $this->assertEquals(1, $this->languageRepository->all()->count()); 83 | } 84 | 85 | /** 86 | * @test 87 | */ 88 | public function it_can_restore() 89 | { 90 | $this->languageRepository->delete(2); 91 | $this->assertEquals(1, $this->languageRepository->all()->count()); 92 | $this->languageRepository->restore(2); 93 | $this->assertEquals(2, $this->languageRepository->all()->count()); 94 | } 95 | 96 | /** 97 | * @test 98 | */ 99 | public function it_can_find_by_locale() 100 | { 101 | $language = $this->languageRepository->findByLocale('es'); 102 | $this->assertNotNull($language); 103 | $this->assertEquals('es', $language->locale); 104 | $this->assertEquals('Spanish', $language->name); 105 | } 106 | 107 | /** 108 | * @test 109 | */ 110 | public function it_can_find_trashed_by_locale() 111 | { 112 | $this->languageRepository->delete(2); 113 | $language = $this->languageRepository->findTrashedByLocale('es'); 114 | $this->assertNotNull($language); 115 | $this->assertEquals('es', $language->locale); 116 | $this->assertEquals('Spanish', $language->name); 117 | } 118 | 119 | /** 120 | * @test 121 | */ 122 | public function it_can_find_all_except_one() 123 | { 124 | $this->languageRepository->create(['locale' => 'ca', 'name' => 'Catalan']); 125 | $languages = $this->languageRepository->allExcept('es'); 126 | $this->assertNotNull($languages); 127 | $this->assertEquals(2, $languages->count()); 128 | 129 | $this->assertEquals('en', $languages[0]->locale); 130 | $this->assertEquals('English', $languages[0]->name); 131 | $this->assertEquals('ca', $languages[1]->locale); 132 | $this->assertEquals('Catalan', $languages[1]->name); 133 | } 134 | 135 | /** 136 | * @test 137 | */ 138 | public function it_can_get_a_list_of_all_available_locales() 139 | { 140 | $this->assertEquals(['en', 'es'], $this->languageRepository->availableLocales()); 141 | } 142 | 143 | /** 144 | * @test 145 | */ 146 | public function it_can_check_a_locale_exists() 147 | { 148 | $this->assertTrue($this->languageRepository->isValidLocale('es')); 149 | $this->assertFalse($this->languageRepository->isValidLocale('ca')); 150 | } 151 | 152 | /** 153 | * @test 154 | */ 155 | public function it_can_calculate_the_percent_translated() 156 | { 157 | $this->assertEquals(0, $this->languageRepository->percentTranslated('es')); 158 | 159 | $this->translationRepository->create([ 160 | 'locale' => 'es', 161 | 'namespace' => '*', 162 | 'group' => 'group', 163 | 'item' => 'item', 164 | 'text' => 'text', 165 | ]); 166 | $this->translationRepository->create([ 167 | 'locale' => 'en', 168 | 'namespace' => '*', 169 | 'group' => 'group', 170 | 'item' => 'item', 171 | 'text' => 'text', 172 | ]); 173 | $this->translationRepository->create([ 174 | 'locale' => 'en', 175 | 'namespace' => '*', 176 | 'group' => 'group', 177 | 'item' => 'item2', 178 | 'text' => 'text', 179 | ]); 180 | 181 | $this->assertEquals(50, $this->languageRepository->percentTranslated('es')); 182 | $this->assertEquals(100, $this->languageRepository->percentTranslated('en')); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /tests/Repositories/TranslationRepositoryTest.php: -------------------------------------------------------------------------------- 1 | languageRepository = \App::make(LanguageRepository::class); 15 | $this->translationRepository = \App::make(TranslationRepository::class); 16 | } 17 | 18 | /** 19 | * @test 20 | */ 21 | public function test_can_create() 22 | { 23 | $translation = $this->translationRepository->create([ 24 | 'locale' => 'es', 25 | 'namespace' => '*', 26 | 'group' => 'group', 27 | 'item' => 'item', 28 | 'text' => 'text', 29 | ]); 30 | 31 | $this->assertTrue($translation->exists()); 32 | 33 | $this->assertEquals('es', $translation->locale); 34 | $this->assertEquals('*', $translation->namespace); 35 | $this->assertEquals('group', $translation->group); 36 | $this->assertEquals('item', $translation->item); 37 | $this->assertEquals('text', $translation->text); 38 | } 39 | 40 | /** 41 | * @test 42 | */ 43 | public function test_namespace_is_required() 44 | { 45 | $translation = $this->translationRepository->create([ 46 | 'locale' => 'es', 47 | 'namespace' => '', 48 | 'group' => 'group', 49 | 'item' => 'item', 50 | 'text' => 'text', 51 | ]); 52 | $this->assertNull($translation); 53 | } 54 | 55 | /** 56 | * @test 57 | */ 58 | public function test_locale_is_required() 59 | { 60 | $translation = $this->translationRepository->create([ 61 | 'locale' => '', 62 | 'namespace' => '*', 63 | 'group' => 'group', 64 | 'item' => 'item', 65 | 'text' => 'text', 66 | ]); 67 | $this->assertNull($translation); 68 | } 69 | 70 | /** 71 | * @test 72 | */ 73 | public function test_group_is_required() 74 | { 75 | $translation = $this->translationRepository->create([ 76 | 'locale' => 'es', 77 | 'namespace' => '*', 78 | 'group' => '', 79 | 'item' => 'item', 80 | 'text' => 'text', 81 | ]); 82 | $this->assertNull($translation); 83 | } 84 | 85 | /** 86 | * @test 87 | */ 88 | public function test_item_is_required() 89 | { 90 | $translation = $this->translationRepository->create([ 91 | 'locale' => 'es', 92 | 'namespace' => '*', 93 | 'group' => 'group', 94 | 'item' => '', 95 | 'text' => 'text', 96 | ]); 97 | $this->assertNull($translation); 98 | } 99 | 100 | /** 101 | * @test 102 | */ 103 | public function test_text_not_required() 104 | { 105 | $translation = $this->translationRepository->create([ 106 | 'locale' => 'es', 107 | 'namespace' => '*', 108 | 'group' => 'group', 109 | 'item' => 'item', 110 | 'text' => '', 111 | ]); 112 | $this->assertNotNull($translation); 113 | $this->assertTrue($translation->exists()); 114 | } 115 | 116 | /** 117 | * @test 118 | */ 119 | public function test_cannot_repeat_same_code_on_same_language() 120 | { 121 | $translation = $this->translationRepository->create([ 122 | 'locale' => 'es', 123 | 'namespace' => '*', 124 | 'group' => 'group', 125 | 'item' => 'item', 126 | 'text' => 'text', 127 | ]); 128 | $this->assertNotNull($translation); 129 | $this->assertTrue($translation->exists()); 130 | 131 | $translation = $this->translationRepository->create([ 132 | 'locale' => 'es', 133 | 'namespace' => '*', 134 | 'group' => 'group', 135 | 'item' => 'item', 136 | 'text' => 'text', 137 | ]); 138 | $this->assertNull($translation); 139 | } 140 | 141 | /** 142 | * @test 143 | */ 144 | public function test_update_works() 145 | { 146 | $translation = $this->translationRepository->create([ 147 | 'locale' => 'es', 148 | 'namespace' => '*', 149 | 'group' => 'group', 150 | 'item' => 'item', 151 | 'text' => 'text', 152 | ]); 153 | 154 | $this->assertTrue($this->translationRepository->update($translation->id, 'new text')); 155 | 156 | $translation = $this->translationRepository->find($translation->id); 157 | 158 | $this->assertNotNull($translation); 159 | $this->assertEquals('new text', $translation->text); 160 | $this->assertFalse($translation->isLocked()); 161 | } 162 | 163 | /** 164 | * @test 165 | */ 166 | public function test_update_and_lock() 167 | { 168 | $translation = $this->translationRepository->create([ 169 | 'locale' => 'es', 170 | 'namespace' => '*', 171 | 'group' => 'group', 172 | 'item' => 'item', 173 | 'text' => 'text', 174 | ]); 175 | 176 | $this->assertTrue($this->translationRepository->updateAndLock($translation->id, 'new text')); 177 | 178 | $translation = $this->translationRepository->find($translation->id); 179 | 180 | $this->assertNotNull($translation); 181 | $this->assertEquals('new text', $translation->text); 182 | $this->assertTrue($translation->isLocked()); 183 | } 184 | 185 | /** 186 | * @test 187 | */ 188 | public function test_update_fails_if_lock() 189 | { 190 | $translation = $this->translationRepository->create([ 191 | 'locale' => 'es', 192 | 'namespace' => '*', 193 | 'group' => 'group', 194 | 'item' => 'item', 195 | 'text' => 'text', 196 | ]); 197 | $translation->lock(); 198 | $translation->save(); 199 | 200 | $this->assertFalse($this->translationRepository->update($translation->id, 'new text')); 201 | } 202 | 203 | /** 204 | * @test 205 | */ 206 | public function test_force_update() 207 | { 208 | $translation = $this->translationRepository->create([ 209 | 'locale' => 'es', 210 | 'namespace' => '*', 211 | 'group' => 'group', 212 | 'item' => 'item', 213 | 'text' => 'text', 214 | ]); 215 | $translation->lock(); 216 | $translation->save(); 217 | 218 | $this->assertTrue($this->translationRepository->updateAndLock($translation->id, 'new text')); 219 | 220 | $translation = $this->translationRepository->find($translation->id); 221 | 222 | $this->assertNotNull($translation); 223 | $this->assertEquals('new text', $translation->text); 224 | $this->assertTrue($translation->isLocked()); 225 | } 226 | 227 | /** 228 | * @test 229 | */ 230 | public function test_delete() 231 | { 232 | $translation = $this->translationRepository->create([ 233 | 'locale' => 'es', 234 | 'namespace' => '*', 235 | 'group' => 'group', 236 | 'item' => 'item', 237 | 'text' => 'text', 238 | ]); 239 | $translation2 = $this->translationRepository->create([ 240 | 'locale' => 'es', 241 | 'namespace' => '*', 242 | 'group' => 'group', 243 | 'item' => 'item2', 244 | 'text' => 'text', 245 | ]); 246 | $this->assertEquals(2, $this->translationRepository->count()); 247 | $this->translationRepository->delete($translation->id); 248 | $this->assertEquals(1, $this->translationRepository->count()); 249 | } 250 | 251 | /** 252 | * @test 253 | */ 254 | public function it_deletes_other_locales_if_default() 255 | { 256 | $translation = $this->translationRepository->create([ 257 | 'locale' => 'en', 258 | 'namespace' => '*', 259 | 'group' => 'group', 260 | 'item' => 'item', 261 | 'text' => 'text', 262 | ]); 263 | $translation2 = $this->translationRepository->create([ 264 | 'locale' => 'es', 265 | 'namespace' => '*', 266 | 'group' => 'group', 267 | 'item' => 'item', 268 | 'text' => 'text', 269 | ]); 270 | $translation3 = $this->translationRepository->create([ 271 | 'locale' => 'es', 272 | 'namespace' => '*', 273 | 'group' => 'group', 274 | 'item' => 'item2', 275 | 'text' => 'text', 276 | ]); 277 | $this->assertEquals(3, $this->translationRepository->count()); 278 | $this->translationRepository->delete($translation->id); 279 | $this->assertEquals(1, $this->translationRepository->count()); 280 | } 281 | 282 | /** 283 | * @test 284 | */ 285 | public function it_loads_arrays() 286 | { 287 | $array = [ 288 | 'simple' => 'Simple', 289 | 'group' => [ 290 | 'item' => 'Item', 291 | 'meti' => 'metI', 292 | ], 293 | ]; 294 | $this->translationRepository->loadArray($array, 'en', 'file'); 295 | 296 | $translations = $this->translationRepository->all(); 297 | 298 | $this->assertEquals(3, $translations->count()); 299 | 300 | $this->assertEquals('en', $translations[0]->locale); 301 | $this->assertEquals('*', $translations[0]->namespace); 302 | $this->assertEquals('file', $translations[0]->group); 303 | $this->assertEquals('simple', $translations[0]->item); 304 | $this->assertEquals('Simple', $translations[0]->text); 305 | 306 | $this->assertEquals('en', $translations[1]->locale); 307 | $this->assertEquals('*', $translations[1]->namespace); 308 | $this->assertEquals('file', $translations[1]->group); 309 | $this->assertEquals('group.item', $translations[1]->item); 310 | $this->assertEquals('Item', $translations[1]->text); 311 | 312 | $this->assertEquals('en', $translations[2]->locale); 313 | $this->assertEquals('*', $translations[2]->namespace); 314 | $this->assertEquals('file', $translations[2]->group); 315 | $this->assertEquals('group.meti', $translations[2]->item); 316 | $this->assertEquals('metI', $translations[2]->text); 317 | } 318 | 319 | /** 320 | * @test 321 | */ 322 | public function load_arrays_does_not_overwrite_locked_translations() 323 | { 324 | $array = [ 325 | 'simple' => 'Simple', 326 | 'group' => [ 327 | 'item' => 'Item', 328 | 'meti' => 'metI', 329 | ], 330 | ]; 331 | $this->translationRepository->loadArray($array, 'en', 'file'); 332 | $this->translationRepository->updateAndLock(1, 'Complex'); 333 | $this->translationRepository->loadArray($array, 'en', 'file'); 334 | 335 | $translations = $this->translationRepository->all(); 336 | 337 | $this->assertEquals(3, $translations->count()); 338 | 339 | $this->assertEquals('en', $translations[0]->locale); 340 | $this->assertEquals('*', $translations[0]->namespace); 341 | $this->assertEquals('file', $translations[0]->group); 342 | $this->assertEquals('simple', $translations[0]->item); 343 | $this->assertEquals('Complex', $translations[0]->text); 344 | } 345 | 346 | /** 347 | * @test 348 | */ 349 | public function it_picks_a_random_untranslated_entry() 350 | { 351 | $array = ['simple' => 'Simple']; 352 | $this->translationRepository->loadArray($array, 'en', 'file'); 353 | 354 | $translation = $this->translationRepository->randomUntranslated('es'); 355 | $this->assertNotNull($translation); 356 | } 357 | 358 | /** 359 | * @test 360 | */ 361 | public function it_lists_all_untranslated_entries() 362 | { 363 | $array = ['simple' => 'Simple', 'complex' => 'Complex']; 364 | $this->translationRepository->loadArray($array, 'en', 'file'); 365 | $array = ['simple' => 'Simple']; 366 | $this->translationRepository->loadArray($array, 'es', 'file'); 367 | 368 | $translations = $this->translationRepository->untranslated('es'); 369 | $this->assertNotNull($translations); 370 | $this->assertEquals(1, $translations->count()); 371 | $this->assertEquals('Complex', $translations[0]->text); 372 | } 373 | 374 | /** 375 | * @test 376 | */ 377 | public function it_finds_by_code() 378 | { 379 | $array = ['simple' => 'Simple', 'complex' => 'Complex']; 380 | $this->translationRepository->loadArray($array, 'en', 'file'); 381 | $translation = $this->translationRepository->findByCode('en', '*', 'file', 'complex'); 382 | $this->assertNotNull($translation); 383 | $this->assertEquals('Complex', $translation->text); 384 | } 385 | 386 | /** 387 | * @test 388 | */ 389 | public function it_gets_all_items_in_a_group() 390 | { 391 | $array = ['simple' => 'Simple', 'complex' => 'Complex']; 392 | $this->translationRepository->loadArray($array, 'en', 'file'); 393 | $array = ['test2' => 'test']; 394 | $this->translationRepository->loadArray($array, 'en', 'file2'); 395 | 396 | $translations = $this->translationRepository->getItems('en', '*', 'file'); 397 | $this->assertNotNull($translations); 398 | $this->assertEquals(2, count($translations)); 399 | $this->assertEquals('simple', $translations[1]['item']); 400 | $this->assertEquals('Simple', $translations[1]['text']); 401 | $this->assertEquals('complex', $translations[0]['item']); 402 | $this->assertEquals('Complex', $translations[0]['text']); 403 | } 404 | 405 | /** 406 | * @test 407 | */ 408 | public function it_flag_as_unstable() 409 | { 410 | $array = ['simple' => 'Simple', 'complex' => 'Complex']; 411 | $this->translationRepository->loadArray($array, 'es', 'file'); 412 | 413 | $this->translationRepository->flagAsUnstable('*', 'file', 'complex'); 414 | 415 | $translations = $this->translationRepository->pendingReview('es'); 416 | $this->assertEquals(1, $translations->count()); 417 | $this->assertEquals('Complex', $translations[0]->text); 418 | } 419 | 420 | /** 421 | * @test 422 | */ 423 | public function it_searches_by_code_fragment() 424 | { 425 | $array = ['simple' => 'Simple', 'complex' => 'Complex']; 426 | $this->translationRepository->loadArray($array, 'es', 'file', 'namespace'); 427 | $array = ['test' => '2', 'hhh' => 'Juan']; 428 | $this->translationRepository->loadArray($array, 'es', 'fichero'); 429 | 430 | $this->assertEquals(2, $this->translationRepository->search('es', 'space::')->count()); 431 | $this->assertEquals(1, $this->translationRepository->search('es', 'Juan')->count()); 432 | $this->assertEquals(1, $this->translationRepository->search('es', 'st.2')->count()); 433 | $this->assertEquals(0, $this->translationRepository->search('es', 'ple.2')->count()); 434 | } 435 | 436 | /** 437 | * @test 438 | */ 439 | public function it_translates_text() 440 | { 441 | $array = ['lang' => 'Castellano', 'multi' => 'Multiple', 'multi2' => 'Multiple']; 442 | $this->translationRepository->loadArray($array, 'es', 'file'); 443 | $array = ['lang' => 'English', 'other' => 'Random', 'multi' => 'Multi', 'multi2' => 'Many']; 444 | $this->translationRepository->loadArray($array, 'en', 'file'); 445 | 446 | $this->assertEquals(['Castellano'], $this->translationRepository->translateText('English', 'en', 'es')); 447 | $this->assertEquals(['English'], $this->translationRepository->translateText('Castellano', 'es', 'en')); 448 | $this->assertEquals([], $this->translationRepository->translateText('Complex', 'en', 'es')); 449 | $this->assertEquals(['Multi', 'Many'], $this->translationRepository->translateText('Multiple', 'es', 'en')); 450 | } 451 | 452 | /** 453 | * @test 454 | */ 455 | public function test_flag_as_reviewed() 456 | { 457 | $array = ['simple' => 'Simple', 'complex' => 'Complex']; 458 | $this->translationRepository->loadArray($array, 'es', 'file'); 459 | 460 | $this->translationRepository->flagAsUnstable('*', 'file', 'complex'); 461 | $translations = $this->translationRepository->pendingReview('es'); 462 | $this->assertEquals(1, $translations->count()); 463 | $this->translationRepository->flagAsReviewed(2); 464 | $translations = $this->translationRepository->pendingReview('es'); 465 | $this->assertEquals(0, $translations->count()); 466 | } 467 | } 468 | -------------------------------------------------------------------------------- /tests/Routes/ResourceRouteTest.php: -------------------------------------------------------------------------------- 1 | languageRepository = Mockery::mock(LanguageRepository::class); 18 | $this->router = Mockery::mock(Router::class); 19 | $this->registrar = new ResourceRegistrar($this->router, $this->languageRepository); 20 | } 21 | 22 | protected function getMethod() 23 | { 24 | // Set the method to public for testing 25 | $class = new \ReflectionClass(ResourceRegistrar::class); 26 | $method = $class->getMethod('getGroupResourceName'); 27 | $method->setAccessible(true); 28 | return $method; 29 | } 30 | 31 | public function tearDown(): void 32 | { 33 | Mockery::close(); 34 | parent::tearDown(); 35 | } 36 | 37 | /** 38 | * @test 39 | */ 40 | public function test_group_resource_name_filters_out_locales() 41 | { 42 | $this->router->shouldReceive('getLastGroupPrefix')->andReturn('en/admin/blog'); 43 | $this->languageRepository->shouldReceive('availableLocales')->andReturn(['en', 'es']); 44 | $method = $this->getMethod(); 45 | $result = $method->invoke($this->registrar, '', 'post', 'index'); 46 | $this->assertEquals('admin.blog.post.index', $result); 47 | } 48 | 49 | /** 50 | * @test 51 | */ 52 | public function test_group_resource_name_doesnt_mess_with_prefixes_containing_part_of_the_locale() 53 | { 54 | $this->router->shouldReceive('getLastGroupPrefix')->andReturn('en/enabled/enabler'); 55 | $this->languageRepository->shouldReceive('availableLocales')->andReturn(['en', 'es']); 56 | $method = $this->getMethod(); 57 | $result = $method->invoke($this->registrar, '', 'women', 'index'); 58 | $this->assertEquals('enabled.enabler.women.index', $result); 59 | } 60 | 61 | /** 62 | * @test 63 | */ 64 | public function test_only_locale_prefix() 65 | { 66 | $this->router->shouldReceive('getLastGroupPrefix')->andReturn('en'); 67 | $this->languageRepository->shouldReceive('availableLocales')->andReturn(['en', 'es']); 68 | $method = $this->getMethod(); 69 | $result = $method->invoke($this->registrar, '', 'post', 'index'); 70 | $this->assertEquals('post.index', $result); 71 | } 72 | 73 | /** 74 | * @test 75 | */ 76 | public function test_no_locale_prefix() 77 | { 78 | $this->router->shouldReceive('getLastGroupPrefix')->andReturn('admin'); 79 | $this->languageRepository->shouldReceive('availableLocales')->andReturn(['en', 'es']); 80 | $method = $this->getMethod(); 81 | $result = $method->invoke($this->registrar, '', 'post', 'index'); 82 | $this->assertEquals('admin.post.index', $result); 83 | } 84 | 85 | /** 86 | * @test 87 | */ 88 | public function test_no_prefix() 89 | { 90 | $this->router->shouldReceive('getLastGroupPrefix')->andReturn(''); 91 | $this->languageRepository->shouldReceive('availableLocales')->andReturn(['en', 'es']); 92 | $method = $this->getMethod(); 93 | $result = $method->invoke($this->registrar, '', 'post', 'index'); 94 | $this->assertEquals('post.index', $result); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | app['cache']->clear(); 13 | $this->setUpDatabase($this->app); 14 | $this->setUpRoutes($this->app); 15 | } 16 | 17 | /** 18 | * @param \Illuminate\Foundation\Application $app 19 | * 20 | * @return array 21 | */ 22 | protected function getPackageProviders($app) 23 | { 24 | return [ 25 | \Waavi\Translation\TranslationServiceProvider::class, 26 | ]; 27 | } 28 | 29 | /** 30 | * @param $app 31 | */ 32 | protected function getPackageAliases($app) 33 | { 34 | return [ 35 | 'UriLocalizer' => \Waavi\Translation\Facades\UriLocalizer::class, 36 | 'TranslationCache' => \Waavi\Translation\Facades\TranslationCache::class, 37 | ]; 38 | } 39 | 40 | /** 41 | * @param \Illuminate\Foundation\Application $app 42 | */ 43 | protected function getEnvironmentSetUp($app) 44 | { 45 | $app['config']->set('database.default', 'testbench'); 46 | $app['config']->set('database.connections.testbench', [ 47 | 'driver' => 'sqlite', 48 | 'database' => ':memory:', 49 | 'prefix' => '', 50 | ]); 51 | $app['config']->set('app.key', 'sF5r4kJy5HEcOEx3NWxUcYj1zLZLHxuu'); 52 | $app['config']->set('translator.source', 'database'); 53 | } 54 | 55 | /** 56 | * @param \Illuminate\Foundation\Application $app 57 | */ 58 | protected function setUpDatabase($app) 59 | { 60 | $this->artisan('migrate'); 61 | // Seed the spanish and english languages 62 | $languageRepository = \App::make(LanguageRepository::class); 63 | $languageRepository->create(['locale' => 'en', 'name' => 'English']); 64 | $languageRepository->create(['locale' => 'es', 'name' => 'Spanish']); 65 | } 66 | 67 | /** 68 | * @param \Illuminate\Foundation\Application $app 69 | */ 70 | protected function setUpRoutes($app) 71 | { 72 | \Route::get('/', ['middleware' => 'localize', function () { 73 | return 'Whoops'; 74 | }]); 75 | \Route::get('/ca', ['middleware' => 'localize', function () { 76 | return 'Whoops ca'; 77 | }]); 78 | \Route::post('/', ['middleware' => 'localize', function () { 79 | return 'POST answer'; 80 | }]); 81 | \Route::get('/es', ['middleware' => 'localize', function () { 82 | return 'Hola mundo'; 83 | }]); 84 | \Route::get('/en', ['middleware' => 'localize', function () { 85 | return 'Hello world'; 86 | }]); 87 | \Route::get('/en/locale', ['middleware' => 'localize', function () { 88 | return \App::getLocale(); 89 | }]); 90 | \Route::get('/es/locale', ['middleware' => 'localize', function () { 91 | return \App::getLocale(); 92 | }]); 93 | \Route::get('/api/v1/en/locale', ['middleware' => 'localize:2', function () { 94 | return \App::getLocale(); 95 | }]); 96 | \Route::get('/api/v1/es/locale', ['middleware' => 'localize:2', function () { 97 | return \App::getLocale(); 98 | }]); 99 | \Route::get('/api/v1/ca/locale', ['middleware' => 'localize:2', function () { 100 | return 'Whoops ca'; 101 | }]); 102 | \Route::post('/welcome', ['middleware' => 'localize', function () { 103 | return trans('welcome.title'); 104 | }]); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/Traits/TranslatableTest.php: -------------------------------------------------------------------------------- 1 | increments('id'); 19 | $table->string('title')->nullable(); 20 | $table->string('title_translation')->nullable(); 21 | $table->string('slug')->nullable(); 22 | $table->string('text')->nullable(); 23 | $table->string('text_translation')->nullable(); 24 | $table->timestamps(); 25 | }); 26 | $this->languageRepository = \App::make(LanguageRepository::class); 27 | $this->translationRepository = \App::make(TranslationRepository::class); 28 | } 29 | 30 | /** 31 | * @test 32 | */ 33 | public function it_saves_translations() 34 | { 35 | $dummy = new Dummy; 36 | $dummy->title = 'Dummy title'; 37 | $dummy->text = 'Dummy text'; 38 | $saved = $dummy->save() ? true : false; 39 | $this->assertTrue($saved); 40 | $this->assertEquals(1, Dummy::count()); 41 | $this->assertEquals('slug', $dummy->slug); 42 | // Check that there is a language entry in the database: 43 | $titleTranslation = $this->translationRepository->findByLangCode('en', $dummy->translationCodeFor('title')); 44 | $this->assertEquals('Dummy title', $titleTranslation->text); 45 | $this->assertEquals('Dummy title', $dummy->title); 46 | $textTranslation = $this->translationRepository->findByLangCode('en', $dummy->translationCodeFor('text')); 47 | $this->assertEquals('Dummy text', $textTranslation->text); 48 | $this->assertEquals('Dummy text', $dummy->text); 49 | // Delete it: 50 | $deleted = $dummy->delete(); 51 | $this->assertTrue($deleted); 52 | $this->assertEquals(0, Dummy::count()); 53 | $this->assertEquals(0, $this->translationRepository->count()); 54 | } 55 | 56 | /** 57 | * @test 58 | */ 59 | public function it_flushes_cache() 60 | { 61 | $cacheMock = Mockery::mock(\Waavi\Translation\Cache\SimpleRepository::class); 62 | $this->app->bind('translation.cache.repository', function ($app) use ($cacheMock) {return $cacheMock;}); 63 | $cacheMock->shouldReceive('flush')->with('en', 'translatable', '*'); 64 | $dummy = new Dummy; 65 | $dummy->title = 'Dummy title'; 66 | $dummy->text = 'Dummy text'; 67 | $saved = $dummy->save() ? true : false; 68 | $this->assertTrue($saved); 69 | } 70 | 71 | /** 72 | * @test 73 | */ 74 | public function to_array_features_translated_attributes() 75 | { 76 | $dummy = Dummy::create(['title' => 'Dummy title', 'text' => 'Dummy text']); 77 | $this->assertEquals(1, Dummy::count()); 78 | // Change the text on the translation object: 79 | $titleTranslation = $this->translationRepository->findByLangCode('en', $dummy->translationCodeFor('title')); 80 | $titleTranslation->text = 'Translated text'; 81 | $titleTranslation->save(); 82 | // Verify that toArray pulls from the translation and not model's value, and that the _translation attributes are hidden 83 | $this->assertEquals(['title' => 'Translated text', 'text' => 'Dummy text'], $dummy->makeHidden(['created_at', 'updated_at', 'slug', 'id'])->toArray()); 84 | } 85 | } 86 | 87 | class Dummy extends Model 88 | { 89 | use Translatable; 90 | 91 | /** 92 | * @var array 93 | */ 94 | protected $fillable = ['title', 'text']; 95 | 96 | /** 97 | * @var array 98 | */ 99 | protected $translatableAttributes = ['title', 'text']; 100 | 101 | /** 102 | * @param $value 103 | */ 104 | public function setTitleAttribute($value) 105 | { 106 | $this->attributes['title'] = $value; 107 | $this->attributes['slug'] = 'slug'; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/lang/ca/test.php: -------------------------------------------------------------------------------- 1 | 'Entry should not be imported', 5 | ]; 6 | -------------------------------------------------------------------------------- /tests/lang/en/auth.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'label' => 'Enter your credentials', 6 | 'action' => 'Login', 7 | ], 8 | 'simple' => 'Simple', 9 | ]; 10 | -------------------------------------------------------------------------------- /tests/lang/en/empty.php: -------------------------------------------------------------------------------- 1 | '', 5 | 'emptyArray' => [], 6 | ]; 7 | -------------------------------------------------------------------------------- /tests/lang/en/welcome/page.php: -------------------------------------------------------------------------------- 1 | 'Welcome to the test suite', 5 | ]; 6 | -------------------------------------------------------------------------------- /tests/lang/es/auth.php: -------------------------------------------------------------------------------- 1 | [ 5 | //'label' => 'Enter your credentials', 6 | 'action' => 'Identifícate', 7 | ], 8 | ]; 9 | -------------------------------------------------------------------------------- /tests/lang/es/welcome/page.php: -------------------------------------------------------------------------------- 1 | 'Bienvenido', 5 | ]; 6 | -------------------------------------------------------------------------------- /tests/lang/vendor/package/en/example.php: -------------------------------------------------------------------------------- 1 | 'Vendor text', 5 | ]; 6 | -------------------------------------------------------------------------------- /tests/lang/vendor/package/es/example.php: -------------------------------------------------------------------------------- 1 | 'Texto proveedor', 5 | ]; 6 | --------------------------------------------------------------------------------