├── .editorconfig ├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── CONTRIBUTING.md ├── CREDITS ├── LICENSE.md ├── README.md ├── build ├── coverage │ └── .gitignore ├── logs │ └── .gitignore ├── pre-commit └── setup.sh ├── composer.json ├── composer.lock ├── phpcs.xml ├── phpunit.xml.dist ├── src ├── MultiLang │ ├── Config.php │ ├── Console │ │ ├── ExportCommand.php │ │ ├── ImportCommand.php │ │ ├── MigrationCommand.php │ │ └── TextsCommand.php │ ├── Facades │ │ └── MultiLang.php │ ├── Middleware │ │ └── MultiLang.php │ ├── Models │ │ ├── Localizable.php │ │ ├── LocalizableScope.php │ │ └── Text.php │ ├── MultiLang.php │ ├── MultiLangServiceProvider.php │ └── Repository.php ├── config │ └── config.php ├── helpers.php ├── stubs │ └── migrations │ │ └── texts.stub └── views │ ├── app.blade.php │ └── index.blade.php └── tests ├── Bootstrap.php └── Unit ├── AbstractTestCase.php ├── ConfigTest.php ├── HelpersTest.php ├── Middleware └── MultiLangTest.php ├── MultiLangFacadeTest.php ├── MultiLangTest.php └── RepositoryTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | # Tab indentation (no size specified) 19 | [Makefile] 20 | indent_style = tab 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE & System Related Files # 2 | .buildpath 3 | .project 4 | .settings 5 | .DS_Store 6 | .idea 7 | .phpintel 8 | composer.phar 9 | .phpunit.result.cache 10 | 11 | # Local System Files (i.e. cache, logs, etc.) # 12 | /cache 13 | /tmp 14 | 15 | # Test Related Files # 16 | /phpunit.xml 17 | 18 | # Composer 19 | vendor/ 20 | 21 | .fuse_hidden* 22 | 23 | # phpDocumentor Logs # 24 | phpdoc-* 25 | 26 | # OSX # 27 | ._* 28 | .Spotlight-V100 29 | .Trashes 30 | _ide_helper.php 31 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | tools: 2 | external_code_coverage: 3 | timeout: 600 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: focal 2 | language: php 3 | 4 | php: 5 | - 7.4 6 | - 8.0 7 | 8 | sudo: false 9 | 10 | before_install: 11 | - composer self-update 12 | 13 | install: 14 | - travis_retry composer update --no-interaction --prefer-source 15 | 16 | script: 17 | - composer phpcs 18 | - composer coverage-clover 19 | 20 | 21 | after_script: 22 | - wget https://scrutinizer-ci.com/ocular.phar 23 | - then php ocular.phar code-coverage:upload --format=php-clover build/logs/clover.xml 24 | 25 | matrix: 26 | fast_finish: true 27 | 28 | 29 | notifications: 30 | on_success: never 31 | on_failure: always 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ------------- 3 | 4 | Before you contribute code to this project, please make sure it conforms to the PSR-2 coding standard 5 | and that the project unit tests still pass. The easiest way to contribute is to work on a checkout of the repository, 6 | or your own fork. If you do this, you can run the following commands to check if everything is ready to submit: 7 | 8 | cd project 9 | composer update 10 | ./vendor/bin/phpcs --standard=phpcs.xml -spn --encoding=utf-8 src/ --report-width=150 11 | 12 | Which should give you no output, indicating that there are no coding standard errors. And then: 13 | 14 | ./vendor/bin/phpunit 15 | 16 | Which should give you no failures or errors. You can ignore any skipped tests as these are for external tools. 17 | 18 | Pushing 19 | ------- 20 | 21 | Development is based on the git flow branching model (see http://nvie.com/posts/a-successful-git-branching-model/ ) 22 | If you fix a bug please push in hotfix branch. 23 | If you develop a new feature please create a new branch. 24 | 25 | Version 26 | ------- 27 | Version number: 0.#version.#hotfix 28 | -------------------------------------------------------------------------------- /CREDITS: -------------------------------------------------------------------------------- 1 | This is at least a partial credits-file of people that have 2 | contributed to the current project. It is sorted by name and 3 | formatted to allow easy grepping and beautification by 4 | scripts. The fields are: name (N), email (E), web-address 5 | (W) and description (D). 6 | Thanks, 7 | 8 | Avtandil Kikabidze 9 | ---------- 10 | 11 | N: Avtandil Kikabidze aka LONGMAN 12 | E: akalongman@gmail.com 13 | W: http://longman.me 14 | D: Project owner, Maintainer 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The [MIT License](http://opensource.org/licenses/mit-license.php) 2 | 3 | Copyright (c) 2016 [Avtandil Kikabidze aka LONGMAN](https://github.com/akalongman) 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel 5.x MultiLanguage 2 | 3 | [![Build Status](https://img.shields.io/travis/akalongman/laravel-multilang/master.svg?style=flat-square)](https://travis-ci.org/akalongman/laravel-multilang) 4 | [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/akalongman/laravel-multilang.svg?style=flat-square)](https://scrutinizer-ci.com/g/akalongman/laravel-multilang/?branch=master) 5 | [![Code Quality](https://img.shields.io/scrutinizer/g/akalongman/laravel-multilang.svg?style=flat-square)](https://scrutinizer-ci.com/g/akalongman/laravel-multilang/?branch=master) 6 | [![Latest Stable Version](https://img.shields.io/github/release/akalongman/laravel-multilang.svg?style=flat-square)](https://github.com/akalongman/laravel-multilang/releases) 7 | [![Total Downloads](https://img.shields.io/packagist/dt/Longman/laravel-multilang.svg)](https://packagist.org/packages/longman/laravel-multilang) 8 | [![Downloads Month](https://img.shields.io/packagist/dm/Longman/laravel-multilang.svg)](https://packagist.org/packages/longman/laravel-multilang) 9 | [![License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 10 | 11 | *This version of MultiLang package requires minimum PHP 7.0. 12 | For older PHP versions use MultiLang [1.x](https://github.com/akalongman/laravel-multilang/releases/tag/1.3.3)* 13 | 14 | This is a very useful package to integrate multi language (multi locale) functionality in Laravel 5.x. 15 | It includes a ServiceProvider to register the multilang and Middleware for automatic modification routes like `http://site.com/en/your-routes`. 16 | 17 | This package uses database for storing translations (it caches data on production environment for improving performance) 18 | Also package automatically adds in database missing keys (on the local environment only). 19 | 20 | ## Table of Contents 21 | - [Installation](#installation) 22 | - [Usage](#usage) 23 | - [Translating](#translating) 24 | - [Blade Templates](#blade-templates) 25 | - [URL Generation](#url-generation) 26 | - [Text Scopes](#text-scopes) 27 | - [Import/Export Texts](#importexport-texts) 28 | - [TODO](#todo) 29 | - [Troubleshooting](#troubleshooting) 30 | - [Contributing](#contributing) 31 | - [License](#license) 32 | - [Credits](#credits) 33 | 34 | 35 | ## Installation 36 | 37 | Install this package through [Composer](https://getcomposer.org/). 38 | 39 | Edit your project's `composer.json` file to require `longman/laravel-multilang` 40 | 41 | Create *composer.json* file: 42 | ```json 43 | { 44 | "name": "yourproject/yourproject", 45 | "type": "project", 46 | "require": { 47 | "longman/laravel-multilang": "~2.0" 48 | } 49 | } 50 | ``` 51 | And run composer update 52 | 53 | **Or** run a command in your command line: 54 | 55 | composer require longman/laravel-multilang 56 | 57 | In Laravel the service provider and facade will automatically get registered. 58 | 59 | Copy the package config to your local config with the publish command: 60 | 61 | php artisan vendor:publish --provider="Longman\LaravelMultiLang\MultiLangServiceProvider" 62 | 63 | After run multilang migration command 64 | 65 | php artisan multilang:migration 66 | 67 | Its creates multilang migration file in your database/migrations folder. After you can run 68 | 69 | php artisan migrate 70 | 71 | Also if you want automatically change locale depending on url (like `http://site.com/en/your-routes`) 72 | you must add middleware in app/Http/Kernel.php 73 | 74 | I suggest add multilang after CheckForMaintenanceMode middleware 75 | ```php 76 | protected $middleware = [ 77 | \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, 78 | \Longman\LaravelMultiLang\Middleware\MultiLang::class, 79 | ]; 80 | ``` 81 | 82 | In your RoutesServiceProvider modify that: 83 | ```php 84 | MultiLang::routeGroup(function($router) { 85 | require app_path('Http/routes.php'); 86 | }); 87 | ``` 88 | 89 | or directly in app/Http/routes.php file add multilang group: 90 | ```php 91 | MultiLang::routeGroup(function($router) { 92 | // your routes and route groups here 93 | }); 94 | ``` 95 | 96 | Or if you want only translate strings without automatic resolving locale and redirect, 97 | you can manually set locale in your application like: 98 | ```php 99 | App::setLocale('en'); 100 | ``` 101 | 102 | 103 | ## Usage 104 | 105 | ### Translating 106 | Everywhere in application you can use `t()` helper function like: 107 | 108 | ```php 109 | $string = t('Your translatable string'); 110 | ``` 111 | 112 | You can use markers for dynamic texts and pass any data like 113 | ```php 114 | $string = t('The :attribute must be a date after :date.', ['attribute' => 'Start Date', 'date' => '7 April 1986']); 115 | ``` 116 | which will be return `The Start Date must be a date after 7 April 1986.` 117 | 118 | ### Blade Templates 119 | In blade templates you can use just `@t()` notation like 120 | ```php 121 | @t('Your translatable string') 122 | ``` 123 | which is equivalent to `{{ t('Your translatable string') }}` 124 | 125 | ### URL Generation 126 | Also you can use `lang_url()` helper function for appending current lang marker in urls automatically. 127 | 128 | ```php 129 | $url = lang_url('users'); // which returns /en/users depending on your language (locale) 130 | ``` 131 | 132 | You can force locale and get localized url for current url for example. 133 | 134 | ```php 135 | $url = lang_url('users', [], null, 'ka'); // which returns /ka/users ignoring current locale 136 | ``` 137 | or 138 | ```php 139 | $url = lang_url('en/users', [], null, 'ka'); // also returns /ka/users 140 | ``` 141 | 142 | Also you use named routes via `lang_route()` function 143 | 144 | ```php 145 | $url = lang_route('users'); // which returns en.users depending on your language (locale) 146 | ``` 147 | 148 | Also `Request::locale()` always will return current locale. 149 | 150 | *Note*: Texts will be selected after firing Laravel's `LocaleUpdated` event. Therefore you should use MultiLang middleware, or manually set locale in the application. 151 | 152 | ### Text Scopes 153 | If you want group translations by some scope, in package available defining of scopes. 154 | For example to define scope `admin` in application, you should call: 155 | 156 | ```php 157 | app('multilang')->setScope('admin'); 158 | ``` 159 | 160 | before setting the locale. 161 | 162 | *Note*: Default scope is `global` 163 | 164 | ### Import/Export Texts 165 | For versioning texts with source code (git/svn) and easy management, there is possible import texts from yml file and also export in the file. 166 | 167 | yml file format is: 168 | 169 | ```yml 170 | - 171 | key: 'authorization' 172 | texts: 173 | en: 'Authorization' 174 | ge: 'ავტორიზაცია' 175 | - 176 | key: 'registration' 177 | texts: 178 | en: 'Registration' 179 | ge: 'რეგისტრაცია' 180 | ``` 181 | 182 | Run commands for possible options and more information: 183 | 184 | php artisan help multilang:import 185 | 186 | php artisan help multilang:export 187 | 188 | 189 | ## TODO 190 | 191 | write more tests 192 | 193 | ## Troubleshooting 194 | 195 | If you like living on the edge, please report any bugs you find on the 196 | [laravel-multilang issues](https://github.com/akalongman/laravel-multilang/issues) page. 197 | 198 | ## Contributing 199 | 200 | Pull requests are welcome. 201 | See [CONTRIBUTING.md](CONTRIBUTING.md) for information. 202 | 203 | ## License 204 | 205 | Please see the [LICENSE](LICENSE.md) included in this repository for a full copy of the MIT license, 206 | which this project is licensed under. 207 | 208 | ## Credits 209 | 210 | - [Avtandil Kikabidze aka LONGMAN](https://github.com/akalongman) 211 | 212 | Full credit list in [CREDITS](CREDITS) 213 | -------------------------------------------------------------------------------- /build/coverage/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /build/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /build/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PROJECT=`php -r "echo dirname(dirname(dirname(realpath('$0'))));"` 4 | STAGED_FILES_CMD=`git diff --cached --name-only --diff-filter=ACMR HEAD | grep \\\\.php` 5 | 6 | SFILES=${SFILES:-$STAGED_FILES_CMD} 7 | 8 | echo "Checking PHP Lint..." 9 | for FILE in $SFILES 10 | do 11 | php -l -d display_errors=0 $PROJECT/$FILE 12 | if [ $? != 0 ] 13 | then 14 | echo "Fix the error before commit." 15 | exit 1 16 | fi 17 | FILES="$FILES $PROJECT/$FILE" 18 | done 19 | 20 | if [ "$FILES" != "" ] 21 | then 22 | echo "Running Code Sniffer..." 23 | ./vendor/bin/phpcs --standard=phpcs.xml -p -n --encoding=utf-8 $FILES 24 | if [ $? != 0 ] 25 | then 26 | echo "Fix the error before commit." 27 | exit 1 28 | fi 29 | fi 30 | 31 | exit $? -------------------------------------------------------------------------------- /build/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cp build/pre-commit .git/hooks/pre-commit 4 | chmod +x .git/hooks/pre-commit -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "longman/laravel-multilang", 3 | "type": "library", 4 | "description": "Package to integrate multi language (multi locale) functionality in Laravel 5.x", 5 | "keywords": [ 6 | "locale", 7 | "localization", 8 | "translation", 9 | "language", 10 | "laravel", 11 | "package", 12 | "multilang" 13 | ], 14 | "license": "MIT", 15 | "homepage": "https://github.com/akalongman/laravel-multilang", 16 | "support": { 17 | "issues": "https://github.com/akalongman/laravel-multilang/issues", 18 | "source": "https://github.com/akalongman/laravel-multilang" 19 | }, 20 | "authors": [ 21 | { 22 | "name": "Avtandil Kikabidze aka LONGMAN", 23 | "email": "akalongman@gmail.com", 24 | "homepage": "https://longman.me", 25 | "role": "Maintainer, Developer" 26 | } 27 | ], 28 | "require": { 29 | "php": "^8.1.0", 30 | "ext-mbstring": "*", 31 | "symfony/yaml": "^6.0", 32 | "symfony/translation": "^6.0", 33 | "illuminate/console": "^10.0", 34 | "illuminate/support": "^10.0", 35 | "illuminate/database": "^10.0", 36 | "illuminate/http": "^10.0" 37 | }, 38 | "require-dev": { 39 | "mockery/mockery": "~1.3", 40 | "phpunit/phpunit": "~10.0", 41 | "longman/php-code-style": "^10.0", 42 | "orchestra/testbench": "^8.5" 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "Longman\\LaravelMultiLang\\": "src/MultiLang" 47 | }, 48 | "files": [ 49 | "src/helpers.php" 50 | ] 51 | }, 52 | "autoload-dev": { 53 | "psr-4": { 54 | "Tests\\": "tests/" 55 | } 56 | }, 57 | "config": { 58 | "sort-packages": true, 59 | "allow-plugins": { 60 | "dealerdirect/phpcodesniffer-composer-installer": true 61 | } 62 | }, 63 | "extra": { 64 | "laravel": { 65 | "providers": [ 66 | "Longman\\LaravelMultiLang\\MultiLangServiceProvider" 67 | ], 68 | "aliases": { 69 | "MultiLang": "Longman\\LaravelMultiLang\\Facades\\MultiLang" 70 | } 71 | } 72 | }, 73 | "scripts": { 74 | "phpcs": "./vendor/bin/phpcs --standard=phpcs.xml -spn --encoding=utf-8 src/ tests/ --report-width=150", 75 | "phpcbf": "./vendor/bin/phpcbf --standard=phpcs.xml -spn --encoding=utf-8 src/ tests/ --report-width=150", 76 | "test": "./vendor/bin/phpunit -c phpunit.xml.dist", 77 | "coverage-clover": "./vendor/bin/phpunit --stop-on-failure --coverage-clover build/logs/clover.xml", 78 | "coverage-html": "./vendor/bin/phpunit --stop-on-failure --coverage-html build/coverage" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | */tests/* 8 | 9 | *\.blade\.php$ 10 | 11 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ./tests/ 33 | 34 | 35 | 36 | 37 | ./src 38 | 39 | ./src/MultiLang/TextsTrait.php 40 | ./src/MultiLang/Console 41 | ./src/config 42 | ./src/stubs 43 | ./src/views 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/MultiLang/Config.php: -------------------------------------------------------------------------------- 1 | data = $data; 28 | } 29 | 30 | /** 31 | * Get config parameter 32 | * 33 | * @param string $key 34 | * @param mixed $default 35 | * @return array|mixed|null 36 | */ 37 | public function get(?string $key = null, $default = null) 38 | { 39 | $array = $this->data; 40 | 41 | if ($key === null) { 42 | return $array; 43 | } 44 | 45 | if (array_key_exists($key, $array)) { 46 | return $array[$key]; 47 | } 48 | 49 | foreach (explode('.', $key) as $segment) { 50 | if (is_array($array) && array_key_exists($segment, $array)) { 51 | $array = $array[$segment]; 52 | } else { 53 | return $default; 54 | } 55 | } 56 | 57 | return $array; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/MultiLang/Console/ExportCommand.php: -------------------------------------------------------------------------------- 1 | table = config('multilang.db.texts_table', 'texts'); 91 | $this->db = $this->getDatabase(); 92 | 93 | $lang = $this->option('lang'); 94 | if (! empty($lang)) { 95 | $this->langs = explode(',', $lang); 96 | } 97 | 98 | $scopes = $this->scopes; 99 | $scope = $this->option('scope'); 100 | if (! empty($scope)) { 101 | $scopes = explode(',', $scope); 102 | foreach ($scopes as $scope) { 103 | if (! in_array($scope, $this->scopes)) { 104 | throw new InvalidArgumentException('Scope "' . $scope . '" is not found! Available scopes is ' . implode(', ', $this->scopes)); 105 | } 106 | } 107 | } 108 | 109 | $path = $this->option('path', 'storage/multilang'); 110 | $this->path = base_path($path); 111 | if (! is_dir($this->path)) { 112 | if (! mkdir($this->path, 0777, true)) { 113 | throw new InvalidArgumentException('unable to create the folder "' . $this->path . '"!'); 114 | } 115 | } 116 | if (! is_writable($this->path)) { 117 | throw new InvalidArgumentException('Folder "' . $this->path . '" is not writable!'); 118 | } 119 | 120 | $force = (bool) $this->option('force'); 121 | $clear = (bool) $this->option('clear'); 122 | foreach ($scopes as $scope) { 123 | $this->export($scope, $force, $clear); 124 | } 125 | 126 | return; 127 | 128 | /*$texts = Text::where('scope', 'site')->where('lang', 'en')->get()->toArray(); 129 | 130 | 131 | $newTexts = []; 132 | foreach($texts as $text) { 133 | $arr = []; 134 | $arr['key'] = $text['key']; 135 | $arr['texts']['en'] = $text['value']; 136 | $arr['texts']['ir'] = $text['value']; 137 | 138 | $newTexts[] = $arr; 139 | } 140 | 141 | $yaml = Yaml::dump($newTexts, 3, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK);*/ 142 | 143 | $path = storage_path('texts/site.yml'); 144 | 145 | //dump(file_put_contents($path, $yaml)); 146 | //die; 147 | 148 | $value = Yaml::parse(file_get_contents($path)); 149 | dump($value); 150 | die; 151 | 152 | //$this->info('Database backup restored successfully'); 153 | } 154 | 155 | protected function export(string $scope = 'global', bool $force = false, bool $clear = false) 156 | { 157 | $dbTexts = $this->getTextsFromDb($scope); 158 | 159 | $fileTexts = ! $clear ? $this->getTextsFromFile($scope) : []; 160 | 161 | $textsToWrite = $force ? array_replace_recursive($fileTexts, $dbTexts) : array_replace_recursive($dbTexts, $fileTexts); 162 | 163 | // Reset keys 164 | $textsToWrite = array_values($textsToWrite); 165 | 166 | $yaml = Yaml::dump($textsToWrite, 3, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); 167 | 168 | $path = $this->path . '/' . $scope . '.yml'; 169 | $written = file_put_contents($path, $yaml); 170 | if (! $written) { 171 | $this->error('Export texts of "' . $scope . '" is failed!'); 172 | } 173 | 174 | $this->info('Export texts of "' . $scope . '" is finished in "' . $path . '"'); 175 | } 176 | 177 | /** 178 | * Get a texts from file. 179 | * 180 | * @param string $scope 181 | * @return array 182 | */ 183 | protected function getTextsFromFile(string $scope): array 184 | { 185 | $fileTexts = []; 186 | $path = $this->path . '/' . $scope . '.yml'; 187 | if (is_readable($path)) { 188 | $fileTexts = Yaml::parse(file_get_contents($path)); 189 | } 190 | 191 | $formattedFileTexts = []; 192 | foreach ($fileTexts as $text) { 193 | $formattedFileTexts[$text['key']] = $text; 194 | } 195 | 196 | return $formattedFileTexts; 197 | } 198 | 199 | /** 200 | * Get a texts from database. 201 | * 202 | * @param string $scope 203 | * @return array 204 | */ 205 | protected function getTextsFromDb(string $scope): array 206 | { 207 | $dbTexts = $this->db 208 | ->table($this->table) 209 | ->where('scope', $scope) 210 | ->get(); 211 | 212 | $formattedDbTexts = []; 213 | foreach ($dbTexts as $text) { 214 | $key = $text->key; 215 | $lang = $text->lang; 216 | if (! isset($formattedDbTexts[$key])) { 217 | $formattedDbTexts[$key] = ['key' => $key]; 218 | } 219 | $formattedDbTexts[$key]['texts'][$lang] = $text->value; 220 | } 221 | 222 | return $formattedDbTexts; 223 | } 224 | 225 | /** 226 | * Get a database connection instance. 227 | * 228 | * @return \Illuminate\Database\Connection 229 | */ 230 | protected function getDatabase(): Connection 231 | { 232 | $connection = config('multilang.db.connection', 'default'); 233 | $db = App::make(Database::class); 234 | if ($connection === 'default') { 235 | return $db->connection(); 236 | } 237 | 238 | return $db->connection($connection); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/MultiLang/Console/ImportCommand.php: -------------------------------------------------------------------------------- 1 | table = config('multilang.db.texts_table', 'texts'); 87 | $this->db = $this->getDatabase(); 88 | 89 | $lang = $this->option('lang'); 90 | if (! empty($lang)) { 91 | $this->langs = explode(',', $lang); 92 | } 93 | 94 | $scopes = $this->scopes; 95 | $scope = $this->option('scope'); 96 | if (! empty($scope)) { 97 | $scopes = explode(',', $scope); 98 | foreach ($scopes as $scope) { 99 | if (! in_array($scope, $this->scopes)) { 100 | throw new InvalidArgumentException('Scope "' . $scope . '" is not found! Available scopes is ' . implode(', ', $this->scopes)); 101 | } 102 | } 103 | } 104 | 105 | $path = $this->option('path', 'storage/multilang'); 106 | $this->path = base_path($path); 107 | if (! is_dir($this->path)) { 108 | throw new InvalidArgumentException('Folder "' . $this->path . '" is not accessible!'); 109 | } 110 | 111 | $force = $this->option('force'); 112 | $clear = $this->option('clear'); 113 | foreach ($scopes as $scope) { 114 | $this->import($scope, $force, $clear); 115 | } 116 | } 117 | 118 | protected function import(string $scope = 'global', bool $force = false, bool $clear = false) 119 | { 120 | $path = $this->path . '/' . $scope . '.yml'; 121 | if (! is_readable($path)) { 122 | $this->warn('File "' . $path . '" is not readable!'); 123 | 124 | return false; 125 | } 126 | $data = Yaml::parse(file_get_contents($path)); 127 | if (empty($data)) { 128 | $this->warn('File "' . $path . '" is empty!'); 129 | 130 | return false; 131 | } 132 | 133 | if ($clear) { 134 | $this->db 135 | ->table($this->table) 136 | ->where('scope', $scope) 137 | ->delete(); 138 | } 139 | 140 | $createdAt = Carbon::now()->toDateTimeString(); 141 | $updatedAt = $createdAt; 142 | $inserted = 0; 143 | $updated = 0; 144 | foreach ($data as $text) { 145 | $key = $text['key']; 146 | 147 | foreach ($text['texts'] as $lang => $value) { 148 | if (! empty($this->langs) && ! in_array($lang, $this->langs)) { 149 | continue; 150 | } 151 | 152 | $row = $this->db 153 | ->table($this->table) 154 | ->where('scope', $scope) 155 | ->where('key', $key) 156 | ->where('lang', $lang) 157 | ->first(); 158 | 159 | if (empty($row)) { 160 | // insert row 161 | $ins = []; 162 | $ins['key'] = $key; 163 | $ins['lang'] = $lang; 164 | $ins['scope'] = $scope; 165 | $ins['value'] = $value; 166 | $ins['created_at'] = $createdAt; 167 | $ins['updated_at'] = $updatedAt; 168 | $this->db 169 | ->table($this->table) 170 | ->insert($ins); 171 | $inserted++; 172 | } else { 173 | if ($force) { 174 | // force update row 175 | $upd = []; 176 | $upd['key'] = $key; 177 | $upd['lang'] = $lang; 178 | $upd['scope'] = $scope; 179 | $upd['value'] = $value; 180 | $upd['updated_at'] = $updatedAt; 181 | $this->db 182 | ->table($this->table) 183 | ->where('key', $key) 184 | ->where('lang', $lang) 185 | ->where('scope', $scope) 186 | ->update($upd); 187 | $updated++; 188 | } 189 | } 190 | } 191 | } 192 | 193 | $this->info('Import texts of "' . $scope . '" is finished. Inserted: ' . $inserted . ', Updated: ' . $updated); 194 | } 195 | 196 | /** 197 | * Get a database connection instance. 198 | * 199 | * @return \Illuminate\Database\Connection 200 | */ 201 | protected function getDatabase(): Connection 202 | { 203 | $connection = config('multilang.db.connection', 'default'); 204 | $db = App::make(Database::class); 205 | if ($connection === 'default') { 206 | return $db->connection(); 207 | } 208 | 209 | return $db->connection($connection); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/MultiLang/Console/MigrationCommand.php: -------------------------------------------------------------------------------- 1 | error('Couldn\'t create migration.' . PHP_EOL . 'Table name can\'t be empty. Check your configuration.'); 45 | 46 | return; 47 | } 48 | 49 | $this->line(''); 50 | $this->info('Tables: ' . $table); 51 | 52 | $message = 'A migration that creates "' . $table . '" tables will be created in database/migrations directory'; 53 | 54 | $this->comment($message); 55 | $this->line(''); 56 | 57 | if ($this->confirm('Proceed with the migration creation? [Yes|no]', true)) { 58 | $this->line(''); 59 | 60 | $this->info('Creating migration...'); 61 | if ($this->createMigration($table)) { 62 | $this->info('Migration successfully created!'); 63 | } else { 64 | $this->error( 65 | 'Couldn\'t create migration.' . PHP_EOL . ' Check the write permissions 66 | within the database/migrations directory.', 67 | ); 68 | } 69 | 70 | $this->line(''); 71 | } 72 | } 73 | 74 | /** 75 | * Create the migration. 76 | * 77 | * @param string $table 78 | * @return bool 79 | */ 80 | protected function createMigration(string $table): bool 81 | { 82 | $migrationFile = base_path('database/migrations') . '/' . date('Y_m_d_His') . '_create_multi_lang_texts_table.php'; 83 | 84 | if (file_exists($migrationFile)) { 85 | return false; 86 | } 87 | 88 | $stubPath = __DIR__ . '/../../stubs/migrations/texts.stub'; 89 | $content = file_get_contents($stubPath); 90 | if (empty($content)) { 91 | return false; 92 | } 93 | 94 | $data = str_replace('{{TEXTS_TABLE}}', $table, $content); 95 | 96 | if (! file_put_contents($migrationFile, $data)) { 97 | return false; 98 | } 99 | 100 | return true; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/MultiLang/Console/TextsCommand.php: -------------------------------------------------------------------------------- 1 | option('lang'); 36 | $scope = $this->option('scope'); 37 | 38 | $texts = app('multilang')->getAllTexts($lang, $scope); 39 | 40 | if (empty($texts)) { 41 | $this->info('Application texts is empty'); 42 | 43 | return false; 44 | } 45 | 46 | $headers = ['#', 'Text Key', 'Language', 'Scope', 'Text Value']; 47 | 48 | $rows = []; 49 | $i = 1; 50 | foreach ($texts as $lang => $items) { 51 | foreach ($items as $key => $item) { 52 | $row = [$i, $key, $item->lang, $item->scope, $item->value]; 53 | $rows[] = $row; 54 | $i++; 55 | } 56 | } 57 | $this->table($headers, $rows); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/MultiLang/Facades/MultiLang.php: -------------------------------------------------------------------------------- 1 | app = $app; 44 | $this->redirector = $redirector; 45 | } 46 | 47 | /** 48 | * Handle an incoming request. 49 | * 50 | * @param \Illuminate\Http\Request $request 51 | * @param \Closure $next 52 | * @return mixed 53 | */ 54 | public function handle(Request $request, Closure $next) 55 | { 56 | if (! $this->app->bound(MultiLangLib::class)) { 57 | return $next($request); 58 | } 59 | $multilang = $this->app->make(MultiLangLib::class); 60 | 61 | $url = $multilang->getRedirectUrl($request); 62 | 63 | if (! empty($url)) { 64 | if ($request->expectsJson()) { 65 | return response('Not found', 404); 66 | } else { 67 | return $this->redirector->to($url); 68 | } 69 | } 70 | 71 | $locale = $multilang->detectLocale($request); 72 | 73 | $this->app->setLocale($locale); 74 | 75 | if ($multilang->getConfig()->get('set_carbon_locale')) { 76 | Carbon::setLocale($locale); 77 | } 78 | 79 | if ($multilang->getConfig()->get('set_system_locale')) { 80 | $locales = $multilang->getLocales(); 81 | if (! empty($locales[$locale]['full_locale'])) { 82 | $lcList = $multilang->getConfig()->get('system_locale_lc', LC_ALL); 83 | if (is_array($lcList)) { 84 | foreach ($lcList as $lc) { 85 | setlocale((int) $lc, $locales[$locale]['full_locale']); 86 | } 87 | } else { 88 | setlocale((int) $lcList, $locales[$locale]['full_locale']); 89 | } 90 | } 91 | } 92 | 93 | return $next($request); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/MultiLang/Models/Localizable.php: -------------------------------------------------------------------------------- 1 | getTable() . '.' . $this->getLocalizableColumn(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/MultiLang/Models/LocalizableScope.php: -------------------------------------------------------------------------------- 1 | queryHasLocalizableColumn($builder)) { 26 | $builder->where($model->getQualifiedLocalizableColumn(), '=', app()->getLocale()); 27 | } 28 | } 29 | 30 | /** 31 | * Check if query has "localizable" column 32 | * 33 | * @param \Illuminate\Database\Eloquent\Builder $builder 34 | * @return bool 35 | */ 36 | protected function queryHasLocalizableColumn(Builder $builder) 37 | { 38 | $wheres = $builder->getQuery()->wheres; 39 | $column = $this->getLocalizableColumn($builder); 40 | if (! empty($wheres)) { 41 | foreach ($wheres as $where) { 42 | if (isset($where['column']) && $where['column'] === $column) { 43 | return true; 44 | } 45 | } 46 | } 47 | 48 | return false; 49 | } 50 | 51 | /** 52 | * Get the "localizable" column for the builder. 53 | * 54 | * @param \Illuminate\Database\Eloquent\Builder $builder 55 | * @return string 56 | */ 57 | protected function getLocalizableColumn(Builder $builder) 58 | { 59 | if (count($builder->getQuery()->joins) > 0) { 60 | return $builder->getModel()->getQualifiedLocalizableColumn(); 61 | } else { 62 | return $builder->getModel()->getLocalizableColumn(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/MultiLang/Models/Text.php: -------------------------------------------------------------------------------- 1 | environment = $environment; 96 | 97 | $this->setConfig($config); 98 | 99 | $this->setRepository(new Repository($this->config, $cache, $db)); 100 | } 101 | 102 | /** 103 | * Set multilang config 104 | * 105 | * @param array $config 106 | * @return $this 107 | */ 108 | public function setConfig(array $config): MultiLang 109 | { 110 | $this->config = new Config($config); 111 | 112 | return $this; 113 | } 114 | 115 | /** 116 | * Get multilang config 117 | * 118 | * @return \Longman\LaravelMultiLang\Config 119 | */ 120 | public function getConfig(): Config 121 | { 122 | 123 | return $this->config; 124 | } 125 | 126 | /** 127 | * Set repository object 128 | * 129 | * @param \Longman\LaravelMultiLang\Repository $repository 130 | * @return $this 131 | */ 132 | public function setRepository(Repository $repository): MultiLang 133 | { 134 | $this->repository = $repository; 135 | 136 | return $this; 137 | } 138 | 139 | /** 140 | * Get repository object 141 | * 142 | * @return \Longman\LaravelMultiLang\Repository 143 | */ 144 | public function getRepository(): Repository 145 | { 146 | return $this->repository; 147 | } 148 | 149 | /** 150 | * Set application scope 151 | * 152 | * @param $scope 153 | * @return $this 154 | */ 155 | public function setScope($scope): MultiLang 156 | { 157 | $this->scope = $scope; 158 | 159 | return $this; 160 | } 161 | 162 | /** 163 | * Get application scope 164 | * 165 | * @return string 166 | */ 167 | public function getScope(): string 168 | { 169 | return $this->scope; 170 | } 171 | 172 | /** 173 | * Set locale 174 | * 175 | * @param string $lang 176 | * @return void 177 | */ 178 | public function setLocale(string $lang) 179 | { 180 | if (! $lang) { 181 | throw new InvalidArgumentException('Locale is empty'); 182 | } 183 | $this->lang = $lang; 184 | } 185 | 186 | public function loadTexts(?string $locale = null, ?string $scope = null): array 187 | { 188 | if (is_null($locale)) { 189 | $locale = $this->getLocale(); 190 | } 191 | 192 | if (is_null($scope)) { 193 | $scope = $this->getScope(); 194 | } 195 | 196 | if ($this->environment !== 'production' || $this->config->get('cache.enabled', true) === false) { 197 | $texts = $this->repository->loadFromDatabase($locale, $scope); 198 | } else { 199 | if ($this->repository->existsInCache($locale, $scope)) { 200 | $texts = $this->repository->loadFromCache($locale, $scope); 201 | } else { 202 | $texts = $this->repository->loadFromDatabase($locale, $scope); 203 | $this->repository->storeInCache($locale, $texts, $scope); 204 | } 205 | } 206 | 207 | $this->createTranslator($locale, $scope, $texts); 208 | 209 | $this->texts = $texts; 210 | 211 | return $texts; 212 | } 213 | 214 | /** 215 | * Get translated text 216 | * 217 | * @param string $key 218 | * @param array $replacements 219 | * @return string 220 | */ 221 | public function get(string $key, array $replacements = []): string 222 | { 223 | if (! $this->getConfig()->get('use_texts', true)) { 224 | throw new InvalidArgumentException('Using texts from database is disabled in config'); 225 | } 226 | 227 | if (empty($key)) { 228 | throw new InvalidArgumentException('Text key not provided'); 229 | } 230 | 231 | if (! $this->lang) { 232 | return $key; 233 | } 234 | 235 | if (is_null($this->texts)) { 236 | // Load texts from storage 237 | $this->loadTexts(); 238 | } 239 | 240 | if (! isset($this->texts[$key])) { 241 | $this->queueToSave($key); 242 | } 243 | 244 | if (! empty($replacements)) { 245 | $keys = array_keys($replacements); 246 | $keys = array_map(static function ($v) { 247 | return ':' . $v; 248 | }, $keys); 249 | $replacements = array_combine($keys, $replacements); 250 | } 251 | 252 | return $this->translator->trans($key, $replacements, $this->getScope()); 253 | } 254 | 255 | /** 256 | * Get redirect url in middleware 257 | * 258 | * @param \Illuminate\Http\Request $request 259 | * @return string 260 | */ 261 | public function getRedirectUrl(Request $request): string 262 | { 263 | $excludePatterns = $this->config->get('exclude_segments', []); 264 | if (! empty($excludePatterns)) { 265 | if (call_user_func_array([$request, 'is'], $excludePatterns)) { 266 | return ''; 267 | } 268 | } 269 | 270 | $locale = $request->segment(1); 271 | $fallbackLocale = $this->config->get('default_locale', 'en'); 272 | if (! empty($locale) && strlen($locale) === 2) { 273 | $locales = $this->config->get('locales', []); 274 | 275 | if (! isset($locales[$locale])) { 276 | $segments = $request->segments(); 277 | $segments[0] = $fallbackLocale; 278 | $url = implode('/', $segments); 279 | $queryString = $request->server->get('QUERY_STRING'); 280 | if ($queryString) { 281 | $url .= '?' . $queryString; 282 | } 283 | 284 | return $url; 285 | } 286 | } else { 287 | $segments = $request->segments(); 288 | $url = $fallbackLocale . '/' . implode('/', $segments); 289 | $queryString = $request->server->get('QUERY_STRING'); 290 | if ($queryString) { 291 | $url .= '?' . $queryString; 292 | } 293 | 294 | return $url; 295 | } 296 | 297 | return ''; 298 | } 299 | 300 | /** 301 | * Detect locale based on url segment 302 | * 303 | * @param \Illuminate\Http\Request $request 304 | * @return string 305 | */ 306 | public function detectLocale(Request $request): string 307 | { 308 | $locale = $request->segment(1); 309 | $locales = $this->config->get('locales'); 310 | 311 | if (isset($locales[$locale])) { 312 | return $locales[$locale]['locale'] ?? $locale; 313 | } 314 | 315 | return (string) $this->config->get('default_locale', 'en'); 316 | } 317 | 318 | /** 319 | * Wrap routes to available languages group 320 | * 321 | * @param \Closure $callback 322 | * @return void 323 | */ 324 | public function routeGroup(Closure $callback) 325 | { 326 | $router = app('router'); 327 | 328 | $locales = $this->config->get('locales', []); 329 | 330 | foreach ($locales as $locale => $val) { 331 | $router->group([ 332 | 'prefix' => $locale, 333 | 'as' => $locale . '.', 334 | ], $callback); 335 | } 336 | } 337 | 338 | /** 339 | * Get texts 340 | * 341 | * @return array 342 | */ 343 | public function getTexts(): array 344 | { 345 | 346 | return $this->texts; 347 | } 348 | 349 | /** 350 | * Get all texts 351 | * 352 | * @param string $lang 353 | * @param string $scope 354 | * @return array 355 | */ 356 | public function getAllTexts(?string $lang = null, ?string $scope = null): array 357 | { 358 | return $this->repository->loadAllFromDatabase($lang, $scope); 359 | } 360 | 361 | /** 362 | * Set texts manually 363 | * 364 | * @param array $textsArray 365 | * @return \Longman\LaravelMultiLang\MultiLang 366 | */ 367 | public function setTexts(array $textsArray): MultiLang 368 | { 369 | $texts = []; 370 | foreach ($textsArray as $key => $value) { 371 | $texts[$key] = $value; 372 | } 373 | 374 | $this->texts = $texts; 375 | 376 | $this->createTranslator($this->getLocale(), $this->getScope(), $texts); 377 | 378 | return $this; 379 | } 380 | 381 | /** 382 | * Get language prefixed url 383 | * 384 | * @param string $path 385 | * @param string $lang 386 | * @return string 387 | */ 388 | public function getUrl(string $path, ?string $lang = null): string 389 | { 390 | $locale = $lang ?: $this->getLocale(); 391 | if ($locale) { 392 | $path = $locale . '/' . $this->removeLocaleFromPath($path); 393 | } 394 | 395 | return $path; 396 | } 397 | 398 | /** 399 | * Get language prefixed route 400 | * 401 | * @param string $name 402 | * @return string 403 | */ 404 | public function getRoute(string $name): string 405 | { 406 | $locale = $this->getLocale(); 407 | if ($locale) { 408 | $name = $locale . '.' . $name; 409 | } 410 | 411 | return $name; 412 | } 413 | 414 | /** 415 | * Check if autosave allowed 416 | * 417 | * @return bool 418 | */ 419 | public function autoSaveIsAllowed() 420 | { 421 | if ($this->environment === 'local' && $this->config->get('db.autosave', true)) { 422 | return true; 423 | } 424 | 425 | return false; 426 | } 427 | 428 | /** 429 | * Get locale 430 | * 431 | * @return string 432 | */ 433 | public function getLocale() 434 | { 435 | return $this->lang ?? $this->config->get('default_locale'); 436 | } 437 | 438 | /** 439 | * Get available locales 440 | * 441 | * @return array 442 | */ 443 | public function getLocales(): array 444 | { 445 | return (array) $this->config->get('locales'); 446 | } 447 | 448 | /** 449 | * Save missing texts 450 | * 451 | * @return bool 452 | */ 453 | public function saveTexts(): bool 454 | { 455 | if (empty($this->newTexts)) { 456 | return false; 457 | } 458 | 459 | return $this->repository->save($this->newTexts, $this->scope); 460 | } 461 | 462 | protected function createTranslator(string $locale, string $scope, array $texts): Translator 463 | { 464 | $this->translator = new Translator($locale); 465 | $this->translator->addLoader('array', new ArrayLoader()); 466 | $this->translator->addResource('array', $texts, $locale, $scope); 467 | 468 | return $this->translator; 469 | } 470 | 471 | /** 472 | * Queue missing texts 473 | * 474 | * @param string $key 475 | * @return void 476 | */ 477 | protected function queueToSave(string $key) 478 | { 479 | $this->newTexts[$key] = $key; 480 | } 481 | 482 | /** 483 | * Remove locale from the path 484 | * 485 | * @param string $path 486 | * @return string 487 | */ 488 | private function removeLocaleFromPath(string $path): string 489 | { 490 | $langPath = $path; 491 | 492 | // Remove domain from path 493 | $appUrl = config('app.url', ''); 494 | if (! empty($appUrl) && mb_substr($langPath, 0, mb_strlen($appUrl)) === $appUrl) { 495 | $langPath = ltrim(str_replace($appUrl, '', $langPath), '/'); 496 | } 497 | 498 | $locales = $this->config->get('locales'); 499 | $locale = mb_substr($langPath, 0, 2); 500 | if (isset($locales[$locale])) { 501 | return mb_substr($langPath, 3); 502 | } 503 | 504 | return $path; 505 | } 506 | } 507 | -------------------------------------------------------------------------------- /src/MultiLang/MultiLangServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes( 28 | [ 29 | __DIR__ . '/../config/config.php' => config_path('multilang.php'), 30 | __DIR__ . '/../views' => base_path('resources/views/vendor/multilang'), 31 | ], 32 | ); 33 | 34 | // Append the country settings 35 | $this->mergeConfigFrom( 36 | __DIR__ . '/../config/config.php', 37 | 'multilang', 38 | ); 39 | 40 | // Register blade directives 41 | $this->getBlade()->directive('t', static function ($expression) { 42 | return ""; 43 | }); 44 | 45 | $this->app['events']->listen(RouteMatched::class, function () { 46 | $scope = $this->app['config']->get('app.scope'); 47 | if ($scope && $scope !== 'global') { 48 | $this->app['multilang']->setScope($scope); 49 | } 50 | }); 51 | 52 | $this->app['events']->listen(LocaleUpdated::class, function ($event) { 53 | $this->app['multilang']->setLocale($event->locale); 54 | }); 55 | 56 | $this->loadViewsFrom(__DIR__ . '/../views', 'multilang'); 57 | } 58 | 59 | /** 60 | * Register any application services. 61 | * 62 | * @return void 63 | */ 64 | public function register() 65 | { 66 | $configPath = __DIR__ . '/../config/config.php'; 67 | $this->mergeConfigFrom($configPath, 'multilang'); 68 | 69 | $this->app->singleton('multilang', function ($app) { 70 | $environment = $app->environment(); 71 | $config = $app['config']->get('multilang'); 72 | 73 | $multilang = new MultiLang( 74 | $environment, 75 | $config, 76 | $app['cache'], 77 | $app['db'], 78 | ); 79 | 80 | if ($multilang->autoSaveIsAllowed()) { 81 | $app->terminating(function () use ($multilang) { 82 | $scope = $this->app['config']->get('app.scope'); 83 | if ($scope && $scope !== 'global') { 84 | $multilang->setScope($scope); 85 | } 86 | 87 | return $multilang->saveTexts(); 88 | }); 89 | } 90 | 91 | return $multilang; 92 | }); 93 | 94 | $this->app->alias('multilang', MultiLang::class); 95 | 96 | $this->app->singleton( 97 | 'command.multilang.migration', 98 | static function () { 99 | return new MigrationCommand(); 100 | }, 101 | ); 102 | 103 | $this->app->singleton( 104 | 'command.multilang.texts', 105 | static function () { 106 | return new TextsCommand(); 107 | }, 108 | ); 109 | 110 | $this->app->singleton( 111 | 'command.multilang.import', 112 | static function () { 113 | return new ImportCommand(); 114 | }, 115 | ); 116 | 117 | $this->app->singleton( 118 | 'command.multilang.export', 119 | static function () { 120 | return new ExportCommand(); 121 | }, 122 | ); 123 | 124 | $this->commands( 125 | [ 126 | 'command.multilang.migration', 127 | 'command.multilang.texts', 128 | 'command.multilang.import', 129 | 'command.multilang.export', 130 | ], 131 | ); 132 | 133 | $this->app->make('request')->macro('locale', static function () { 134 | return app('multilang')->getLocale(); 135 | }); 136 | } 137 | 138 | /** 139 | * Get the services provided by the provider. 140 | * 141 | * @return array 142 | */ 143 | public function provides() 144 | { 145 | return [ 146 | 'multilang', 147 | MultiLang::class, 148 | 'command.multilang.migration', 149 | 'command.multilang.texts', 150 | 'command.multilang.import', 151 | 'command.multilang.export', 152 | ]; 153 | } 154 | 155 | private function getBlade(): BladeCompiler 156 | { 157 | return $this->app->make('view')->getEngineResolver()->resolve('blade')->getCompiler(); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/MultiLang/Repository.php: -------------------------------------------------------------------------------- 1 | config = $config; 48 | $this->cache = $cache; 49 | $this->db = $db; 50 | } 51 | 52 | /** 53 | * Get cache key name based on lang and scope 54 | * 55 | * @param string $lang 56 | * @param string $scope 57 | * @return string 58 | */ 59 | public function getCacheName(string $lang, ?string $scope = null): string 60 | { 61 | $key = $this->config->get('db.texts_table', 'texts') . '_' . $lang; 62 | if (! is_null($scope)) { 63 | $key .= '_' . $scope; 64 | } 65 | 66 | return $key; 67 | } 68 | 69 | /** 70 | * Load texts from database storage 71 | * 72 | * @param string $lang 73 | * @param string $scope 74 | * @return array 75 | */ 76 | public function loadFromDatabase(string $lang, ?string $scope = null): array 77 | { 78 | $query = $this->getDb()->table($this->getTableName()) 79 | ->where('lang', $lang); 80 | 81 | if (! is_null($scope) && $scope !== 'global') { 82 | $query = $query->whereNested(static function ($query) use ($scope) { 83 | $query->where('scope', 'global'); 84 | $query->orWhere('scope', $scope); 85 | }); 86 | } else { 87 | $query = $query->where('scope', 'global'); 88 | } 89 | 90 | $texts = $query->get(['key', 'value', 'lang', 'scope']); 91 | 92 | $array = []; 93 | foreach ($texts as $row) { 94 | $array[$row->key] = $row->value; 95 | } 96 | 97 | return $array; 98 | } 99 | 100 | /** 101 | * Load all texts from database storage 102 | * 103 | * @param string $lang 104 | * @param string $scope 105 | * @return array 106 | */ 107 | public function loadAllFromDatabase(?string $lang = null, ?string $scope = null): array 108 | { 109 | $query = $this->getDb()->table($this->getTableName()); 110 | 111 | if (! is_null($lang)) { 112 | $query = $query->where('lang', $lang); 113 | } 114 | 115 | if (! is_null($scope)) { 116 | $query = $query->whereNested(static function ($query) use ($scope) { 117 | $query->where('scope', 'global'); 118 | $query->orWhere('scope', $scope); 119 | }); 120 | } 121 | 122 | $texts = $query->get(); 123 | 124 | $array = []; 125 | foreach ($texts as $row) { 126 | $array[$row->lang][$row->key] = $row; 127 | } 128 | 129 | return $array; 130 | } 131 | 132 | /** 133 | * Load texts from cache storage 134 | * 135 | * @param string $lang 136 | * @param string $scope 137 | * @return array 138 | */ 139 | public function loadFromCache(string $lang, ?string $scope = null): array 140 | { 141 | $texts = $this->getCache()->get($this->getCacheName($lang, $scope), []); 142 | 143 | return $texts; 144 | } 145 | 146 | /** 147 | * Store texts in cache 148 | * 149 | * @param string $lang 150 | * @param array $texts 151 | * @param string $scope 152 | * @return $this 153 | */ 154 | public function storeInCache(string $lang, array $texts, ?string $scope = null): Repository 155 | { 156 | $this->getCache()->put($this->getCacheName($lang, $scope), $texts, $this->config->get('cache.lifetime', 1440)); 157 | 158 | return $this; 159 | } 160 | 161 | /** 162 | * Check if we must load texts from cache 163 | * 164 | * @param string $lang 165 | * @param string $scope 166 | * @return bool 167 | */ 168 | public function existsInCache(string $lang, ?string $scope = null): bool 169 | { 170 | return $this->getCache()->has($this->getCacheName($lang, $scope)); 171 | } 172 | 173 | /** 174 | * Save missing texts in database 175 | * 176 | * @param array $texts 177 | * @param string $scope 178 | * @return bool 179 | */ 180 | public function save(array $texts, ?string $scope = null): bool 181 | { 182 | if (empty($texts)) { 183 | return false; 184 | } 185 | 186 | $table = $this->getTableName(); 187 | $locales = $this->config->get('locales', []); 188 | if (is_null($scope)) { 189 | $scope = 'global'; 190 | } 191 | 192 | $now = Carbon::now()->toDateTimeString(); 193 | foreach ($texts as $k => $v) { 194 | foreach ($locales as $lang => $localeData) { 195 | $exists = $this->getDb() 196 | ->table($table) 197 | ->where([ 198 | 'key' => $k, 199 | 'lang' => $lang, 200 | 'scope' => $scope, 201 | ])->first(); 202 | 203 | if ($exists) { 204 | continue; 205 | } 206 | 207 | $this->getDb() 208 | ->table($table) 209 | ->insert([ 210 | 'key' => $k, 211 | 'lang' => $lang, 212 | 'scope' => $scope, 213 | 'value' => $v, 214 | 'created_at' => $now, 215 | 'updated_at' => $now, 216 | ]); 217 | } 218 | } 219 | 220 | return true; 221 | } 222 | 223 | /** 224 | * Get texts table name 225 | * 226 | * @return string 227 | */ 228 | public function getTableName(): string 229 | { 230 | $table = $this->config->get('db.texts_table', 'texts'); 231 | 232 | return (string) $table; 233 | } 234 | 235 | /** 236 | * Get a database connection instance. 237 | * 238 | * @return \Illuminate\Database\Connection 239 | */ 240 | protected function getDb(): Connection 241 | { 242 | $connection = $this->config->get('db.connection'); 243 | if ($connection === 'default') { 244 | return $this->db->connection(); 245 | } 246 | 247 | return $this->db->connection($connection); 248 | } 249 | 250 | /** 251 | * Get a cache driver instance. 252 | * 253 | * @return \Illuminate\Contracts\Cache\Repository 254 | */ 255 | protected function getCache(): CacheRepository 256 | { 257 | $store = $this->config->get('cache.store', 'default'); 258 | if ($store === 'default') { 259 | return $this->cache->store(); 260 | } 261 | 262 | return $this->cache->store($store); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/config/config.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'en' => [ 16 | 'name' => 'English', 17 | 'native_name' => 'English', 18 | 'locale' => 'en', // ISO 639-1 19 | 'canonical_locale' => 'en_GB', // ISO 3166-1 20 | 'full_locale' => 'en_GB.UTF-8', 21 | ], 22 | 23 | // Add yours here 24 | ], 25 | 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | Fallback locale/language 29 | |-------------------------------------------------------------------------- 30 | | 31 | | Fallback locale for routing 32 | | 33 | */ 34 | 'default_locale' => 'en', 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Set Carbon locale 39 | |-------------------------------------------------------------------------- 40 | | 41 | | Call Carbon::setLocale($locale) and set current locale in middleware 42 | | 43 | */ 44 | 'set_carbon_locale' => true, 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Set System locale 49 | |-------------------------------------------------------------------------- 50 | | 51 | | Call setlocale() and set current locale in middleware 52 | | 53 | */ 54 | 'set_system_locale' => true, 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | Locale LC 59 | |-------------------------------------------------------------------------- 60 | | 61 | | Which locale to set. You can specify array of locale types, e.g LC_TIME, LC_CTYPE 62 | | 63 | */ 64 | 'system_locale_lc' => LC_ALL, 65 | 66 | /* 67 | |-------------------------------------------------------------------------- 68 | | Exclude segments from redirect 69 | |-------------------------------------------------------------------------- 70 | | 71 | | Exclude segments from redirects in the middleware 72 | | 73 | */ 74 | 'exclude_segments' => [ 75 | // 76 | ], 77 | 78 | /* 79 | |-------------------------------------------------------------------------- 80 | | Texts Management 81 | |-------------------------------------------------------------------------- 82 | */ 83 | 84 | /* 85 | |-------------------------------------------------------------------------- 86 | | Use Texts from Database/Cache 87 | |-------------------------------------------------------------------------- 88 | | 89 | | Load or not translations from database/cache 90 | | 91 | */ 92 | 'use_texts' => true, 93 | 94 | /* 95 | |-------------------------------------------------------------------------- 96 | | Cache Configuration 97 | |-------------------------------------------------------------------------- 98 | | 99 | | Cache parameters 100 | | 101 | */ 102 | 'cache' => [ 103 | 'enabled' => true, 104 | 'store' => env('CACHE_DRIVER', 'default'), 105 | 'lifetime' => 1440, 106 | ], 107 | 108 | /* 109 | |-------------------------------------------------------------------------- 110 | | DB Configuration 111 | |-------------------------------------------------------------------------- 112 | | 113 | | DB parameters 114 | | 115 | */ 116 | 'db' => [ 117 | 'autosave' => true, // Autosave missing texts in the database. Only when environment is local 118 | 'connection' => env('DB_CONNECTION', 'default'), 119 | 'texts_table' => 'texts', 120 | ], 121 | 122 | ]; 123 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | getUrl($path, $locale); 24 | 25 | return url($path, $parameters, $secure); 26 | } 27 | } 28 | 29 | if (! function_exists('lang_redirect')) { 30 | /** 31 | * Get an instance of the redirector. 32 | * 33 | * @param string|null $to 34 | * @param int $status 35 | * @param array $headers 36 | * @param bool $secure 37 | * @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse 38 | */ 39 | function lang_redirect(?string $to = null, int $status = 302, array $headers = [], ?bool $secure = null) 40 | { 41 | if (is_null($to)) { 42 | return app('redirect'); 43 | } 44 | 45 | $multilang = app('multilang'); 46 | 47 | $to = $multilang->getUrl($to); 48 | 49 | return app('redirect')->to($to, $status, $headers, $secure); 50 | } 51 | } 52 | 53 | if (! function_exists('lang_route')) { 54 | /** 55 | * Get route by name 56 | * 57 | * @param string $name 58 | * @param array $parameters 59 | * @param bool $absolute 60 | * @return string 61 | */ 62 | function lang_route(string $name, array $parameters = [], bool $absolute = true): string 63 | { 64 | $multilang = app('multilang'); 65 | 66 | $name = $multilang->getRoute($name); 67 | 68 | return app('url')->route($name, $parameters, $absolute); 69 | } 70 | } 71 | 72 | if (! function_exists('t')) { 73 | /** 74 | * Get translated text 75 | * 76 | * @param string $text 77 | * @param array $replace 78 | * @return string 79 | */ 80 | function t(string $text, array $replace = []): string 81 | { 82 | $text = app('multilang')->get($text, $replace); 83 | 84 | return $text; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/stubs/migrations/texts.stub: -------------------------------------------------------------------------------- 1 | table = '{{TEXTS_TABLE}}'; 18 | 19 | } 20 | 21 | /** 22 | * Run the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function up() 27 | { 28 | Schema::create($this->table, function (Blueprint $table) { 29 | $table->char('key'); 30 | $table->char('lang', 2); 31 | $table->longText('value'); 32 | $table->enum('scope', ['admin', 'site', 'global'])->default('global'); 33 | $table->timestamps(); 34 | $table->primary(['key', 'lang', 'scope']); 35 | }); 36 | } 37 | 38 | /** 39 | * Reverse the migrations. 40 | * 41 | * @return void 42 | */ 43 | public function down() 44 | { 45 | Schema::drop($this->table); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/views/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Text management 9 | 10 | 11 | 12 | 13 |
14 | @yield('content') 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/views/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('multilang::app') 2 | 3 | @section('content') 4 | 33 | @if (count(session('errors')) > 0) 34 |
35 | 40 |
41 | @endif 42 |
43 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | @forelse ($texts as $text) 58 | 59 | 60 | 68 | 69 | 70 | @empty 71 | 72 | 73 | 74 | @endforelse 75 | 76 |
KeyValueLang
{{$text->key}} 61 | 67 | {{$text->lang}}
Texts not found
77 |
78 | 79 |
80 | 81 | @endsection 82 | -------------------------------------------------------------------------------- /tests/Bootstrap.php: -------------------------------------------------------------------------------- 1 | app['db']->getSchemaBuilder(); 31 | $schema->dropIfExists('texts'); 32 | $schema->create('texts', static function (Blueprint $table) { 33 | $table->char('key'); 34 | $table->char('lang', 2); 35 | $table->text('value')->nullable(); 36 | $table->enum('scope', ['admin', 'site', 'global'])->default('global'); 37 | $table->timestamps(); 38 | $table->primary(['key', 'lang', 'scope']); 39 | }); 40 | 41 | for ($i = 0; $i <= 10; $i++) { 42 | $this->app->db->table('texts')->insert([ 43 | 'key' => 'text key ' . $i, 44 | 'lang' => 'ka', 45 | 'value' => 'text value ' . $i, 46 | ]); 47 | } 48 | } 49 | 50 | protected function getMultilang(string $env = 'testing', array $config = []): MultiLang 51 | { 52 | $cache = $this->app->cache; 53 | $database = $this->app->db; 54 | 55 | $defaultConfig = include __DIR__ . '/../../src/config/config.php'; 56 | $config = array_replace_recursive($defaultConfig, $config); 57 | 58 | $multilang = new MultiLang($env, $config, $cache, $database); 59 | 60 | return $multilang; 61 | } 62 | 63 | protected function getRepository(array $config = []): Repository 64 | { 65 | $cache = $this->app->cache; 66 | $database = $this->app->db; 67 | 68 | $defaultConfig = include __DIR__ . '/../../src/config/config.php'; 69 | $config = array_replace_recursive($defaultConfig, $config); 70 | 71 | $config = $this->getConfig($config); 72 | 73 | $repository = new Repository($config, $cache, $database); 74 | 75 | return $repository; 76 | } 77 | 78 | protected function getConfig(array $config): Config 79 | { 80 | $configObject = new Config($config); 81 | return $configObject; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/Unit/ConfigTest.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'ka' => [ 17 | 'name' => 'Georgian', 18 | ], 19 | ], 20 | ]; 21 | $config = $this->getConfig($configData); 22 | 23 | $this->assertEquals($configData, $config->get()); 24 | } 25 | 26 | /** 27 | * @test 28 | */ 29 | public function set_get() 30 | { 31 | $config = [ 32 | 'locales' => [ 33 | 'ka' => [ 34 | 'name' => 'Georgian', 35 | ], 36 | ], 37 | ]; 38 | $config = $this->getConfig($config); 39 | 40 | $this->assertEquals('Georgian', $config->get('locales.ka.name')); 41 | } 42 | 43 | /** 44 | * @test 45 | */ 46 | public function get_default_value() 47 | { 48 | $config = [ 49 | 'locales' => [ 50 | 'ka' => [ 51 | 'name' => 'Georgian', 52 | ], 53 | ], 54 | ]; 55 | $config = $this->getConfig($config); 56 | 57 | $this->assertEquals('Thai', $config->get('locales.th.name', 'Thai')); 58 | } 59 | 60 | /** 61 | * @test 62 | */ 63 | public function table_name() 64 | { 65 | $config = [ 66 | 'db' => [ 67 | 'autosave' => true, 68 | 'connection' => 'mysql', 69 | 'texts_table' => 'texts', 70 | ], 71 | ]; 72 | 73 | $config = $this->getConfig($config); 74 | 75 | $this->assertEquals('texts', $config->get('db.texts_table')); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/Unit/HelpersTest.php: -------------------------------------------------------------------------------- 1 | createTable(); 19 | } 20 | 21 | /** 22 | * @test 23 | */ 24 | public function t_should_return_valid_translation() 25 | { 26 | /** @var \Longman\LaravelMultiLang\MultiLang $multilang */ 27 | $multilang = app('multilang'); 28 | 29 | $texts = [ 30 | 'text1' => 'value1', 31 | 'text2' => 'value2', 32 | 'te.x-t/3' => 'value3', 33 | ]; 34 | 35 | $multilang->setLocale('ka'); 36 | $multilang->setTexts($texts); 37 | 38 | $this->assertEquals('value1', t('text1')); 39 | } 40 | 41 | /** 42 | * @test 43 | */ 44 | public function lang_url_should_return_valid_url() 45 | { 46 | /** @var \Longman\LaravelMultiLang\MultiLang $multilang */ 47 | $multilang = app('multilang'); 48 | 49 | $texts = [ 50 | 'text1' => 'value1', 51 | 'text2' => 'value2', 52 | 'te.x-t/3' => 'value3', 53 | ]; 54 | 55 | $multilang->setLocale('ka', $texts); 56 | 57 | $this->assertEquals('http://localhost/ka/users/list', lang_url('users/list')); 58 | } 59 | 60 | /** 61 | * @test 62 | */ 63 | public function lang_url_should_return_url_generator_if_path_is_null() 64 | { 65 | $this->assertInstanceOf(UrlGenerator::class, lang_url()); 66 | } 67 | 68 | /** @test */ 69 | public function lang_redirect_should_return_redirector_instance_if_path_is_null() 70 | { 71 | $this->assertInstanceOf(Redirector::class, lang_redirect()); 72 | } 73 | 74 | /** @test */ 75 | public function lang_redirect_should_return_redirect_response() 76 | { 77 | /** @var \Longman\LaravelMultiLang\MultiLang $multilang */ 78 | $multilang = app('multilang'); 79 | $multilang->setLocale('ka'); 80 | 81 | $redirect = lang_redirect('path', 302, ['X-header' => 'value']); 82 | 83 | $this->assertInstanceOf(RedirectResponse::class, $redirect); 84 | $this->assertEquals('http://localhost/ka/path', $redirect->getTargetUrl()); 85 | $this->assertEquals($redirect->headers->get('X-header'), 'value'); 86 | $this->assertEquals($redirect->getStatusCode(), 302); 87 | } 88 | 89 | /** @test */ 90 | public function lang_route_should_throw_exception_if_route_is_not_defined() 91 | { 92 | $this->expectException(InvalidArgumentException::class); 93 | lang_route('missing-route'); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/Unit/Middleware/MultiLangTest.php: -------------------------------------------------------------------------------- 1 | createTable(); 18 | } 19 | 20 | /** 21 | * @test 22 | */ 23 | public function handle_no_redirect() 24 | { 25 | $multilang = $this->getMultilang(); 26 | $multilang->setLocale('en'); 27 | $middleware = new MultiLangMiddleware($this->app, $this->app->redirect, $multilang); 28 | 29 | $request = new Request( 30 | $query = [], 31 | $request = [], 32 | $attributes = [], 33 | $cookies = [], 34 | $files = [], 35 | $server = ['REQUEST_URI' => '/en/auth/login'], 36 | $content = null, 37 | ); 38 | 39 | $result = $middleware->handle($request, static function (Request $request) { 40 | 41 | return 'no_redirect'; 42 | }); 43 | 44 | $this->assertEquals('no_redirect', $result); 45 | } 46 | 47 | /** 48 | * @test 49 | */ 50 | public function handle_non_exists_language_must_redirect() 51 | { 52 | $multilang = $this->getMultilang(); 53 | $multilang->setLocale('en'); 54 | $middleware = new MultiLangMiddleware($this->app, $this->app->redirect, $multilang); 55 | 56 | $request = new Request( 57 | $query = [], 58 | $request = [], 59 | $attributes = [], 60 | $cookies = [], 61 | $files = [], 62 | $server = ['REQUEST_URI' => '/ka/auth/login'], 63 | $content = null, 64 | ); 65 | 66 | $result = $middleware->handle($request, static function () { 67 | return 'no_redirect'; 68 | }); 69 | 70 | $location = $result->headers->get('location'); 71 | 72 | $this->assertEquals('http://localhost/en/auth/login', $location); 73 | } 74 | 75 | /** 76 | * @test 77 | */ 78 | public function handle_no_language_must_redirect() 79 | { 80 | $multilang = $this->getMultilang(); 81 | $multilang->setLocale('en'); 82 | $middleware = new MultiLangMiddleware($this->app, $this->app->redirect, $multilang); 83 | 84 | $request = new Request( 85 | $query = [], 86 | $request = [], 87 | $attributes = [], 88 | $cookies = [], 89 | $files = [], 90 | $server = ['REQUEST_URI' => '/auth/login'], 91 | $content = null, 92 | ); 93 | 94 | $result = $middleware->handle($request, static function () { 95 | return 'no_redirect'; 96 | }); 97 | 98 | $location = $result->headers->get('location'); 99 | 100 | $this->assertEquals('http://localhost/en/auth/login', $location); 101 | } 102 | 103 | /** 104 | * @test 105 | */ 106 | public function handle_query_string() 107 | { 108 | $multilang = $this->getMultilang(); 109 | $multilang->setLocale('en'); 110 | $middleware = new MultiLangMiddleware($this->app, $this->app->redirect, $multilang); 111 | 112 | $queryString = 'param1=value1¶m2=value2'; 113 | $requestUri = '/ka/auth/login?' . $queryString; 114 | 115 | $request = new Request( 116 | $query = [], 117 | $request = [], 118 | $attributes = [], 119 | $cookies = [], 120 | $files = [], 121 | $server = ['REQUEST_URI' => $requestUri, 'QUERY_STRING' => $queryString], 122 | $content = null, 123 | ); 124 | 125 | $result = $middleware->handle($request, static function () { 126 | return 'no_redirect'; 127 | }); 128 | 129 | $location = $result->headers->get('location'); 130 | 131 | $this->assertEquals('http://localhost/en/auth/login?' . $queryString, $location); 132 | } 133 | 134 | /** 135 | * @test 136 | */ 137 | public function return_404_not_found_response_when_json_is_requested_on_non_existing_url() 138 | { 139 | $multilang = $this->getMultilang(); 140 | $multilang->setLocale('en'); 141 | $middleware = new MultiLangMiddleware($this->app, $this->app->redirect, $multilang); 142 | 143 | $request = new Request( 144 | $query = [], 145 | $request = [], 146 | $attributes = [], 147 | $cookies = [], 148 | $files = [], 149 | $server = ['REQUEST_URI' => '/ka/auth/login'], 150 | $content = null, 151 | ); 152 | $request->headers->set('accept', 'application/json'); 153 | 154 | /** @var \Illuminate\Http\Response $response */ 155 | $response = $middleware->handle($request, static function () { 156 | return '404'; 157 | }); 158 | 159 | $this->assertTrue($request->expectsJson()); 160 | $this->assertEquals(404, $response->getStatusCode()); 161 | $this->assertEquals('Not found', $response->getContent()); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /tests/Unit/MultiLangFacadeTest.php: -------------------------------------------------------------------------------- 1 | app['db']->getSchemaBuilder(); 18 | 19 | $schema->create('texts', static function (Blueprint $table) { 20 | $table->char('key'); 21 | $table->char('lang', 2); 22 | $table->text('value')->default(''); 23 | $table->enum('scope', ['admin', 'site', 'global'])->default('global'); 24 | $table->timestamps(); 25 | $table->primary(['key', 'lang', 'scope']); 26 | }); 27 | 28 | $this->inited = true; 29 | } 30 | 31 | /** @test */ 32 | public function call_facade() 33 | { 34 | MultiLangFacade::setLocale('en'); 35 | $texts = [ 36 | 'text1' => 'Custom Text', 37 | ]; 38 | MultiLangFacade::setTexts($texts); 39 | 40 | $this->assertEquals(MultiLangFacade::get('text1'), 'Custom Text'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Unit/MultiLangTest.php: -------------------------------------------------------------------------------- 1 | createTable(); 20 | } 21 | 22 | /** 23 | * @test 24 | */ 25 | public function get_locale() 26 | { 27 | $multilang = $this->getMultilang(); 28 | $multilang->setLocale('ka'); 29 | 30 | $this->assertEquals('ka', $multilang->getLocale()); 31 | 32 | $multilang = $this->getMultilang(); 33 | $multilang->setLocale('en'); 34 | 35 | $this->assertEquals('en', $multilang->getLocale()); 36 | } 37 | 38 | /** 39 | * @test 40 | */ 41 | public function get_url() 42 | { 43 | $multilang = $this->getMultilang(); 44 | $multilang->setLocale('ka'); 45 | 46 | $this->assertEquals('ka/users', $multilang->getUrl('users')); 47 | } 48 | 49 | /** 50 | * @test 51 | */ 52 | public function get_url_with_forced_locale() 53 | { 54 | $config = [ 55 | 'locales' => [ 56 | 'en' => [ 57 | 'name' => 'English', 58 | 'native_name' => 'English', 59 | 'default' => true, 60 | ], 61 | 'ka' => [ 62 | 'name' => 'Georgian', 63 | 'native_name' => 'ქართული', 64 | 'default' => false, 65 | ], 66 | ], 67 | ]; 68 | $multilang = $this->getMultilang('local', $config); 69 | $multilang->setLocale('ka'); 70 | 71 | $this->assertEquals('en/users', $multilang->getUrl('users', 'en')); 72 | $this->assertEquals('en/users', $multilang->getUrl('ka/users', 'en')); 73 | // With locale which not exists 74 | $this->assertEquals('en/ss/users', $multilang->getUrl('ss/users', 'en')); 75 | } 76 | 77 | /** 78 | * @test 79 | */ 80 | public function get_route() 81 | { 82 | $multilang = $this->getMultilang(); 83 | $multilang->setLocale('ka'); 84 | 85 | $this->assertEquals('ka.users', $multilang->getRoute('users')); 86 | } 87 | 88 | /** 89 | * @test 90 | */ 91 | public function check_get_texts() 92 | { 93 | $multilang = $this->getMultilang('testing'); 94 | $texts = [ 95 | 'text1' => 'value1', 96 | 'text2' => 'value2', 97 | 'te.x-t/3' => 'value3', 98 | ]; 99 | 100 | $multilang->setLocale('ka'); 101 | $multilang->setTexts($texts); 102 | 103 | $this->assertEquals($texts, $multilang->getTexts()); 104 | } 105 | 106 | /** 107 | * @test 108 | */ 109 | public function get_text_value() 110 | { 111 | $multilang = $this->getMultilang(); 112 | $multilang->setLocale('ka'); 113 | 114 | $multilang->setTexts([ 115 | 'text1' => 'value1', 116 | 'text2' => 'value2', 117 | 'te.x-t/3' => 'value3', 118 | ]); 119 | 120 | $this->assertEquals('value1', $multilang->get('text1')); 121 | 122 | $this->assertEquals('value3', $multilang->get('te.x-t/3')); 123 | } 124 | 125 | /** 126 | * @test 127 | */ 128 | public function should_return_key_when_no_lang() 129 | { 130 | $multilang = $this->getMultilang(); 131 | 132 | $this->assertEquals('value5', $multilang->get('value5')); 133 | } 134 | 135 | /** 136 | * @test 137 | */ 138 | public function should_return_key() 139 | { 140 | $multilang = $this->getMultilang(); 141 | $multilang->setLocale('ka'); 142 | 143 | $multilang->setTexts([ 144 | 'text1' => 'value1', 145 | 'text2' => 'value2', 146 | 'te.x-t/3' => 'value3', 147 | ]); 148 | 149 | $this->assertEquals('value5', $multilang->get('value5')); 150 | } 151 | 152 | /** 153 | * @test 154 | */ 155 | public function set_get_texts() 156 | { 157 | $multilang = $this->getMultilang(); 158 | $multilang->setLocale('ka'); 159 | 160 | $texts = [ 161 | 'text1' => 'value1', 162 | 'text2' => 'value2', 163 | 'te.x-t/3 dsasad sadadas' => 'value3', 164 | ]; 165 | 166 | $multilang->setTexts($texts); 167 | 168 | $this->assertEquals($texts, $multilang->getTexts()); 169 | } 170 | 171 | /** @test */ 172 | public function set_empty_locale() 173 | { 174 | $this->expectException(TypeError::class); 175 | $multilang = $this->getMultilang(); 176 | $multilang->setLocale(null); 177 | } 178 | 179 | /** @test */ 180 | public function get_string_without_key() 181 | { 182 | $this->expectException(TypeError::class); 183 | $multilang = $this->getMultilang(); 184 | $multilang->setLocale('ka'); 185 | 186 | $multilang->get(null); 187 | } 188 | 189 | /** 190 | * @test 191 | */ 192 | public function check_must_load_from_cache() 193 | { 194 | $multilang = $this->getMultilang('production'); 195 | $multilang->setLocale('ka'); 196 | 197 | $texts = [ 198 | 'text1' => 'value1', 199 | 'text2' => 'value2', 200 | ]; 201 | 202 | $this->app->cache->put($multilang->getRepository()->getCacheName('ka'), $texts, 1440); 203 | 204 | $this->assertTrue($multilang->getRepository()->existsInCache('ka')); 205 | 206 | $this->app->cache->forget($multilang->getRepository()->getCacheName('ka')); 207 | } 208 | 209 | /** 210 | * @test 211 | */ 212 | public function store_load_from_cache() 213 | { 214 | $multilang = $this->getMultilang('production'); 215 | $multilang->setLocale('ka'); 216 | $multilang->loadTexts(); 217 | 218 | $this->assertTrue($multilang->getRepository()->existsInCache('ka', 'global')); 219 | 220 | $texts = []; 221 | for ($i = 0; $i <= 10; $i++) { 222 | $texts['text key ' . $i] = 'text value ' . $i; 223 | } 224 | 225 | $this->assertEquals($texts, $multilang->getRepository()->loadFromCache('ka', 'global')); 226 | } 227 | 228 | /** 229 | * @test 230 | */ 231 | public function check_autosave_allowed() 232 | { 233 | $multilang = $this->getMultilang('local'); 234 | $multilang->setLocale('ka'); 235 | 236 | $this->assertTrue($multilang->autoSaveIsAllowed()); 237 | 238 | $multilang = $this->getMultilang('production'); 239 | $multilang->setLocale('ka'); 240 | 241 | $this->assertFalse($multilang->autoSaveIsAllowed()); 242 | } 243 | 244 | /** 245 | * @test 246 | */ 247 | public function check_autosave() 248 | { 249 | $multilang = $this->getMultilang('local'); 250 | $multilang->setLocale('en'); 251 | 252 | $this->assertFalse($multilang->saveTexts()); 253 | 254 | $strings = [ 255 | 'aaaaa1' => 'aaaaa1', 256 | 'aaaaa2' => 'aaaaa2', 257 | 'aaaaa3' => 'aaaaa3', 258 | ]; 259 | foreach ($strings as $string) { 260 | $multilang->get($string); 261 | } 262 | 263 | $this->assertTrue($multilang->saveTexts()); 264 | 265 | $multilang = $this->getMultilang('local'); 266 | $multilang->setLocale('en'); 267 | $multilang->loadTexts(); 268 | 269 | $this->assertEquals($strings, $multilang->getTexts()); 270 | } 271 | 272 | /** 273 | * @test 274 | */ 275 | public function check_autosave_for_all_langs() 276 | { 277 | $config = [ 278 | 'locales' => [ 279 | 'en' => [ 280 | 'name' => 'English', 281 | 'native_name' => 'English', 282 | 'default' => true, 283 | ], 284 | 'ka' => [ 285 | 'name' => 'Georgian', 286 | 'native_name' => 'ქართული', 287 | 'default' => false, 288 | ], 289 | ], 290 | ]; 291 | 292 | $multilang = $this->getMultilang('local', $config); 293 | $multilang->setLocale('en'); 294 | 295 | $this->assertFalse($multilang->saveTexts()); 296 | 297 | $strings = [ 298 | 'keyyy1', 299 | 'keyyy2', 300 | 'keyyy3', 301 | ]; 302 | foreach ($strings as $string) { 303 | $multilang->get($string); 304 | } 305 | 306 | $this->assertTrue($multilang->saveTexts()); 307 | 308 | $multilang = $this->getMultilang('local'); 309 | $multilang->setLocale('ka'); 310 | $multilang->loadTexts(); 311 | 312 | $this->assertEquals('ka', $multilang->getLocale('ka')); 313 | 314 | $texts = $multilang->getTexts(); 315 | 316 | foreach ($strings as $string) { 317 | $this->assertArrayHasKey($string, $texts); 318 | } 319 | } 320 | 321 | /** 322 | * @test 323 | */ 324 | public function check_autosave_if_exists() 325 | { 326 | $multilang = $this->getMultilang('local'); 327 | $multilang->setLocale('en'); 328 | 329 | $this->assertFalse($multilang->saveTexts()); 330 | 331 | $strings = [ 332 | 'aaaaa1' => 'aaaaa1', 333 | 'aaaaa2' => 'aaaaa2', 334 | 'aaaaa3' => 'aaaaa3', 335 | ]; 336 | foreach ($strings as $string) { 337 | $multilang->get($string); 338 | } 339 | 340 | $this->assertTrue($multilang->saveTexts()); 341 | 342 | $this->assertTrue($multilang->saveTexts()); 343 | } 344 | 345 | /** 346 | * @test 347 | */ 348 | public function get_locales() 349 | { 350 | $config = [ 351 | 'locales' => [ 352 | 'en' => [ 353 | 'name' => 'English', 354 | ], 355 | 'ka' => [ 356 | 'name' => 'Georgian', 357 | ], 358 | 'az' => [ 359 | 'name' => 'Azerbaijanian', 360 | ], 361 | ], 362 | ]; 363 | 364 | $multilang = $this->getMultilang('local', $config); 365 | $multilang->setLocale('en'); 366 | 367 | $this->assertEquals(3, count($multilang->getLocales())); 368 | } 369 | 370 | /** 371 | * @test 372 | */ 373 | public function should_replace_markers() 374 | { 375 | $multilang = $this->getMultilang('local'); 376 | $multilang->setLocale('en'); 377 | 378 | $texts = [ 379 | 'text1' => 'The :attribute must be a date after :date.', 380 | ]; 381 | 382 | $multilang->setTexts($texts); 383 | 384 | $this->assertEquals( 385 | $multilang->get('text1', ['attribute' => 'Start Date', 'date' => '7 April 1986']), 386 | 'The Start Date must be a date after 7 April 1986.', 387 | ); 388 | } 389 | 390 | /** 391 | * @TODO 392 | */ 393 | public function check_redirect_url() 394 | { 395 | $multilang = $this->getMultilang('local'); 396 | $multilang->setLocale('en'); 397 | 398 | $this->assertFalse($multilang->saveTexts()); 399 | 400 | $strings = [ 401 | 'aaaaa1' => 'aaaaa1', 402 | 'aaaaa2' => 'aaaaa2', 403 | 'aaaaa3' => 'aaaaa3', 404 | ]; 405 | foreach ($strings as $string) { 406 | $multilang->get($string); 407 | } 408 | 409 | $this->assertTrue($multilang->saveTexts()); 410 | 411 | $this->assertTrue($multilang->saveTexts()); 412 | } 413 | 414 | /** @test */ 415 | public function scope_setter_and_getter() 416 | { 417 | $instance = $this->getMultilang(); 418 | $instance->setScope('test-scope'); 419 | 420 | $this->assertInstanceOf(MultiLang::class, $instance); 421 | $this->assertEquals('test-scope', $instance->getScope()); 422 | } 423 | 424 | /** @test */ 425 | public function load_texts_from_cache_repository() 426 | { 427 | $multilang = $this->getMultilang('production'); 428 | $multilang->setLocale('ka'); 429 | $texts = $multilang->getRepository()->loadFromDatabase('ka', 'global'); 430 | $multilang->getRepository()->storeInCache('ka', $texts, 'global'); 431 | 432 | $this->assertTrue($multilang->getRepository()->existsInCache('ka', 'global')); 433 | $cacheTexts = $multilang->loadTexts('ka', 'global'); 434 | $this->assertCount(count($texts), $cacheTexts); 435 | } 436 | 437 | /** @test */ 438 | public function detect_locale_should_return_default_locale_if_non_set() 439 | { 440 | $multilang = $this->getMultilang(); 441 | $fallback = $multilang->detectLocale(new Request()); 442 | 443 | $this->assertEquals('en', $fallback); 444 | } 445 | 446 | /** @test */ 447 | public function get_all_texts_should_return_all_from_database() 448 | { 449 | $multilang = $this->getMultilang(); 450 | $this->assertCount(11, $multilang->getAllTexts('ka', 'global')['ka']); 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /tests/Unit/RepositoryTest.php: -------------------------------------------------------------------------------- 1 | createTable(); 18 | } 19 | 20 | /** 21 | * @test 22 | */ 23 | public function check_set_get_cache_name() 24 | { 25 | $repository = $this->getRepository(); 26 | 27 | $this->assertEquals('texts_ka', $repository->getCacheName('ka')); 28 | } 29 | 30 | /** 31 | * @test 32 | */ 33 | public function check_set_get_cache_name_with_scope() 34 | { 35 | $repository = $this->getRepository(); 36 | 37 | $this->assertEquals('texts_ka_scope_name', $repository->getCacheName('ka', 'scope_name')); 38 | } 39 | 40 | /** 41 | * @test 42 | */ 43 | public function check_set_get_table_name() 44 | { 45 | $repository = $this->getRepository(['db' => ['texts_table' => 'mytable']]); 46 | 47 | $this->assertEquals('mytable', $repository->getTableName()); 48 | } 49 | 50 | /** 51 | * @test 52 | */ 53 | public function save_and_load_from_database() 54 | { 55 | $config = [ 56 | 'locales' => [ 57 | 'en' => [ 58 | 'name' => 'English', 59 | ], 60 | 'az' => [ 61 | 'name' => 'Azerbaijanian', 62 | ], 63 | ], 64 | ]; 65 | $repository = $this->getRepository($config); 66 | 67 | $texts = [ 68 | 'text1' => 'value1', 69 | 'text2' => 'value2', 70 | 'text3' => 'value3', 71 | ]; 72 | 73 | $textsScoped = [ 74 | 'text1' => 'value1 scoped', 75 | 'text2' => 'value2 scoped', 76 | 'text3' => 'value3 scoped', 77 | ]; 78 | 79 | $repository->save($texts); 80 | $repository->save($textsScoped, 'site'); 81 | 82 | $this->assertFalse($repository->save([])); 83 | $this->assertEquals($texts, $repository->loadFromDatabase('en')); 84 | $this->assertEquals($texts, $repository->loadFromDatabase('az')); 85 | $this->assertEquals($textsScoped, $repository->loadFromDatabase('en', 'site')); 86 | $this->assertEquals($textsScoped, $repository->loadFromDatabase('az', 'site')); 87 | } 88 | 89 | /** 90 | * @test 91 | */ 92 | public function save_and_load_from_cache() 93 | { 94 | $config = [ 95 | 'locales' => [ 96 | 'en' => [ 97 | 'name' => 'English', 98 | ], 99 | 'az' => [ 100 | 'name' => 'Azerbaijanian', 101 | ], 102 | ], 103 | ]; 104 | $repository = $this->getRepository($config); 105 | 106 | $texts = [ 107 | 'text1' => 'value1', 108 | 'text2' => 'value2', 109 | 'text3' => 'value3', 110 | ]; 111 | 112 | $repository->storeInCache('en', $texts); 113 | $repository->storeInCache('az', $texts); 114 | 115 | $this->assertTrue($repository->existsInCache('en')); 116 | $this->assertTrue($repository->existsInCache('az')); 117 | 118 | $this->assertEquals($texts, $repository->loadFromCache('en')); 119 | $this->assertEquals($texts, $repository->loadFromCache('az')); 120 | } 121 | } 122 | --------------------------------------------------------------------------------