├── .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 | [](https://packagist.org/packages/waavi/translation)
4 | [](LICENSE.md)
5 | [](https://travis-ci.org/Waavi/translation)
6 | [](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 |
411 | {{ $currentLanguage->name }}
412 |
417 |
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 |
--------------------------------------------------------------------------------