├── .gitignore
├── src
├── config
│ └── multilingual.php
├── TranslationsManager.php
├── MultilingualServiceProvider.php
└── Translatable.php
├── tests
├── models
│ ├── MULTILINGUAL_TEST__UNTRANSLATABLE_PLANET_MODEL.php
│ └── MULTILINGUAL_TEST_PLANET_MODEL.php
├── TestCase.php
├── ValidationTest.php
└── TranslationsTest.php
├── phpunit.xml
├── composer.json
├── LICENSE.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | composer.lock
3 | .idea/
4 | .phpunit.result.cache
5 |
--------------------------------------------------------------------------------
/src/config/multilingual.php:
--------------------------------------------------------------------------------
1 | ['en'],
7 | 'fallback_locale' => 'en',
8 |
9 |
10 | ];
11 |
--------------------------------------------------------------------------------
/tests/models/MULTILINGUAL_TEST__UNTRANSLATABLE_PLANET_MODEL.php:
--------------------------------------------------------------------------------
1 | 'integer',
16 | 'name' => 'array',
17 | ];
18 |
19 | }
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 | ./tests/
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/tests/models/MULTILINGUAL_TEST_PLANET_MODEL.php:
--------------------------------------------------------------------------------
1 | 'integer',
17 | 'name' => 'array',
18 | 'order' => 'integer',
19 | ];
20 |
21 | }
--------------------------------------------------------------------------------
/src/TranslationsManager.php:
--------------------------------------------------------------------------------
1 | translations = $translations;
15 | }
16 |
17 | /**
18 | * @param $key
19 | * @return mixed
20 | */
21 | public function __get($key)
22 | {
23 | return @$this->translations[$key] ?: '';
24 | }
25 |
26 | /**
27 | * Return an array of available locales with values
28 | *
29 | * @return array
30 | */
31 | public function toArray()
32 | {
33 | return $this->translations;
34 | }
35 | }
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "themsaid/laravel-multilingual",
3 | "description": "Easy multilingual laravel models",
4 | "keywords": ["laravel", "multilingual", "translation"],
5 | "homepage": "https://github.com/themsaid/laravel-multilingual",
6 | "license": "MIT",
7 | "authors": [
8 | {
9 | "name": "Mohamed Said",
10 | "email": "theMohamedSaid@gmail.com"
11 | }
12 | ],
13 | "require": {
14 | "illuminate/support": "^5.1||^6.0",
15 | "php" : ">=5.3.0"
16 | },
17 | "require-dev": {
18 | "phpunit/phpunit" : "^8.0",
19 | "orchestra/testbench": "^4.0"
20 | },
21 | "autoload": {
22 | "psr-4": {
23 | "Themsaid\\Multilingual\\": "src"
24 | },
25 | "classmap": [
26 | "tests"
27 | ]
28 | },
29 | "extra": {
30 | "laravel": {
31 | "providers": [
32 | "Themsaid\\Multilingual\\MultilingualServiceProvider"
33 | ]
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Mohamed Said
4 |
5 | > Permission is hereby granted, free of charge, to any person obtaining a copy
6 | > of this software and associated documentation files (the "Software"), to deal
7 | > in the Software without restriction, including without limitation the rights
8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | > copies of the Software, and to permit persons to whom the Software is
10 | > furnished to do so, subject to the following conditions:
11 | >
12 | > The above copyright notice and this permission notice shall be included in
13 | > all copies or substantial portions of the Software.
14 | >
15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | > THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/MultilingualServiceProvider.php:
--------------------------------------------------------------------------------
1 | publishes([
21 | __DIR__.'/config/multilingual.php' => config_path('multilingual.php'),
22 | ]);
23 |
24 |
25 | $this->app['validator']->extendImplicit('translatable_required', function ($attribute, $value, $parameters) use ($systemLocales) {
26 | if (! is_array($value)) {
27 | return false;
28 | }
29 |
30 | // Get only the locales that has a value and exists in the system locales array
31 | $locales = array_filter(array_keys($value), function ($locale) use ($value, $systemLocales) {
32 | return @$value[$locale] && in_array($locale, $systemLocales);
33 | });
34 |
35 | foreach ($systemLocales as $systemLocale) {
36 | if (! in_array($systemLocale, $locales)) {
37 | return false;
38 | }
39 | }
40 |
41 | return true;
42 | });
43 | }
44 |
45 | /**
46 | * Register any package services.
47 | *
48 | * @return void
49 | */
50 | public function register()
51 | {
52 | $this->mergeConfigFrom(
53 | __DIR__.'/config/multilingual.php', 'multilingual'
54 | );
55 | }
56 | }
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | prepareDatabase();
24 | }
25 |
26 | public function test_database_setup()
27 | {
28 | $this->assertTrue(Schema::hasTable('planets'));
29 | }
30 |
31 | /**
32 | * Define environment setup.
33 | *
34 | * @param \Illuminate\Foundation\Application $app
35 | * @return void
36 | */
37 | protected function getEnvironmentSetUp($app)
38 | {
39 | $app['config']->set('multilingual.locales', ['en', 'sp']);
40 | $app['config']->set('multilingual.fallback_locale', 'en');
41 |
42 | $app['config']->set('database.default', 'mysql');
43 | $app['config']->set('database.connections.mysql', [
44 | 'driver' => 'mysql',
45 | 'host' => 'localhost',
46 | 'database' => $this->DBName,
47 | 'username' => $this->DBUsername,
48 | 'password' => $this->DBPassword,
49 | 'charset' => 'utf8',
50 | 'collation' => 'utf8_unicode_ci',
51 | 'prefix' => '',
52 | 'strict' => false,
53 | ]);
54 | }
55 |
56 | /**
57 | * Loading package service provider
58 | *
59 | * @param \Illuminate\Foundation\Application $app
60 | * @return array
61 | */
62 | protected function getPackageProviders($app)
63 | {
64 | return ['Themsaid\Multilingual\MultilingualServiceProvider'];
65 | }
66 |
67 | /**
68 | * Get package aliases.
69 | *
70 | * @param \Illuminate\Foundation\Application $app
71 | *
72 | * @return array
73 | */
74 | protected function getPackageAliases($app)
75 | {
76 | return [
77 | 'Schema' => 'Illuminate\Database\Schema\Blueprint'
78 | ];
79 | }
80 |
81 | public function prepareDatabase()
82 | {
83 | if ( ! Schema::hasTable('planets')) {
84 | Schema::create('planets', function (Blueprint $table) {
85 | $table->increments('id');
86 | $table->string('name');
87 | $table->string('order');
88 | });
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Translatable.php:
--------------------------------------------------------------------------------
1 | translatable)) {
19 | // We check if the attribute is translatable and return a proper
20 | // value based on the current locale
21 | if (in_array($key, $this->translatable)) {
22 | return $this->getValueOfCurrentLocaleForKey($key);
23 | };
24 |
25 | // We check if the attribute is expected to return the
26 | // TranslationManager's object
27 | $translatableKey = str_replace('Translations', '', $key);
28 | if (in_array($translatableKey, $this->translatable)) {
29 | return new TranslationsManager(
30 | $this->getAttributeValue($translatableKey)
31 | );
32 | }
33 | }
34 |
35 | return parent::getAttribute($key);
36 | }
37 |
38 | /**
39 | * Get the value of current locale for $key attribute, if not found it falls
40 | * back to the fallback locale or the first locale in array.
41 | *
42 | * @param $key
43 | * @return string
44 | */
45 | public function getValueOfCurrentLocaleForKey($key)
46 | {
47 | $translations = $this->getAttributeValue($key);
48 | $currentLocale = config('app.locale');
49 | $fallbackLocale = config('multilingual.fallback_locale');
50 |
51 | if ( ! $translations) return "";
52 |
53 | if ( ! @$translations[$currentLocale]) {
54 | return @$translations[$fallbackLocale] ?: '';
55 | };
56 |
57 | return @$translations[$currentLocale];
58 | }
59 |
60 | /**
61 | * Alter the default Illuminate\Database\Eloquent\Model method for checking if attribute
62 | * should be casted as JSON, we check if the attribute is translatable & cast
63 | * it as JSON even if it's not casted as JSON in Model::$casts
64 | *
65 | * @param string $key
66 | * @return bool
67 | */
68 | protected function isJsonCastable($key)
69 | {
70 | if (isset($this->translatable) && in_array($key, $this->translatable)) {
71 | return true;
72 | }
73 |
74 | return parent::isJsonCastable($key);
75 | }
76 |
77 | /**
78 | * Alter the default behaviour when it comes to using json_encode for model attributes,
79 | * this will save the json as UTF-8 to the database instead of escaping characters.
80 | *
81 | * @param array $value
82 | * @return string
83 | */
84 | protected function asJson($value)
85 | {
86 | return json_encode($value, JSON_UNESCAPED_UNICODE);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/tests/ValidationTest.php:
--------------------------------------------------------------------------------
1 | ''],
11 | ['name' => 'translatable_required']
12 | );
13 |
14 | $this->assertTrue($validator->messages()->has('name'));
15 | }
16 |
17 | public function test_validation_fails_if_required_but_empty_array()
18 | {
19 | $validator = Validator::make(
20 | ['name' => []],
21 | ['name' => 'translatable_required']
22 | );
23 |
24 | $this->assertTrue($validator->messages()->has('name'));
25 | }
26 |
27 | public function test_validation_fails_if_required_but_string()
28 | {
29 | $validator = Validator::make(
30 | ['name' => 'This is not cool'],
31 | ['name' => 'translatable_required']
32 | );
33 |
34 | $this->assertTrue($validator->messages()->has('name'));
35 | }
36 |
37 | public function test_validation_fails_if_required_and_has_correct_keys_but_empty_values()
38 | {
39 | $validator = Validator::make(
40 | ['name' => ['en' => '']],
41 | ['name' => 'translatable_required']
42 | );
43 |
44 | $this->assertTrue($validator->messages()->has('name'));
45 | }
46 |
47 | public function test_validation_fails_if_required_and_has_missing_translations()
48 | {
49 | $validator = Validator::make(
50 | ['name' => ['en' => 'One']],
51 | ['name' => 'translatable_required']
52 | );
53 |
54 | $this->assertTrue($validator->messages()->has('name'));
55 | }
56 |
57 | public function test_validation_fails_if_required_and_has_empty_translations()
58 | {
59 | $validator = Validator::make(
60 | ['name' => ['en' => 'One', 'sp' => '']],
61 | ['name' => 'translatable_required']
62 | );
63 |
64 | $this->assertTrue($validator->messages()->has('name'));
65 | }
66 |
67 | public function test_validation_succeed_if_required_and_OK()
68 | {
69 | $validator = Validator::make(
70 | ['name' => ['en' => 'One', 'sp' => 'Uno']],
71 | ['name' => 'translatable_required']
72 | );
73 |
74 | $this->assertFalse($validator->messages()->has('name'));
75 | }
76 |
77 | public function test_only_specific_locales_required()
78 | {
79 | $validator = Validator::make(
80 | ['name' => ['en' => 'One', 'sp' => 'Uno']],
81 | ['name.en' => 'required']
82 | );
83 | $this->assertTrue($validator->passes());
84 |
85 | $validator = Validator::make(
86 | ['name' => ['sp' => 'Uno']],
87 | ['name.en' => 'required']
88 | );
89 | $this->assertFalse($validator->passes());
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Laravel 5 Multilingual Models
2 |
3 | [](https://packagist.org/packages/themsaid/laravel-multilingual)
4 | [](LICENSE.md)
5 | [](https://packagist.org/packages/themsaid/laravel-multilingual)
6 |
7 |
8 | This laravel package makes Eloquent Models attributes translatable without the need to separate database tables for translation values.
9 |
10 | You simply call `$country->name` and you get a value based on your application's current locale.
11 |
12 | You can also call `$country->nameTranslations->en` to get the value of a specific locale.
13 |
14 | You can check all the translations of a given attributes as easy as `$country->nameTranslations->toArray()`.
15 |
16 | ## Installation
17 |
18 | Begin by installing the package through Composer. Run the following command in your terminal:
19 |
20 | ```
21 | composer require themsaid/laravel-multilingual
22 | ```
23 |
24 | Once composer is done, add the package service provider in the providers array in `config/app.php`
25 |
26 | ```
27 | Themsaid\Multilingual\MultilingualServiceProvider::class
28 | ```
29 |
30 | Finally publish the config file:
31 |
32 | ```
33 | php artisan vendor:publish
34 | ```
35 |
36 | That's all, you are now good to go.
37 |
38 | # Usage
39 |
40 | First you need to make sure that the translatable attributes has a mysql field type of text or json, if you are building the database from a migration file you may do this:
41 |
42 | ```php
43 | increments('id');
48 | $table->json('name');
49 | });
50 | ```
51 |
52 | Now that you have the database ready to save a JSON string, you need to prepare your models:
53 |
54 | ```php
55 | 'array'];
64 | }
65 | ```
66 |
67 | - Add the `Translatable` trait to your model class
68 | - Add a public class property `$translatable` as an array that holds the names of the translatable fields in your model.
69 | - Remember to cast the translatable attribute as 'array' in the `$casts` property of the model.
70 |
71 | Now our model has the `name` attribute translatable, so on creating a new Model you may specify the name field as follow:
72 |
73 | ```php
74 | [
78 | 'en' => "Spain",
79 | 'sp' => 'España'
80 | ]
81 | ]);
82 | ```
83 |
84 | It'll be automatically converted to a JSON string and saved in the name field of the database, you can later retrieve the name like this:
85 |
86 | ```
87 | $country->name
88 | ```
89 |
90 | This will return the country name based on the current locale, if the current locale doesn't have a value then the `fallback_locale` defined in the config file will be used.
91 |
92 | In case nothing can be found an empty string will be returned.
93 |
94 | You may also want to return the value for a specific locale, you can do that using the following syntax:
95 |
96 | ```
97 | $country->nameTranslations->en
98 | ```
99 |
100 | This will return the English name of the country.
101 |
102 | To return an array of all the available translations you may use:
103 |
104 | ```
105 | $country->nameTranslations->toArray()
106 | ```
107 |
108 | # Validation
109 | You can use the new array validation features released with laravel 5.2 to validate the presence of specific locales:
110 |
111 | ```php
112 | ['en'=>'One', 'sp'=>'Uno']],
116 | ['name.en' => 'required']
117 | );
118 | ```
119 |
120 | However a validation rule is included in this package that deals with requiring all the validations to be provided:
121 |
122 | ```php
123 | ['en'=>'One', 'sp'=>'Uno']],
127 | ['name' => 'translatable_required']
128 | );
129 | ```
130 |
131 | The `translatable_required` rule will make sure all the values of the available locales are set.
132 |
133 | You may define the available locales as well as the fallback_locale from the package config file.
134 |
135 | Now you only need to add the translated message of our new validation rule, add this to the `validation.php` translation file:
136 |
137 | ```
138 | 'translatable_required' => 'The :attribute translations must be provided.',
139 | ```
140 |
141 | # Queries
142 | If you're using MySQL 5.7 or above, it's recommended that you use the json data type for housing translations in the Database,
143 | this will allow you to query these columns like this:
144 |
145 | ```php
146 | Company::whereRaw('name->"$.en" = \'Monsters Inc.\'')->orderByRaw('specs->"$.founded_at"')->get();
147 | ```
148 |
149 | However in laravel 5.2.23 and above you can use the fluent syntax:
150 |
151 | ```php
152 | Company::where('name->en', 'Monsters Inc.')->orderBy('specs->founded_at')->get();
153 |
154 | ```
155 |
--------------------------------------------------------------------------------
/tests/TranslationsTest.php:
--------------------------------------------------------------------------------
1 | 'Mercury'
19 | ]);
20 |
21 | $this->assertEquals('Mercury', $planet->name);
22 | }
23 |
24 | public function test_translatable_attribute_casted_as_NOT_ARRAY_is_saved_as_json_still()
25 | {
26 | $planet = Planet::create([
27 | 'order' => [
28 | 'en' => 'One',
29 | 'sp' => 'Uno'
30 | ]
31 | ]);
32 |
33 | $this->assertJson($planet->getOriginal('order'));
34 | }
35 |
36 | public function test_translatable_attribute_return_value_of_current_locale()
37 | {
38 | $planet = Planet::create([
39 | 'name' => [
40 | 'en' => 'Mercury',
41 | 'sp' => 'Mercurio'
42 | ]
43 | ]);
44 |
45 | config(['app.locale' => 'sp']);
46 |
47 | $this->assertEquals('Mercurio', $planet->name);
48 | }
49 |
50 | public function test_translatable_attribute_return_empty_string_if_no_translations()
51 | {
52 | $planet = Planet::create();
53 |
54 | config(['app.locale' => 'en']);
55 |
56 | $this->assertEquals('', $planet->name);
57 | }
58 |
59 | public function test_translatable_attribute_return_default_value_if_current_locale_not_exist()
60 | {
61 | $planet = Planet::create([
62 | 'name' => [
63 | 'en' => 'Mercury',
64 | 'sp' => 'Mercurio'
65 | ]
66 | ]);
67 |
68 | config(['multilingual.fallback_locale' => 'sp']);
69 | config(['app.locale' => 'ar']);
70 |
71 | $this->assertEquals('Mercurio', $planet->name);
72 | }
73 |
74 | public function test_translatable_attribute_return_fallback_value_if_current_locale_empty()
75 | {
76 | $planet = Planet::create([
77 | 'name' => [
78 | 'en' => '',
79 | 'sp' => 'Mercurio'
80 | ]
81 | ]);
82 |
83 | config(['multilingual.fallback_locale' => 'sp']);
84 | config(['app.locale' => 'en']);
85 |
86 | $this->assertEquals('Mercurio', $planet->name);
87 | }
88 |
89 | public function test_translatable_attribute_return_empty_value_if_current_and_fallback_locale_empty()
90 | {
91 | $planet = Planet::create([
92 | 'name' => [
93 | 'en' => '',
94 | 'sp' => '',
95 | 'fr' => 'No Idea'
96 | ]
97 | ]);
98 |
99 | config(['multilingual.fallback_locale' => 'sp']);
100 | config(['app.locale' => 'en']);
101 |
102 | $this->assertEquals('', $planet->name);
103 | }
104 |
105 | public function test_returning_array_of_all_translations()
106 | {
107 | $planetName = [
108 | 'en' => 'Mercury',
109 | 'sp' => 'Mercurio'
110 | ];
111 |
112 | $planet = Planet::create([
113 | 'name' => $planetName
114 | ]);
115 |
116 | config(['app.locale' => 'en']);
117 |
118 | $this->assertEquals($planetName, $planet->nameTranslations->toArray());
119 | }
120 |
121 | public function test_returning_the_value_of_specific_locale()
122 | {
123 | $planetName = [
124 | 'en' => 'Mercury',
125 | 'sp' => 'Mercurio'
126 | ];
127 |
128 | $planet = Planet::create([
129 | 'name' => $planetName
130 | ]);
131 |
132 | config(['app.locale' => 'en']);
133 |
134 | $this->assertEquals('Mercurio', $planet->nameTranslations->sp);
135 | }
136 |
137 | public function test_returning_empty_string_if_NO_value_of_specific_locale()
138 | {
139 | $planetName = [
140 | 'en' => 'Mercury',
141 | 'sp' => ''
142 | ];
143 |
144 | $planet = Planet::create([
145 | 'name' => $planetName
146 | ]);
147 |
148 | config(['app.locale' => 'en']);
149 |
150 | $this->assertEquals('', $planet->nameTranslations->sp);
151 | }
152 |
153 | public function test_returning_empty_string_if_value_not_array()
154 | {
155 | $planetId = Planet::insertGetId([
156 | 'name' => 'Earth'
157 | ]);
158 |
159 | $planet = Planet::find($planetId);
160 |
161 | config(['app.locale' => 'en']);
162 |
163 | $this->assertEquals('', $planet->name);
164 | }
165 |
166 | public function test_returning_empty_string_for_a_specific_locale_if_value_not_array()
167 | {
168 | $planetId = Planet::insertGetId([
169 | 'name' => 'Earth'
170 | ]);
171 |
172 | $planet = Planet::find($planetId);
173 |
174 | config(['app.locale' => 'en']);
175 |
176 | $this->assertEquals('', $planet->nameTranslations->sp);
177 | }
178 |
179 | }
180 |
--------------------------------------------------------------------------------