├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── run-tests.yml ├── .gitignore ├── .scrutinizer.yml ├── ADDITIONS.md ├── CACHING.md ├── CHANGELOG.md ├── LICENSE ├── README.md ├── UPGRADING.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Mcamara │ └── LaravelLocalization │ │ ├── Commands │ │ ├── RouteTranslationsCacheCommand.php │ │ ├── RouteTranslationsClearCommand.php │ │ └── RouteTranslationsListCommand.php │ │ ├── Exceptions │ │ ├── SupportedLocalesNotDefined.php │ │ └── UnsupportedLocaleException.php │ │ ├── Facades │ │ └── LaravelLocalization.php │ │ ├── Interfaces │ │ └── LocalizedUrlRoutable.php │ │ ├── LanguageNegotiator.php │ │ ├── LaravelLocalization.php │ │ ├── LaravelLocalizationServiceProvider.php │ │ ├── Middleware │ │ ├── LaravelLocalizationMiddlewareBase.php │ │ ├── LaravelLocalizationRedirectFilter.php │ │ ├── LaravelLocalizationRoutes.php │ │ ├── LaravelLocalizationViewPath.php │ │ ├── LocaleCookieRedirect.php │ │ └── LocaleSessionRedirect.php │ │ └── Traits │ │ ├── LoadsTranslatedCachedRoutes.php │ │ └── TranslatedRouteCommandContext.php ├── config │ └── config.php └── stubs │ └── routes.stub └── tests ├── CustomTranslatorTest.php ├── LaravelLocalizationTest.php ├── ModelWithTranslatableRoutes.php ├── TestCase.php ├── full-config └── config.php └── lang ├── en └── routes.php └── es └── routes.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [mcamara, iwasherefirst2] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **More info:** 24 | - Version of Laravel 25 | - Version of the Laravel-localization package 26 | - Which middleware is used in `Route::groups` 27 | - Copy of the config file ( or at least setting of `supportedLocales`, `useAcceptLanguageHeader` and `hideDefaultLocaleInURL`). 28 | - Minimal steps to reproduce on a clean Laravel installation. 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: true 14 | matrix: 15 | os: [ubuntu-latest] 16 | php: [8.4, 8.3, 8.2] 17 | laravel: [12.*, 11.*, 10.*] 18 | stability: [prefer-lowest, prefer-stable] 19 | include: 20 | - laravel: 12.* 21 | testbench: 10.* 22 | - laravel: 11.* 23 | testbench: 9.* 24 | - laravel: 10.* 25 | testbench: 8.* 26 | 27 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} 28 | 29 | steps: 30 | - name: Checkout code 31 | uses: actions/checkout@v4 32 | 33 | - name: Setup PHP 34 | uses: shivammathur/setup-php@v2 35 | with: 36 | php-version: ${{ matrix.php }} 37 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo 38 | coverage: none 39 | 40 | - name: Setup problem matchers 41 | run: | 42 | echo "::add-matcher::${{ runner.tool_cache }}/php.json" 43 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 44 | 45 | - name: Install dependencies 46 | run: | 47 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench-browser-kit:${{ matrix.testbench }}" --no-interaction --no-update 48 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction 49 | 50 | - name: Execute tests 51 | run: vendor/bin/phpunit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.buildpath 2 | /.project 3 | /.settings 4 | /vendor 5 | composer.lock 6 | composer.phar 7 | Thumbs.db 8 | phpunit.xml 9 | .phpunit.cache 10 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | checks: 2 | php: 3 | code_rating: true 4 | duplication: true 5 | filter: 6 | excluded_paths: 7 | - tests/* 8 | - src/config/* -------------------------------------------------------------------------------- /ADDITIONS.md: -------------------------------------------------------------------------------- 1 | # Additional information 2 | 3 | ## Installation 4 | 5 | ### For Laravel 5.4 and below: 6 | 7 | For older versions of the framework, follow the steps below: 8 | 9 | Register the service provider in `config/app.php` 10 | 11 | ```php 12 | 'providers' => [ 13 | // [...] 14 | Mcamara\LaravelLocalization\LaravelLocalizationServiceProvider::class, 15 | ], 16 | ``` 17 | 18 | You may also register the `LaravelLocalization` facade: 19 | 20 | ```php 21 | 'aliases' => [ 22 | // [...] 23 | 'LaravelLocalization' => Mcamara\LaravelLocalization\Facades\LaravelLocalization::class, 24 | ], 25 | ``` 26 | 27 | ## Config 28 | 29 | ### Service Providers 30 | 31 | Otherwise, you can use `ConfigServiceProviders` (check this file for more info). 32 | 33 | For example, editing the default config service provider that Laravel loads when it's installed. This file is placed in `app/providers/ConfigServicePovider.php` and would look like this: 34 | 35 | ```php 36 | [ 45 | 'ace' => array( 'name' => 'Achinese', 'script' => 'Latn', 'native' => 'Aceh' ), 46 | 'ca' => array( 'name' => 'Catalan', 'script' => 'Latn', 'native' => 'català' ), 47 | 'en' => array( 'name' => 'English', 'script' => 'Latn', 'native' => 'English' ), 48 | ], 49 | 50 | 'laravellocalization.useAcceptLanguageHeader' => true, 51 | 52 | 'laravellocalization.hideDefaultLocaleInURL' => true 53 | ]); 54 | } 55 | 56 | } 57 | ``` 58 | 59 | This config would add Catalan and Achinese as languages and override any other previous supported locales and all the other options in the package. 60 | 61 | You can create your own config providers and add them on your application config file (check the providers array in `config/app.php`). 62 | -------------------------------------------------------------------------------- /CACHING.md: -------------------------------------------------------------------------------- 1 | # Laravel Localization: Caching Routes 2 | 3 | If you want to cache the routes in all languages, you will need to use special Artisan commands. **Using `artisan route:cache`** will not work correctly! 4 | 5 | ## Setup 6 | 7 | For the route caching solution to work, it is required to make a minor adjustment to your application route provision. 8 | 9 | 10 | **before laravel 11** 11 | In your App's `RouteServiceProvider`, use the `LoadsTranslatedCachedRoutes` trait: 12 | 13 | ```php 14 | $this->loadCachedRoutes()); 33 | ... 34 | } 35 | ``` 36 | 37 | ## Usage 38 | 39 | To cache your routes, use: 40 | 41 | ``` bash 42 | php artisan route:trans:cache 43 | ``` 44 | 45 | ... instead of the normal `route:cache` command. 46 | 47 | To list the routes for a given locale, use 48 | 49 | ``` bash 50 | php artisan route:trans:list {locale} 51 | 52 | # for instance: 53 | php artisan route:trans:list en 54 | ``` 55 | 56 | To clear cached routes for all locales, use 57 | 58 | ``` bash 59 | php artisan route:trans:clear 60 | ``` 61 | 62 | ### Note 63 | 64 | Using `route:clear` will also effectively unset the cache (at the minor cost of leaving some clutter in your bootstrap/cache directory). 65 | 66 | 67 | ## History 68 | 69 | Caching routes, before version 1.3, was done using a separate package, 70 | [https://github.com/czim/laravel-localization-route-cache](https://github.com/czim/laravel-localization-route-cache). 71 | That separate package is no longer required, and should be removed when upgrading to 1.3 or newer. 72 | 73 | 74 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 2.0.0 2 | - Support [contextual binding](https://laravel.com/docs/container#contextual-binding) ([#879](https://github.com/mcamara/laravel-localization/pull/879)) 3 | 4 | For guidance on the upgrade process, please refer to the [UPGRADING.md](/UPGRADING.md) file. 5 | 6 | ### 1.4.0 7 | - Added compatibility with Laravel 6 8 | 9 | ### 1.3.20 10 | - Respect locales mapping ([#631](https://github.com/mcamara/laravel-localization/pull/631)) 11 | 12 | ### 1.3.11 13 | - Merged in solution for caching translated and localized routes (originally in separate package [czim/laravel-localization-route-cache](https://github.com/czim/laravel-localization-route-cache)) by [CZim](https://github.com/czim). 14 | If you used this package, be sure to remove it when upgrading to this version. 15 | - Added `'utf8suffix' => env('LARAVELLOCALIZATION_UTF8SUFFIX', '.UTF-8')` to config file. 16 | 17 | ### 1.2.3 18 | - Added `getLocalesOrder()` function to the package 19 | 20 | ### 1.2 21 | - Added compatibility with Laravel 5.4 22 | 23 | ### 1.1 24 | - Added compatibility with Laravel 5.2 25 | 26 | ### 1.0.12 27 | - Added regional for date localization 28 | 29 | ### 1.0.7 30 | - Added Session and Cookie Middleware 31 | - Deleted useSessionLocale and useCookieLocale from config file 32 | 33 | ### 1.0 34 | - Laravel 5 supported 35 | - Added Middleware 36 | - Removed deprecated functions 37 | 38 | ### 0.15.0 39 | - Added tests from scratch 40 | - Refactored multiple functions 41 | - getLocalizedURL now accepts attributes for the url (if needed) 42 | - $routeName is always a string, no need to be an array if it just have the translation key for the current url 43 | 44 | ### 0.14.0 45 | - Laravel 4.2 compatibility 46 | - Removed Laravel 4.0 compatibility 47 | 48 | ### 0.13.5 49 | - Fixes issue with grouped routes 50 | 51 | ### 0.13.4 52 | - Fixes issue localizing a url when segment starts with a locale 53 | 54 | ### 0.13.3 55 | - Allow no url to be passed in localizeURL 56 | 57 | ### 0.13.2 58 | - Fixes issue with double slashes in localized urls 59 | - Strip trailing slashes from all localized urls 60 | 61 | ### 0.13.1 62 | - Fixes URL localization issue when the base path is not / (a.k.a, Laravel is not installed in the web root). 63 | 64 | ### 0.13.0 65 | - Deprecated "getLanguageBar" 66 | 67 | ### 0.12.1 68 | - Throws exception if Larvel's default locale is not in the array of supported locales. 69 | 70 | ### 0.12.0 71 | - Changes 302 redirect in to 307 to prevent POST values from being consumed. 72 | - Added localizeURL function 73 | 74 | ### 0.11.0 75 | - Deprecated "getCurrentLanguageDirection", "getCurrentLanguageScript" 76 | - Added "getCurrentLocaleDirection", "getCurrentLocaleScript", "getCurrentLocaleName" 77 | 78 | ### 0.10.1 79 | - Fixes to maintain compatibility with older config and languagebar.blade.php templates 80 | - Fixed backward compatibility of getLanguageBar 81 | - getLocalizedURL now returns URL paths in the same format as parameter inputted; trailing and leading slashes or lack of are respected. 82 | - getLocalizedURL now compatible with querystrings 83 | - merged getNonLocalizedURL and getLocalizedURL 84 | - getNonLocalizedURL($url = null) is now a wrapper for getLocalizedURL(false, $url = null) 85 | 86 | ### 0.10 87 | - Standardizing function names and variables using locale 88 | - Deprecated getCleanRoute 89 | - Deprecated useBrowserLanguage 90 | - Changed config useBrowserLanguage to useAcceptLanguageHeader 91 | - Deprecated useSessionLanguage 92 | - Changed config useSessionLanguage to useSessionLocale 93 | - Deprecated useCookieLanguagee 94 | - Changed config useCookieLanguage to useCookieLocale 95 | 96 | ### 0.9 97 | - Fixes issue #47 98 | - Fixes issue where getCleanRoute would only clean out the currently set locale. 99 | - getLocalizedURL now throws an UnsupportedLocaleException if the requested locale is not in the list of supported locales. 100 | 101 | ### 0.8 102 | - Changed getLanguageBar to just return view. All other code has been moved to languagebar view. 103 | - Deprecated getPrintCurrentLanguage 104 | - Deprecated getLanguageBarClassName 105 | 106 | ### 0.7 107 | - Merged languagesAllowed & supportedLanguages 108 | - Added native for language names 109 | - Added new function getSupportedLocales 110 | - Deprecated getAllowedLanguages use getSupportedLocales instead 111 | - Deprecated getSupportedLanguages use getSupportedLocales instead 112 | 113 | ### 0.6 114 | - Added support for language script and direction 115 | 116 | ### 0.5 117 | - Added multi-language routes 118 | - Function `getCurrentLanguage` is not static 119 | 120 | ### 0.4 121 | - Added the ability to edit the language bar code 122 | 123 | ### 0.3 124 | - Added 'LaravelLocalizationRedirectFilter' filter 125 | 126 | ### 0.2 127 | - Added `getURLLanguage` method. 128 | - Added `getLanguageBar` method. 129 | - Added `getURLLanguage` method. 130 | - Added config file 131 | - Added `useBrowserLanguage` config value 132 | - Added README 133 | 134 | ### 0.1 135 | - Initial release. 136 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 mcamara 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Localization 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/mcamara/laravel-localization.svg?style=flat-square)](https://packagist.org/packages/mcamara/laravel-localization) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/mcamara/laravel-localization.svg?style=flat-square)](https://packagist.org/packages/mcamara/laravel-localization) 5 | ![GitHub Actions](https://github.com/mcamara/laravel-localization/actions/workflows/run-tests.yml/badge.svg) 6 | [![Open Source Helpers](https://www.codetriage.com/mcamara/laravel-localization/badges/users.svg)](https://www.codetriage.com/mcamara/laravel-localization) 7 | [![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com) 8 | 9 | Easy i18n localization for Laravel, an useful tool to combine with Laravel localization classes. 10 | 11 | The package offers the following: 12 | 13 | - Detect language from browser 14 | - Smart redirects (Save locale in session/cookie) 15 | - Smart routing (Define your routes only once, no matter how many languages you use) 16 | - Translatable Routes 17 | - Supports caching & testing 18 | - Option to hide default locale in url 19 | - Many snippets and helpers (like language selector) 20 | 21 | ## Table of Contents 22 | 23 | - Installation 24 | - Usage 25 | - Redirect Middleware 26 | - Helpers 27 | - Translated Routes 28 | - Caching routes 29 | - Testing 30 | - Common Issues 31 | - POST is not working 32 | - MethodNotAllowedHttpException 33 | - Validation message is always in default locale 34 | - Collaborators 35 | - Changelog 36 | - License 37 | 38 | ## Laravel compatibility 39 | 40 | Laravel | laravel-localization 41 | :-------------|:---------- 42 | 4.0.x | 0.13.x 43 | 4.1.x | 0.13.x 44 | 4.2.x | 0.15.x 45 | 5.0.x/5.1.x | 1.0.x 46 | 5.2.x-5.4.x (PHP 7 not required) | 1.2. 47 | 5.2.0-6.x (PHP version >= 7 required) | 1.4.x 48 | 6.x-10.x (PHP version >= 7 required) | 1.8.x 49 | 10.x-12.x (PHP version >= 8.2 required) | 2.0.x 50 | 51 | ## Installation 52 | 53 | Install the package via composer: `composer require mcamara/laravel-localization` 54 | 55 | For Laravel 5.4 and below it necessary to [register the service provider](/ADDITIONS.md#for-laravel-5.4-and-below). 56 | 57 | ### Config Files 58 | 59 | In order to edit the default configuration you may execute: 60 | 61 | ``` 62 | php artisan vendor:publish --provider="Mcamara\LaravelLocalization\LaravelLocalizationServiceProvider" 63 | ``` 64 | 65 | After that, `config/laravellocalization.php` will be created. 66 | 67 | The configuration options are: 68 | 69 | - **supportedLocales** Languages of your app (Default: English & Spanish). 70 | - **useAcceptLanguageHeader** If true, then automatically detect language from browser. 71 | - **hideDefaultLocaleInURL** If true, then do not show default locale in url. 72 | - **localesOrder** Sort languages in custom order. 73 | - **localesMapping** Rename url locales. 74 | - **utf8suffix** Allow changing utf8suffix for CentOS etc. 75 | - **urlsIgnored** Ignore specific urls. 76 | 77 | ### Register Middleware 78 | 79 | You may register the package middleware in the `app/Http/Kernel.php` file: 80 | 81 | ```php 82 | \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRoutes::class, 95 | 'localizationRedirect' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRedirectFilter::class, 96 | 'localeSessionRedirect' => \Mcamara\LaravelLocalization\Middleware\LocaleSessionRedirect::class, 97 | 'localeCookieRedirect' => \Mcamara\LaravelLocalization\Middleware\LocaleCookieRedirect::class, 98 | 'localeViewPath' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationViewPath::class 99 | ]; 100 | } 101 | ``` 102 | 103 | If you are using Laravel 11, you may register in `bootstrap/app.php` file in closure `withMiddleware`: 104 | 105 | ```php 106 | return Application::configure(basePath: dirname(__DIR__)) 107 | // Other application configurations 108 | ->withMiddleware(function (Middleware $middleware) { 109 | $middleware->alias([ 110 | /**** OTHER MIDDLEWARE ALIASES ****/ 111 | 'localize' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRoutes::class, 112 | 'localizationRedirect' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRedirectFilter::class, 113 | 'localeSessionRedirect' => \Mcamara\LaravelLocalization\Middleware\LocaleSessionRedirect::class, 114 | 'localeCookieRedirect' => \Mcamara\LaravelLocalization\Middleware\LocaleCookieRedirect::class, 115 | 'localeViewPath' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationViewPath::class, 116 | ]); 117 | }) 118 | ``` 119 | 120 | ## Usage 121 | 122 | Add the following to your routes file: 123 | 124 | ```php 125 | // routes/web.php 126 | 127 | Route::group(['prefix' => LaravelLocalization::setLocale()], function() 128 | { 129 | /** ADD ALL LOCALIZED ROUTES INSIDE THIS GROUP **/ 130 | Route::get('/', function() 131 | { 132 | return View::make('hello'); 133 | }); 134 | 135 | Route::get('test',function(){ 136 | return View::make('test'); 137 | }); 138 | }); 139 | 140 | /** OTHER PAGES THAT SHOULD NOT BE LOCALIZED **/ 141 | 142 | ``` 143 | 144 | Once this route group is added to the routes file, a user can access all locales added into `supportedLocales` (`en` and `es` by default). 145 | For example, the above route file creates the following addresses: 146 | 147 | ``` 148 | // Set application language to English 149 | http://url-to-laravel/en 150 | http://url-to-laravel/en/test 151 | 152 | // Set application language to Spanish 153 | http://url-to-laravel/es 154 | http://url-to-laravel/es/test 155 | 156 | // Set application language to English or Spanish (depending on browsers default locales) 157 | // if nothing found set to default locale 158 | http://url-to-laravel 159 | http://url-to-laravel/test 160 | ``` 161 | The package sets your application locale `App::getLocale()` according to your url. The locale may then be used for [Laravel's localization features](http://laravel.com/docs/localization). 162 | 163 | You may add middleware to your group like this: 164 | 165 | ```php 166 | Route::group( 167 | [ 168 | 'prefix' => LaravelLocalization::setLocale(), 169 | 'middleware' => [ 'localeSessionRedirect', 'localizationRedirect', 'localeViewPath' ] 170 | ], function(){ //... 171 | }); 172 | ``` 173 | 174 | ### Recommendations 175 | 176 | ***1.***: It is **strongly** recommended to use a [redirecting middleware](#redirect-middleware). 177 | Urls without locale should only be used to determine browser/default locale and to redirect to the [localized url](#localized-urls). 178 | Otherwise, when search engine robots crawl for example `http://url-to-laravel/test` they may get different language content for each visit. 179 | Also having multiple urls for the same content creates a SEO duplicate-content issue. 180 | 181 | ***2.***: It is **strongly** recommended to [localize your links](#localized-urls), even if you use a redirect middleware. 182 | Otherwise, you will cause at least one redirect each time a user clicks on a link. 183 | Also, any action url from a post form must be localized, to prevent that it gets redirected to a get request. 184 | 185 | 186 | ## Redirect Middleware 187 | 188 | The following redirection middleware depends on the settings of `hideDefaultLocaleInURL` 189 | and `useAcceptLanguageHeader` in `config/laravellocalization.php`: 190 | 191 | ### LocaleSessionRedirect 192 | 193 | Whenever a locale is present in the url, it will be stored in the session by this middleware. 194 | 195 | If there is no locale present in the url, then this middleware will check the following 196 | 197 | - If no locale is saved in session and `useAcceptLanguageHeader` is set to true, compute locale from browser and redirect to url with locale. 198 | - If a locale is saved in session redirect to url with locale, unless its the default locale and `hideDefaultLocaleInURL` is set to true. 199 | 200 | For example, if a user navigates to http://url-to-laravel/test and `en` is the current locale, it would redirect him automatically to http://url-to-laravel/en/test. 201 | 202 | ### LocaleCookieRedirect 203 | 204 | Similar to LocaleSessionRedirect, but it stores value in a cookie instead of a session. 205 | 206 | Whenever a locale is present in the url, it will be stored in the cookie by this middleware. 207 | 208 | In there is no locale present in the url, then this middleware will check the following 209 | 210 | - If no locale is saved in cookie and `useAcceptLanguageHeader` is set to true, compute locale from browser and redirect to url with locale. 211 | - If a locale is saved in cookie redirect to url with locale, unless its the default locale and `hideDefaultLocaleInURL` is set to true. 212 | 213 | For example, if a user navigates to http://url-to-laravel/test and `de` is the current locale, it would redirect him automatically to http://url-to-laravel/de/test. 214 | 215 | 216 | ### LaravelLocalizationRedirectFilter 217 | 218 | When the default locale is present in the url and `hideDefaultLocaleInURL` is set to true, then the middleware redirects to the url without locale. 219 | 220 | For example, if `es` is the default locale, then http://url-to-laravel/es/test would be redirected to http://url-to-laravel/test and the`App::getLocale()` would be 221 | set to `es`. 222 | 223 | 224 | ## Helpers 225 | 226 | This package comes with a bunch of helpers. 227 | 228 | ### Localized URLs 229 | Localized URLS taken into account [route model binding]([https://laravel.com/docs/master/routing#route-model-binding]) when generating the localized route, 230 | aswell as the `hideDefaultLocaleInURL` and [Translated Routes](#translated-routes) settings. 231 | 232 | 233 | #### Get localized URL 234 | 235 | ```php 236 | // If current locale is Spanish, it returns `/es/test` 237 | @lang('Follow this link') 238 | ``` 239 | 240 | #### Get localized URL for an specific locale 241 | Get current URL in specific locale: 242 | 243 | ```php 244 | // Returns current url with English locale. 245 | {{ LaravelLocalization::getLocalizedURL('en') }} 246 | ``` 247 | 248 | ### Get Clean routes 249 | 250 | Returns a URL clean of any localization. 251 | 252 | ```php 253 | // Returns /about 254 | {{ LaravelLocalization::getNonLocalizedURL('/es/about') }} 255 | ``` 256 | 257 | ### Get URL for an specific translation key 258 | 259 | Returns a route, [localized](#translated-routes) to the desired locale. If the translation key does not exist in the locale given, this function will return false. 260 | 261 | 262 | ```php 263 | // Returns /es/acerca 264 | {{ LaravelLocalization::getURLFromRouteNameTranslated('es', 'routes.about') }} 265 | ``` 266 | **Example of a localized link using routes with attributes** 267 | 268 | ```php 269 | // An array of attributes can be provided. 270 | // Returns /en/archive/ghosts, /fr/archive/fantômes, /pt/arquivo/fantasmas, etc. 271 | Ghost Stories 272 | ``` 273 | 274 | 275 | ### Get Supported Locales 276 | 277 | Return all supported locales and their properties as an array. 278 | 279 | ```php 280 | {{ LaravelLocalization::getSupportedLocales() }} 281 | ``` 282 | 283 | 284 | 285 | ### Get Supported Locales Custom Order 286 | 287 | Return all supported locales but in the order specified in the configuration file. You can use this function to print locales in the language selector. 288 | 289 | ```php 290 | {{ LaravelLocalization::getLocalesOrder() }} 291 | ``` 292 | 293 | ### Get Supported Locales Keys 294 | 295 | Return an array with all the keys for the supported locales. 296 | 297 | ```php 298 | {{ LaravelLocalization::getSupportedLanguagesKeys() }} 299 | ``` 300 | 301 | ### Get Current Locale 302 | 303 | Return the key of the current locale. 304 | 305 | ```php 306 | {{ LaravelLocalization::getCurrentLocale() }} 307 | ``` 308 | 309 | ### Get Current Locale Name 310 | Return current locale's name as string (English/Spanish/Arabic/ ..etc). 311 | 312 | ```php 313 | {{ LaravelLocalization::getCurrentLocaleName() }} 314 | ``` 315 | 316 | ### Get Current Locale Native Name 317 | Return current locale's native name as string (English/Español/عربى/ ..etc). 318 | 319 | ```php 320 | {{ LaravelLocalization::getCurrentLocaleNative() }} 321 | ``` 322 | 323 | ### Get Current Locale Regional Name 324 | Return current locale's regional name as string (en_GB/en_US/fr_FR/ ..etc). 325 | 326 | ```php 327 | {{ LaravelLocalization::getCurrentLocaleRegional() }} 328 | ``` 329 | 330 | ### Get Current Locale Direction 331 | 332 | Return current locale's direction as string (ltr/rtl). 333 | 334 | 335 | ```php 336 | {{ LaravelLocalization::getCurrentLocaleDirection() }} 337 | ``` 338 | 339 | 340 | 341 | ### Get Current Locale Script 342 | Return the [ISO 15924](http://www.unicode.org/iso15924) code for the current locale script as a string; "Latn", "Cyrl", "Arab", etc. 343 | 344 | ```php 345 | {{ LaravelLocalization::getCurrentLocaleScript() }} 346 | ``` 347 | 348 | ### Set view-base-path to current locale 349 | 350 | Register the middleware `LaravelLocalizationViewPath` to set current locale as view-base-path. 351 | 352 | Now you can wrap your views in language-based folders like the translation files. 353 | 354 | `resources/views/en/`, `resources/views/fr`, ... 355 | 356 | 357 | ### Map your own custom lang url segments 358 | 359 | As you can modify the supportedLocales even by renaming their keys, it is possible to use the string ```uk``` instead of ```en-GB``` to provide custom lang url segments. Of course, you need to prevent any collisions with already existing keys and should stick to the convention as long as possible. But if you are using such a custom key, you have to store your mapping to the ```localesMapping``` array. This ``` 360 | localesMapping``` is needed to enable the LanguageNegotiator to correctly assign the desired locales based on HTTP Accept Language Header. Here is a quick example how to map HTTP Accept Language Header 'en-GB' to url segment 'uk': 361 | 362 | ```php 363 | // config/laravellocalization.php 364 | 365 | 'localesMapping' => [ 366 | 'en-GB' => 'uk' 367 | ], 368 | ``` 369 | 370 | After that ```http://url-to-laravel/en-GB/a/b/c``` becomes ```http://url-to-laravel/uk/a/b/c```. 371 | 372 | ```php 373 | LaravelLocalization::getLocalizedURL('en-GB', 'a/b/c'); // http://url-to-laravel/uk/a/b/c 374 | LaravelLocalization::getLocalizedURL('uk', 'a/b/c'); // http://url-to-laravel/uk/a/b/c 375 | ``` 376 | 377 | ## Creating a language selector 378 | 379 | If you're supporting multiple locales in your project you will probably want to provide the users with a way to change language. Below is a simple example of blade template code you can use to create your own language selector. 380 | 381 | ```blade 382 | 391 | ``` 392 | Here default language will be forced in getLocalizedURL() to be present in the URL even `hideDefaultLocaleInURL = true`. 393 | 394 | Note that Route Model Binding is supported. 395 | 396 | ## Translated Routes 397 | 398 | You may translate your routes. For example, http://url/en/about and http://url/es/acerca (acerca is about in spanish) 399 | or http://url/en/article/important-article and http://url/es/articulo/important-article (article is articulo in spanish) would be redirected to the same controller/view as follows: 400 | 401 | It is necessary that at least the `localize` middleware in loaded in your `Route::group` middleware (See [installation instruction](#installation)). 402 | 403 | For each language, add a `routes.php` into `resources/lang/**/routes.php` folder. 404 | The file contains an array with all translatable routes. For example, like this: 405 | 406 | > Keep in mind: starting from [Laravel 9](https://laravel.com/docs/9.x/upgrade#the-lang-directory), the `resources/lang` folder is now located in the root project folder (`lang`). 407 | > If your project has `lang` folder in the root, you must add a `routes.php` into `lang/**/routes.php` folder. 408 | 409 | ```php 410 | "about", 414 | "article" => "article/{article}", 415 | ]; 416 | ``` 417 | ```php 418 | "acerca", 422 | "article" => "articulo/{article}", 423 | ]; 424 | ``` 425 | 426 | You may add the routes in `routes/web.php` like this: 427 | 428 | ```php 429 | Route::group(['prefix' => LaravelLocalization::setLocale(), 430 | 'middleware' => [ 'localize' ]], function () { 431 | 432 | Route::get(LaravelLocalization::transRoute('routes.about'), function () { 433 | return view('about'); 434 | }); 435 | 436 | Route::get(LaravelLocalization::transRoute('routes.article'), function (\App\Article $article) { 437 | return $article; 438 | }); 439 | 440 | //,... 441 | }); 442 | ``` 443 | 444 | Once files are saved, you can access http://url/en/about , http://url/es/acerca , http://url/en/article/important-article and http://url/es/articulo/important-article without any problem. 445 | 446 | ### Translatable route parameters 447 | 448 | Maybe you noticed in the previous example the English slug in the Spanish url: 449 | 450 | http://url/es/articulo/important-article 451 | 452 | It is possible to have translated slugs, for example like this: 453 | 454 | http://url/en/article/important-change 455 | http://url/es/articulo/cambio-importante 456 | 457 | However, in order to do this, each article must have many slugs (one for each locale). 458 | Its up to you how you want to implement this relation. The only requirement for translatable route parameters is, that the relevant model implements the interface `LocalizedUrlRoutable`. 459 | 460 | #### Implementing LocalizedUrlRoutable 461 | 462 | To implement `\Mcamara\LaravelLocalization\Interfaces\LocalizedUrlRoutable`, 463 | one has to create the function `getLocalizedRouteKey($locale)`, which must return for a given locale the translated slug. In the above example, inside the model article, `getLocalizedRouteKey('en')` should return `important-change` and `getLocalizedRouteKey('es')` should return `cambio-importante`. 464 | 465 | #### Route Model Binding 466 | 467 | To use [route-model-binding](https://laravel.com/docs/routing#route-model-binding), one should overwrite the function `resolveRouteBinding($slug)` 468 | in the model. The function should return the model that belongs to the translated slug `$slug`. 469 | For example: 470 | 471 | ```php 472 | public function resolveRouteBinding($slug) 473 | { 474 | return static::findByLocalizedSlug($slug)->first() ?? abort(404); 475 | } 476 | ``` 477 | 478 | #### Tutorial Video 479 | 480 | You may want to checkout this [video](https://youtu.be/B1AUqCdizgc) which demonstrates how one may set up translatable route parameters. 481 | 482 | ## Events 483 | 484 | You can capture the URL parameters during translation if you wish to translate them too. To do so, just create an event listener for the `routes.translation` event like so: 485 | 486 | ```php 487 | Event::listen('routes.translation', function($locale, $attributes) 488 | { 489 | // Do your magic 490 | 491 | return $attributes; 492 | }); 493 | ``` 494 | 495 | Be sure to pass the locale and the attributes as parameters to the closure. You may also use Event Subscribers, see: [http://laravel.com/docs/events#event-subscribers](http://laravel.com/docs/events#event-subscribers) 496 | 497 | ## Caching routes 498 | 499 | To cache your routes, use: 500 | 501 | ``` bash 502 | php artisan route:trans:cache 503 | ``` 504 | 505 | ... instead of the normal `route:cache` command. Using `artisan route:cache` will **not** work correctly! 506 | 507 | For the route caching solution to work, it is required to make a minor adjustment to your application route provision. 508 | 509 | **before laravel 11** 510 | 511 | In your App's `RouteServiceProvider`, use the `LoadsTranslatedCachedRoutes` trait: 512 | 513 | ```php 514 | $this->loadCachedRoutes()); 535 | ... 536 | } 537 | ``` 538 | 539 | 540 | 541 | For more details see [here](CACHING.md). 542 | 543 | ## Common Issues 544 | 545 | ### POST is not working 546 | 547 | This may happen if you do not localize your action route that is inside your `Routes::group`. 548 | This may cause a redirect, which then changes the post request into a get request. 549 | To prevent that, simply use the [localize helper](#get-localized-url). 550 | 551 | For example, if you use `Auth::routes()` and put them into your `Route::group` Then 552 | 553 | ``` 554 |
555 | 556 |
557 | ``` 558 | 559 | will not work. Instead, one has to use 560 | 561 | ```php 562 |
563 | 564 |
565 | ``` 566 | 567 | 568 | Another way to solve this is to put http method to config to 'laravellocalization.httpMethodsIgnored' 569 | to prevent of processing this type of requests 570 | 571 | ### MethodNotAllowedHttpException 572 | 573 | If you do not localize your post url and use a redirect middleware, 574 | then the post request gets redirected as a get request. 575 | If you have not defined such a get route, you will cause this exception. 576 | 577 | To localize your post url see the example in [POST is not working](#post-is-not-working). 578 | 579 | ### Validation message is only in default locale 580 | 581 | This also happens if you did not localize your post url. 582 | If you don't localize your post url, the default locale is set while validating, 583 | and when returning to `back()` it shows the validation message in default locale. 584 | 585 | To localize your post url see the example in [POST is not working](#post-is-not-working). 586 | 587 | ## Testing 588 | 589 | During the test setup, the called route is not yet known. This means no language can be set. 590 | When a request is made during a test, this results in a 404 - without the prefix set the localized route does not seem to exist. 591 | 592 | To fix this, you can use this function to manually set the language prefix: 593 | ```php 594 | // TestCase.php 595 | protected function refreshApplicationWithLocale($locale) 596 | { 597 | self::tearDown(); 598 | putenv(LaravelLocalization::ENV_ROUTE_KEY . '=' . $locale); 599 | self::setUp(); 600 | } 601 | 602 | protected function tearDown(): void 603 | { 604 | putenv(LaravelLocalization::ENV_ROUTE_KEY); 605 | parent::tearDown(); 606 | } 607 | 608 | // YourTest.php 609 | public function testBasicTest() 610 | { 611 | $this->refreshApplicationWithLocale('en'); 612 | // Testing code 613 | } 614 | ``` 615 | 616 | ## Collaborators 617 | - [Adam Nielsen (iwasherefirst2)](https://github.com/iwasherefirst2) 618 | 619 | Ask [mcamara](https://github.com/mcamara) if you want to be one of them! 620 | 621 | ## Changelog 622 | 623 | View changelog here -> [changelog](CHANGELOG.md) 624 | 625 | ## License 626 | 627 | Laravel Localization is an open-sourced laravel package licensed under the MIT license 628 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | ## From v1 to v2 2 | This package now uses [dependency injection](https://laravel.com/docs/container#introduction) to retrieve dependencies from the container. 3 | 4 | This modification is a breaking change, especially if you had made extensions to the `__construct` method within the `Mcamara\LaravelLocalization\LaravelLocalization` class. 5 | You may now use depdency injection in your own implementation and forward the dependencies to the parent constructor. 6 | ```php 7 | use Mcamara\LaravelLocalization\LaravelLocalization; 8 | use Illuminate\Contracts\Config\Repository as ConfigRepository; 9 | use Illuminate\Contracts\Foundation\Application; 10 | use Illuminate\Contracts\Routing\UrlGenerator; 11 | use Illuminate\Contracts\Translation\Translator; 12 | use Illuminate\Http\Request; 13 | use Illuminate\Routing\Router; 14 | 15 | class MyLaravelLocalization extends LaravelLocalization 16 | { 17 | public function __construct( 18 | mixed $myCustomVariable, 19 | Application $app, 20 | ConfigRepository $configRepository, 21 | Translator $translator, 22 | Router $router, 23 | Request $request, 24 | UrlGenerator $url 25 | ) { 26 | parent::__construct($app, $configRepository, $translator, $router, $request, $url); 27 | } 28 | } 29 | ``` 30 | 31 | If your previous approach involved overriding the `LaravelLocalization` singleton in the container and generating a new instance of your custom implementation, there's now a more straightforward method for binding. This will automatically inject the correct dependencies for you. 32 | ```diff 33 | use Mcamara\LaravelLocalization\LaravelLocalization; 34 | 35 | -$this->app->singleton(LaravelLocalization::class, function () { 36 | - return new MyLaravelLocalization(); 37 | -}); 38 | +$this->app->singleton(LaravelLocalization::class, MyLaravelLocalization::class); 39 | ``` 40 | 41 | For more information, please see the following PR [#879](https://github.com/mcamara/laravel-localization/pull/879/files) -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcamara/laravel-localization", 3 | "description": "Easy localization for Laravel", 4 | "keywords": [ 5 | "localization", 6 | "laravel", 7 | "php" 8 | ], 9 | "homepage": "https://github.com/mcamara/laravel-localization", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Marc Cámara", 14 | "email": "mcamara88@gmail.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.2", 20 | "laravel/framework": "^10.0|^11.0|^12.0" 21 | }, 22 | "require-dev": { 23 | "orchestra/testbench-browser-kit": "^8.5|^9.0|^10.0", 24 | "phpunit/phpunit": "^10.1|^11.0" 25 | }, 26 | "suggest": { 27 | "ext-intl": "*" 28 | }, 29 | "scripts": { 30 | "test": "vendor/bin/phpunit" 31 | }, 32 | "autoload": { 33 | "classmap": [], 34 | "psr-0": { 35 | "Mcamara\\LaravelLocalization": "src/" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Mcamara\\LaravelLocalization\\Tests\\": "tests" 41 | } 42 | }, 43 | "extra": { 44 | "laravel": { 45 | "providers": [ 46 | "Mcamara\\LaravelLocalization\\LaravelLocalizationServiceProvider" 47 | ], 48 | "aliases": { 49 | "LaravelLocalization": "Mcamara\\LaravelLocalization\\Facades\\LaravelLocalization" 50 | } 51 | } 52 | }, 53 | "minimum-stability": "dev", 54 | "prefer-stable": true 55 | } 56 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | tests 16 | 17 | 18 | 19 | 20 | ./src/Mcamara 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Mcamara/LaravelLocalization/Commands/RouteTranslationsCacheCommand.php: -------------------------------------------------------------------------------- 1 | call('route:trans:clear'); 31 | 32 | $this->cacheRoutesPerLocale(); 33 | 34 | $this->info('Routes cached successfully for all locales!'); 35 | } 36 | 37 | /** 38 | * Cache the routes separately for each locale. 39 | */ 40 | protected function cacheRoutesPerLocale() 41 | { 42 | // Store the default routes cache, 43 | // this way the Application will detect that routes are cached. 44 | $allLocales = $this->getSupportedLocales(); 45 | 46 | array_push($allLocales, null); 47 | 48 | foreach ($allLocales as $locale) { 49 | 50 | $routes = $this->getFreshApplicationRoutesForLocale($locale); 51 | 52 | if (count($routes) == 0) { 53 | $this->error("Your application doesn't have any routes."); 54 | return; 55 | } 56 | 57 | foreach ($routes as $route) { 58 | $route->prepareForSerialization(); 59 | } 60 | 61 | $this->files->put( 62 | $this->makeLocaleRoutesPath($locale), $this->buildRouteCacheFile($routes) 63 | ); 64 | } 65 | } 66 | 67 | /** 68 | * Boot a fresh copy of the application and get the routes for a given locale. 69 | * 70 | * @param string|null $locale 71 | * @return \Illuminate\Routing\RouteCollection 72 | */ 73 | protected function getFreshApplicationRoutesForLocale($locale = null) 74 | { 75 | if ($locale === null) { 76 | return $this->getFreshApplicationRoutes(); 77 | } 78 | 79 | 80 | $key = LaravelLocalization::ENV_ROUTE_KEY; 81 | 82 | putenv("{$key}={$locale}"); 83 | 84 | $routes = $this->getFreshApplicationRoutes(); 85 | 86 | putenv("{$key}="); 87 | 88 | return $routes; 89 | } 90 | 91 | /** 92 | * Build the route cache file. 93 | * 94 | * @param \Illuminate\Routing\RouteCollection $routes 95 | * @return string 96 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 97 | */ 98 | protected function buildRouteCacheFile(RouteCollection $routes) 99 | { 100 | $stub = $this->files->get( 101 | realpath( 102 | __DIR__ 103 | . DIRECTORY_SEPARATOR . '..' 104 | . DIRECTORY_SEPARATOR . '..' 105 | . DIRECTORY_SEPARATOR . '..' 106 | . DIRECTORY_SEPARATOR . 'stubs' 107 | . DIRECTORY_SEPARATOR . 'routes.stub' 108 | ) 109 | ); 110 | 111 | return str_replace( 112 | [ 113 | '{{routes}}', 114 | '{{translatedRoutes}}', 115 | ], 116 | [ 117 | base64_encode(serialize($routes)), 118 | $this->getLaravelLocalization()->getSerializedTranslatedRoutes(), 119 | ], 120 | $stub 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Mcamara/LaravelLocalization/Commands/RouteTranslationsClearCommand.php: -------------------------------------------------------------------------------- 1 | files = $files; 44 | } 45 | 46 | /** 47 | * Execute the console command. 48 | * 49 | * @return void 50 | */ 51 | public function handle() 52 | { 53 | foreach ($this->getSupportedLocales() as $locale) { 54 | 55 | $path = $this->makeLocaleRoutesPath($locale); 56 | 57 | if ($this->files->exists($path)) { 58 | $this->files->delete($path); 59 | } 60 | } 61 | 62 | $path = $this->laravel->getCachedRoutesPath(); 63 | 64 | if ($this->files->exists($path)) { 65 | $this->files->delete($path); 66 | } 67 | 68 | $this->info('Route caches for locales cleared!'); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Mcamara/LaravelLocalization/Commands/RouteTranslationsListCommand.php: -------------------------------------------------------------------------------- 1 | argument('locale'); 32 | 33 | if ( ! $this->isSupportedLocale($locale)) { 34 | $this->error("Unsupported locale: '{$locale}'."); 35 | return; 36 | } 37 | 38 | $this->loadFreshApplicationRoutes($locale); 39 | 40 | parent::handle(); 41 | } 42 | 43 | /** 44 | * Boot a fresh copy of the application and replace the router/routes. 45 | * 46 | * @param string $locale 47 | * @return void 48 | */ 49 | protected function loadFreshApplicationRoutes($locale) 50 | { 51 | $app = require $this->getBootstrapPath() . '/app.php'; 52 | 53 | $key = LaravelLocalization::ENV_ROUTE_KEY; 54 | 55 | putenv("{$key}={$locale}"); 56 | 57 | $app->make(Kernel::class)->bootstrap(); 58 | 59 | putenv("{$key}="); 60 | 61 | $this->router = $app['router']; 62 | } 63 | 64 | /** 65 | * Get the console command arguments. 66 | * 67 | * @return array 68 | */ 69 | protected function getArguments() 70 | { 71 | return [ 72 | ['locale', InputArgument::REQUIRED, 'The locale to list routes for.'], 73 | ]; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Mcamara/LaravelLocalization/Exceptions/SupportedLocalesNotDefined.php: -------------------------------------------------------------------------------- 1 | app = app(); 55 | 56 | $this->configRepository = $this->app['config']; 57 | 58 | $this->defaultLocale = $defaultLocale; 59 | 60 | if (class_exists('Locale')) { 61 | $this->use_intl = true; 62 | 63 | foreach ($supportedLanguages as $key => $supportedLanguage) { 64 | if ( ! isset($supportedLanguage['lang'])) { 65 | $supportedLanguage['lang'] = Locale::canonicalize($key); 66 | } else { 67 | $supportedLanguage['lang'] = Locale::canonicalize($supportedLanguage['lang']); 68 | } 69 | if (isset($supportedLanguage['regional'])) { 70 | $supportedLanguage['regional'] = Locale::canonicalize($supportedLanguage['regional']); 71 | } 72 | 73 | $this->supportedLanguages[$key] = $supportedLanguage; 74 | } 75 | } else { 76 | $this->supportedLanguages = $supportedLanguages; 77 | } 78 | 79 | $this->request = $request; 80 | } 81 | 82 | /** 83 | * Negotiates language with the user's browser through the Accept-Language 84 | * HTTP header or the user's host address. Language codes are generally in 85 | * the form "ll" for a language spoken in only one country, or "ll-CC" for a 86 | * language spoken in a particular country. For example, U.S. English is 87 | * "en-US", while British English is "en-UK". Portuguese as spoken in 88 | * Portugal is "pt-PT", while Brazilian Portuguese is "pt-BR". 89 | * 90 | * This function is based on negotiateLanguage from Pear HTTP2 91 | * http://pear.php.net/package/HTTP2/ 92 | * 93 | * Quality factors in the Accept-Language: header are supported, e.g.: 94 | * Accept-Language: en-UK;q=0.7, en-US;q=0.6, no, dk;q=0.8 95 | * 96 | * @return string The negotiated language result or app.locale. 97 | */ 98 | public function negotiateLanguage() 99 | { 100 | $matches = $this->getMatchesFromAcceptedLanguages(); 101 | foreach ($matches as $key => $q) { 102 | 103 | $key = ($this->configRepository->get('laravellocalization.localesMapping')[$key]) ?? $key; 104 | 105 | if (!empty($this->supportedLanguages[$key])) { 106 | return $key; 107 | } 108 | 109 | if ($this->use_intl) { 110 | $key = Locale::canonicalize($key); 111 | } 112 | 113 | // Search for acceptable locale by 'regional' => 'af_ZA' or 'lang' => 'af-ZA' match. 114 | foreach ( $this->supportedLanguages as $key_supported => $locale ) { 115 | if ( (isset($locale['regional']) && $locale['regional'] == $key) || (isset($locale['lang']) && $locale['lang'] == $key) ) { 116 | return $key_supported; 117 | } 118 | } 119 | } 120 | // If any (i.e. "*") is acceptable, return the first supported format 121 | if (isset($matches['*'])) { 122 | reset($this->supportedLanguages); 123 | 124 | return key($this->supportedLanguages); 125 | } 126 | 127 | if ($this->use_intl && !empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { 128 | $http_accept_language = Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']); 129 | 130 | if (!empty($this->supportedLanguages[$http_accept_language])) { 131 | return $http_accept_language; 132 | } 133 | } 134 | 135 | if ($this->request->server('REMOTE_HOST')) { 136 | $remote_host = explode('.', $this->request->server('REMOTE_HOST')); 137 | $lang = strtolower(end($remote_host)); 138 | 139 | if (!empty($this->supportedLanguages[$lang])) { 140 | return $lang; 141 | } 142 | } 143 | 144 | return $this->defaultLocale; 145 | } 146 | 147 | /** 148 | * Return all the accepted languages from the browser. 149 | * 150 | * @return array Matches from the header field Accept-Languages 151 | */ 152 | private function getMatchesFromAcceptedLanguages() 153 | { 154 | $matches = []; 155 | 156 | if ($acceptLanguages = $this->request->header('Accept-Language')) { 157 | $acceptLanguages = explode(',', $acceptLanguages); 158 | 159 | $generic_matches = []; 160 | foreach ($acceptLanguages as $option) { 161 | $option = array_map('trim', explode(';', $option)); 162 | $l = $option[0]; 163 | if (isset($option[1])) { 164 | $q = (float) str_replace('q=', '', $option[1]); 165 | } else { 166 | $q = null; 167 | // Assign default low weight for generic values 168 | if ($l == '*/*') { 169 | $q = 0.01; 170 | } elseif (substr($l, -1) == '*') { 171 | $q = 0.02; 172 | } 173 | } 174 | // Unweighted values, get high weight by their position in the 175 | // list 176 | $q = $q ?? 1000 - \count($matches); 177 | $matches[$l] = $q; 178 | 179 | //If for some reason the Accept-Language header only sends language with country 180 | //we should make the language without country an accepted option, with a value 181 | //less than it's parent. 182 | $l_ops = explode('-', $l); 183 | array_pop($l_ops); 184 | while (!empty($l_ops)) { 185 | //The new generic option needs to be slightly less important than it's base 186 | $q -= 0.001; 187 | $op = implode('-', $l_ops); 188 | if (empty($generic_matches[$op]) || $generic_matches[$op] > $q) { 189 | $generic_matches[$op] = $q; 190 | } 191 | array_pop($l_ops); 192 | } 193 | } 194 | $matches = array_merge($generic_matches, $matches); 195 | 196 | arsort($matches, SORT_NUMERIC); 197 | } 198 | 199 | return $matches; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Mcamara/LaravelLocalization/LaravelLocalization.php: -------------------------------------------------------------------------------- 1 | app = $app; 136 | $this->configRepository = $configRepository; 137 | $this->translator = $translator; 138 | $this->router = $router; 139 | $this->request = $request; 140 | $this->url = $url; 141 | 142 | // set default locale 143 | $this->defaultLocale = $this->configRepository->get('app.locale'); 144 | $supportedLocales = $this->getSupportedLocales(); 145 | 146 | if (empty($supportedLocales[$this->defaultLocale])) { 147 | throw new UnsupportedLocaleException('Laravel default locale is not in the supportedLocales array.'); 148 | } 149 | } 150 | 151 | /** 152 | * Set and return current locale. 153 | * 154 | * @param string $locale Locale to set the App to (optional) 155 | * 156 | * @return string Returns locale (if route has any) or null (if route does not have a locale) 157 | */ 158 | public function setLocale($locale = null) 159 | { 160 | if (empty($locale) || !\is_string($locale)) { 161 | // If the locale has not been passed through the function 162 | // it tries to get it from the first segment of the url 163 | $locale = $this->request->segment(1); 164 | 165 | // If the locale is determined by env, use that 166 | // Note that this is how per-locale route caching is performed. 167 | if ( ! $locale) { 168 | $locale = $this->getForcedLocale(); 169 | } 170 | } 171 | 172 | $locale = $this->getInversedLocaleFromMapping($locale); 173 | 174 | if (!empty($this->supportedLocales[$locale])) { 175 | $this->currentLocale = $locale; 176 | } else { 177 | // if the first segment/locale passed is not valid 178 | // the system would ask which locale have to take 179 | // it could be taken by the browser 180 | // depending on your configuration 181 | 182 | $locale = null; 183 | 184 | // if we reached this point and hideDefaultLocaleInURL is true 185 | // we have to assume we are routing to a defaultLocale route. 186 | if ($this->hideDefaultLocaleInURL()) { 187 | $this->currentLocale = $this->defaultLocale; 188 | } 189 | // but if hideDefaultLocaleInURL is false, we have 190 | // to retrieve it from the browser... 191 | else { 192 | $this->currentLocale = $this->getCurrentLocale(); 193 | } 194 | } 195 | 196 | $this->app->setLocale($this->currentLocale); 197 | $this->translator->setLocale($this->currentLocale); 198 | 199 | // Regional locale such as de_DE, so formatLocalized works in Carbon 200 | $regional = $this->getCurrentLocaleRegional(); 201 | $suffix = $this->configRepository->get('laravellocalization.utf8suffix'); 202 | if ($regional) { 203 | setlocale(LC_TIME, $regional . $suffix); 204 | setlocale(LC_MONETARY, $regional . $suffix); 205 | } 206 | 207 | return $this->getLocaleFromMapping($locale); 208 | } 209 | 210 | /** 211 | * Check if $locale is default locale and supposed to be hidden in url 212 | * 213 | * @param string $locale Locale to be checked 214 | * 215 | * @return boolean Returns true if above requirement are met, otherwise false 216 | */ 217 | 218 | public function isHiddenDefault($locale) 219 | { 220 | return ($this->getDefaultLocale() === $locale && $this->hideDefaultLocaleInURL()); 221 | } 222 | 223 | /** 224 | * Set and return supported locales. 225 | * 226 | * @param array $locales Locales that the App supports 227 | */ 228 | public function setSupportedLocales($locales) 229 | { 230 | $this->supportedLocales = $locales; 231 | } 232 | 233 | /** 234 | * Returns an URL adapted to $locale or current locale. 235 | * 236 | * @param string $url URL to adapt. If not passed, the current url would be taken. 237 | * @param string|bool $locale Locale to adapt, false to remove locale 238 | * 239 | * @throws UnsupportedLocaleException 240 | * 241 | * @return string URL translated 242 | */ 243 | public function localizeURL($url = null, $locale = null) 244 | { 245 | return $this->getLocalizedURL($locale, $url); 246 | } 247 | 248 | /** 249 | * Returns an URL adapted to $locale. 250 | * 251 | * 252 | * @param string|bool $locale Locale to adapt, false to remove locale 253 | * @param string|false $url URL to adapt in the current language. If not passed, the current url would be taken. 254 | * @param array $attributes Attributes to add to the route, if empty, the system would try to extract them from the url. 255 | * @param bool $forceDefaultLocation Force to show default location even hideDefaultLocaleInURL set as TRUE 256 | * 257 | * @throws SupportedLocalesNotDefined 258 | * @throws UnsupportedLocaleException 259 | * 260 | * @return string|false URL translated, False if url does not exist 261 | */ 262 | public function getLocalizedURL($locale = null, $url = null, $attributes = [], $forceDefaultLocation = false) 263 | { 264 | if ($locale === null) { 265 | $locale = $this->getCurrentLocale(); 266 | } 267 | 268 | if (!$this->checkLocaleInSupportedLocales($locale)) { 269 | throw new UnsupportedLocaleException('Locale \''.$locale.'\' is not in the list of supported locales.'); 270 | } 271 | 272 | if (empty($attributes)) { 273 | $attributes = $this->extractAttributes($url, $locale); 274 | } 275 | 276 | if (empty($url)) { 277 | $url = $this->request->fullUrl(); 278 | $urlQuery = parse_url($url, PHP_URL_QUERY); 279 | $urlQuery = $urlQuery ? '?'.$urlQuery : ''; 280 | 281 | if (!empty($this->routeName)) { 282 | return $this->getURLFromRouteNameTranslated($locale, $this->routeName, $attributes, $forceDefaultLocation) . $urlQuery; 283 | } 284 | } else { 285 | $urlQuery = parse_url($url, PHP_URL_QUERY); 286 | $urlQuery = $urlQuery ? '?'.$urlQuery : ''; 287 | 288 | $url = $this->url->to($url); 289 | } 290 | 291 | $url = preg_replace('/'. preg_quote($urlQuery, '/') . '$/', '', $url); 292 | 293 | if ($locale && $translatedRoute = $this->findTranslatedRouteByUrl($url, $attributes, $this->currentLocale)) { 294 | return $this->getURLFromRouteNameTranslated($locale, $translatedRoute, $attributes, $forceDefaultLocation).$urlQuery; 295 | } 296 | 297 | $base_path = $this->request->getBaseUrl(); 298 | $parsed_url = parse_url($url); 299 | $url_locale = $this->getDefaultLocale(); 300 | 301 | if (!$parsed_url || empty($parsed_url['path'])) { 302 | $path = $parsed_url['path'] = ''; 303 | } else { 304 | $parsed_url['path'] = str_replace($base_path, '', '/'.ltrim($parsed_url['path'], '/')); 305 | $path = $parsed_url['path']; 306 | foreach ($this->getSupportedLocales() as $localeCode => $lang) { 307 | $localeCode = $this->getLocaleFromMapping($localeCode); 308 | 309 | $parsed_url['path'] = preg_replace('%^/?'.$localeCode.'/%', '$1', $parsed_url['path']); 310 | if ($parsed_url['path'] !== $path) { 311 | $url_locale = $localeCode; 312 | break; 313 | } 314 | 315 | $parsed_url['path'] = preg_replace('%^/?'.$localeCode.'$%', '$1', $parsed_url['path']); 316 | if ($parsed_url['path'] !== $path) { 317 | $url_locale = $localeCode; 318 | break; 319 | } 320 | } 321 | } 322 | 323 | $parsed_url['path'] = ltrim($parsed_url['path'], '/'); 324 | 325 | if ($translatedRoute = $this->findTranslatedRouteByPath($parsed_url['path'], $url_locale)) { 326 | return $this->getURLFromRouteNameTranslated($locale, $translatedRoute, $attributes, $forceDefaultLocation).$urlQuery; 327 | } 328 | 329 | $locale = $this->getLocaleFromMapping($locale); 330 | 331 | if (!empty($locale)) { 332 | if ($forceDefaultLocation || $locale != $this->getDefaultLocale() || !$this->hideDefaultLocaleInURL()) { 333 | $parsed_url['path'] = $locale.'/'.ltrim($parsed_url['path'], '/'); 334 | } 335 | } 336 | $parsed_url['path'] = ltrim(ltrim($base_path, '/').'/'.$parsed_url['path'], '/'); 337 | 338 | //Make sure that the pass path is returned with a leading slash only if it come in with one. 339 | if (Str::startsWith($path, '/') === true) { 340 | $parsed_url['path'] = '/'.$parsed_url['path']; 341 | } 342 | $parsed_url['path'] = rtrim($parsed_url['path'], '/'); 343 | 344 | $url = $this->unparseUrl($parsed_url); 345 | 346 | if ($this->checkUrl($url)) { 347 | return $url.$urlQuery; 348 | } 349 | 350 | return $this->createUrlFromUri($url).$urlQuery; 351 | } 352 | 353 | /** 354 | * Returns an URL adapted to the route name and the locale given. 355 | * 356 | * 357 | * @param string|bool $locale Locale to adapt 358 | * @param string $transKeyName Translation key name of the url to adapt 359 | * @param array $attributes Attributes for the route (only needed if transKeyName needs them) 360 | * @param bool $forceDefaultLocation Force to show default location even hideDefaultLocaleInURL set as TRUE 361 | * 362 | * @throws SupportedLocalesNotDefined 363 | * @throws UnsupportedLocaleException 364 | * 365 | * @return string|false URL translated 366 | */ 367 | public function getURLFromRouteNameTranslated($locale, $transKeyName, $attributes = [], $forceDefaultLocation = false) 368 | { 369 | if (!$this->checkLocaleInSupportedLocales($locale)) { 370 | throw new UnsupportedLocaleException('Locale \''.$locale.'\' is not in the list of supported locales.'); 371 | } 372 | 373 | if (!\is_string($locale)) { 374 | $locale = $this->getDefaultLocale(); 375 | } 376 | 377 | $route = ''; 378 | 379 | if ($forceDefaultLocation || !($locale === $this->defaultLocale && $this->hideDefaultLocaleInURL())) { 380 | $route = '/'.$locale; 381 | } 382 | if (\is_string($locale) && $this->translator->has($transKeyName, $locale)) { 383 | $translation = $this->translator->get($transKeyName, [], $locale); 384 | $route .= '/'.$translation; 385 | 386 | $route = $this->substituteAttributesInRoute($attributes, $route, $locale); 387 | } 388 | 389 | if (empty($route)) { 390 | // This locale does not have any key for this route name 391 | return false; 392 | } 393 | 394 | return rtrim($this->createUrlFromUri($route), '/'); 395 | } 396 | 397 | /** 398 | * It returns an URL without locale (if it has it) 399 | * Convenience function wrapping getLocalizedURL(false). 400 | * 401 | * @param string|false $url URL to clean, if false, current url would be taken 402 | * 403 | * @return string URL with no locale in path 404 | */ 405 | public function getNonLocalizedURL($url = null) 406 | { 407 | return $this->getLocalizedURL(false, $url); 408 | } 409 | 410 | /** 411 | * Returns default locale. 412 | * 413 | * @return string 414 | */ 415 | public function getDefaultLocale() 416 | { 417 | return $this->defaultLocale; 418 | } 419 | 420 | /** 421 | * Return locales mapping. 422 | * 423 | * @return array 424 | */ 425 | public function getLocalesMapping() 426 | { 427 | if (empty($this->localesMapping)) { 428 | $this->localesMapping = $this->configRepository->get('laravellocalization.localesMapping'); 429 | } 430 | 431 | return $this->localesMapping; 432 | } 433 | 434 | /** 435 | * Returns a locale from the mapping. 436 | * 437 | * @param string|null $locale 438 | * 439 | * @return string|null 440 | */ 441 | public function getLocaleFromMapping($locale) 442 | { 443 | return $this->getLocalesMapping()[$locale] ?? $locale; 444 | } 445 | 446 | /** 447 | * Returns inversed locale from the mapping. 448 | * 449 | * @param string|null $locale 450 | * 451 | * @return string|null 452 | */ 453 | public function getInversedLocaleFromMapping($locale) 454 | { 455 | return \array_flip($this->getLocalesMapping())[$locale] ?? $locale; 456 | } 457 | 458 | /** 459 | * Return an array of all supported Locales. 460 | * 461 | * @throws SupportedLocalesNotDefined 462 | * 463 | * @return array 464 | */ 465 | public function getSupportedLocales() 466 | { 467 | if (!empty($this->supportedLocales)) { 468 | return $this->supportedLocales; 469 | } 470 | 471 | $locales = $this->configRepository->get('laravellocalization.supportedLocales'); 472 | 473 | if (empty($locales) || !\is_array($locales)) { 474 | throw new SupportedLocalesNotDefined(); 475 | } 476 | 477 | $this->supportedLocales = $locales; 478 | 479 | return $locales; 480 | } 481 | 482 | /** 483 | * Return an array of all supported Locales but in the order the user 484 | * has specified in the config file. Useful for the language selector. 485 | * 486 | * @return array 487 | */ 488 | public function getLocalesOrder() 489 | { 490 | $locales = $this->getSupportedLocales(); 491 | 492 | $order = $this->configRepository->get('laravellocalization.localesOrder'); 493 | 494 | uksort($locales, function ($a, $b) use ($order) { 495 | $pos_a = array_search($a, $order); 496 | $pos_b = array_search($b, $order); 497 | return $pos_a - $pos_b; 498 | }); 499 | 500 | return $locales; 501 | } 502 | 503 | /** 504 | * Returns current locale name. 505 | * 506 | * @return string current locale name 507 | */ 508 | public function getCurrentLocaleName() 509 | { 510 | return $this->supportedLocales[$this->getCurrentLocale()]['name']; 511 | } 512 | 513 | /** 514 | * Returns current locale native name. 515 | * 516 | * @return string current locale native name 517 | */ 518 | public function getCurrentLocaleNative() 519 | { 520 | return $this->supportedLocales[$this->getCurrentLocale()]['native']; 521 | } 522 | 523 | /** 524 | * Returns current locale direction. 525 | * 526 | * @return string current locale direction 527 | */ 528 | public function getCurrentLocaleDirection() 529 | { 530 | if (!empty($this->supportedLocales[$this->getCurrentLocale()]['dir'])) { 531 | return $this->supportedLocales[$this->getCurrentLocale()]['dir']; 532 | } 533 | 534 | switch ($this->getCurrentLocaleScript()) { 535 | // Other (historic) RTL scripts exist, but this list contains the only ones in current use. 536 | case 'Arab': 537 | case 'Hebr': 538 | case 'Mong': 539 | case 'Tfng': 540 | case 'Thaa': 541 | return 'rtl'; 542 | default: 543 | return 'ltr'; 544 | } 545 | } 546 | 547 | /** 548 | * Returns current locale script. 549 | * 550 | * @return string current locale script 551 | */ 552 | public function getCurrentLocaleScript() 553 | { 554 | return $this->supportedLocales[$this->getCurrentLocale()]['script']; 555 | } 556 | 557 | /** 558 | * Returns current language's native reading. 559 | * 560 | * @return string current language's native reading 561 | */ 562 | public function getCurrentLocaleNativeReading() 563 | { 564 | return $this->supportedLocales[$this->getCurrentLocale()]['native']; 565 | } 566 | 567 | /** 568 | * Returns current language. 569 | * 570 | * @return string current language 571 | */ 572 | public function getCurrentLocale() 573 | { 574 | if ($this->currentLocale) { 575 | return $this->currentLocale; 576 | } 577 | 578 | if ($this->useAcceptLanguageHeader() && !$this->app->runningInConsole()) { 579 | $negotiator = new LanguageNegotiator($this->defaultLocale, $this->getSupportedLocales(), $this->request); 580 | 581 | return $negotiator->negotiateLanguage(); 582 | } 583 | 584 | // or get application default language 585 | return $this->configRepository->get('app.locale'); 586 | } 587 | 588 | /** 589 | * Returns current regional. 590 | * 591 | * @return string current regional 592 | */ 593 | public function getCurrentLocaleRegional() 594 | { 595 | // need to check if it exists, since 'regional' has been added 596 | // after version 1.0.11 and existing users will not have it 597 | if (isset($this->supportedLocales[$this->getCurrentLocale()]['regional'])) { 598 | return $this->supportedLocales[$this->getCurrentLocale()]['regional']; 599 | } else { 600 | return; 601 | } 602 | } 603 | 604 | /** 605 | * Returns supported languages language key. 606 | * 607 | * @return array keys of supported languages 608 | */ 609 | public function getSupportedLanguagesKeys() 610 | { 611 | return array_keys($this->supportedLocales); 612 | } 613 | 614 | /** 615 | * Check if Locale exists on the supported locales array. 616 | * 617 | * @param string|bool $locale string|bool Locale to be checked 618 | * 619 | * @throws SupportedLocalesNotDefined 620 | * 621 | * @return bool is the locale supported? 622 | */ 623 | public function checkLocaleInSupportedLocales($locale) 624 | { 625 | $inversedLocale = $this->getInversedLocaleFromMapping($locale); 626 | $locales = $this->getSupportedLocales(); 627 | if ($locale !== false && empty($locales[$locale]) && empty($locales[$inversedLocale])) { 628 | return false; 629 | } 630 | 631 | return true; 632 | } 633 | 634 | /** 635 | * Change route attributes for the ones in the $attributes array. 636 | * 637 | * @param $attributes array Array of attributes 638 | * @param string $route string route to substitute 639 | * 640 | * @return string route with attributes changed 641 | */ 642 | protected function substituteAttributesInRoute($attributes, $route, $locale = null) 643 | { 644 | foreach ($attributes as $key => $value) { 645 | if ($value instanceOf Interfaces\LocalizedUrlRoutable) { 646 | $value = $value->getLocalizedRouteKey($locale); 647 | } 648 | elseif ($value instanceOf UrlRoutable) { 649 | $value = $value->getRouteKey(); 650 | } 651 | $route = str_replace(array('{'.$key.'}', '{'.$key.'?}'), $value, $route); 652 | } 653 | 654 | // delete empty optional arguments that are not in the $attributes array 655 | $route = preg_replace('/\/{[^)]+\?}/', '', $route); 656 | 657 | return $route; 658 | } 659 | 660 | /** 661 | * Returns translated routes. 662 | * 663 | * @return array translated routes 664 | */ 665 | protected function getTranslatedRoutes() 666 | { 667 | return $this->translatedRoutes; 668 | } 669 | 670 | /** 671 | * Set current route name. 672 | * 673 | * @param string $routeName current route name 674 | */ 675 | public function setRouteName($routeName) 676 | { 677 | $this->routeName = $routeName; 678 | } 679 | 680 | /** 681 | * Translate routes and save them to the translated routes array (used in the localize route filter). 682 | * 683 | * @param string $routeName Key of the translated string 684 | * 685 | * @return string Translated string 686 | */ 687 | public function transRoute($routeName) 688 | { 689 | if (!\in_array($routeName, $this->translatedRoutes)) { 690 | $this->translatedRoutes[] = $routeName; 691 | } 692 | 693 | return $this->translator->get($routeName); 694 | } 695 | 696 | /** 697 | * Returns the translation key for a given path. 698 | * 699 | * @param string $path Path to get the key translated 700 | * 701 | * @return string|false Key for translation, false if not exist 702 | */ 703 | public function getRouteNameFromAPath($path) 704 | { 705 | $attributes = $this->extractAttributes($path); 706 | 707 | $path = parse_url($path)['path']; 708 | $path = trim(str_replace('/'.$this->currentLocale.'/', '', $path), "/"); 709 | 710 | foreach ($this->translatedRoutes as $route) { 711 | if (trim($this->substituteAttributesInRoute($attributes, $this->translator->get($route), $this->currentLocale), '/') === $path) { 712 | return $route; 713 | } 714 | } 715 | 716 | return false; 717 | } 718 | 719 | /** 720 | * Returns the translated route for the path and the url given. 721 | * 722 | * @param string $path Path to check if it is a translated route 723 | * @param string $url_locale Language to check if the path exists 724 | * 725 | * @return string|false Key for translation, false if not exist 726 | */ 727 | protected function findTranslatedRouteByPath($path, $url_locale) 728 | { 729 | // check if this url is a translated url 730 | foreach ($this->translatedRoutes as $translatedRoute) { 731 | if ($this->translator->get($translatedRoute, [], $url_locale) == rawurldecode($path)) { 732 | return $translatedRoute; 733 | } 734 | } 735 | 736 | return false; 737 | } 738 | 739 | /** 740 | * Returns the translated route for an url and the attributes given and a locale. 741 | * 742 | * 743 | * @param string|false|null $url Url to check if it is a translated route 744 | * @param array $attributes Attributes to check if the url exists in the translated routes array 745 | * @param string $locale Language to check if the url exists 746 | * 747 | * @throws SupportedLocalesNotDefined 748 | * @throws UnsupportedLocaleException 749 | * 750 | * @return string|false Key for translation, false if not exist 751 | */ 752 | protected function findTranslatedRouteByUrl($url, $attributes, $locale) 753 | { 754 | if (empty($url)) { 755 | return false; 756 | } 757 | 758 | if (isset($this->cachedTranslatedRoutesByUrl[$locale][$url])) { 759 | return $this->cachedTranslatedRoutesByUrl[$locale][$url]; 760 | } 761 | 762 | // check if this url is a translated url 763 | foreach ($this->translatedRoutes as $translatedRoute) { 764 | $routeName = $this->getURLFromRouteNameTranslated($locale, $translatedRoute, $attributes); 765 | 766 | // We can ignore extra url parts and compare only their url_path (ignore arguments that are not attributes) 767 | if (parse_url($this->getNonLocalizedURL($routeName), PHP_URL_PATH) == parse_url($this->getNonLocalizedURL(urldecode($url)), PHP_URL_PATH)) { 768 | $this->cachedTranslatedRoutesByUrl[$locale][$url] = $translatedRoute; 769 | 770 | return $translatedRoute; 771 | } 772 | } 773 | 774 | return false; 775 | } 776 | 777 | /** 778 | * Returns true if the string given is a valid url. 779 | * 780 | * @param string $url String to check if it is a valid url 781 | * 782 | * @return bool Is the string given a valid url? 783 | */ 784 | protected function checkUrl($url) 785 | { 786 | return filter_var($url, FILTER_VALIDATE_URL); 787 | } 788 | 789 | /** 790 | * Returns the config repository for this instance. 791 | * 792 | * @return \Illuminate\Contracts\Config\Repository Configuration repository 793 | */ 794 | public function getConfigRepository() 795 | { 796 | return $this->configRepository; 797 | } 798 | 799 | /** 800 | * Returns the translation key for a given path. 801 | * 802 | * @return bool Returns value of useAcceptLanguageHeader in config. 803 | */ 804 | protected function useAcceptLanguageHeader() 805 | { 806 | return $this->configRepository->get('laravellocalization.useAcceptLanguageHeader'); 807 | } 808 | 809 | public function hideUrlAndAcceptHeader() 810 | { 811 | return $this->hideDefaultLocaleInURL() && $this->useAcceptLanguageHeader(); 812 | } 813 | 814 | /** 815 | * Returns the translation key for a given path. 816 | * 817 | * @return bool Returns value of hideDefaultLocaleInURL in config. 818 | */ 819 | public function hideDefaultLocaleInURL() 820 | { 821 | return $this->configRepository->get('laravellocalization.hideDefaultLocaleInURL'); 822 | } 823 | 824 | /** 825 | * Create an url from the uri. 826 | * 827 | * @param string $uri Uri 828 | * 829 | * @return string Url for the given uri 830 | */ 831 | public function createUrlFromUri($uri) 832 | { 833 | $uri = ltrim($uri, '/'); 834 | 835 | if (empty($this->baseUrl)) { 836 | return app('url')->to($uri); 837 | } 838 | 839 | return $this->baseUrl.$uri; 840 | } 841 | 842 | /** 843 | * Sets the base url for the site. 844 | * 845 | * @param string $url Base url for the site 846 | */ 847 | public function setBaseUrl($url) 848 | { 849 | if (substr($url, -1) != '/') { 850 | $url .= '/'; 851 | } 852 | 853 | $this->baseUrl = $url; 854 | } 855 | 856 | /** 857 | * Returns serialized translated routes for caching purposes. 858 | * 859 | * @return string 860 | */ 861 | public function getSerializedTranslatedRoutes() 862 | { 863 | return base64_encode(serialize($this->translatedRoutes)); 864 | } 865 | 866 | /** 867 | * Sets the translated routes list. 868 | * Only useful from a cached routes context. 869 | * 870 | * @param string $serializedRoutes 871 | */ 872 | public function setSerializedTranslatedRoutes($serializedRoutes) 873 | { 874 | if ( ! $serializedRoutes) { 875 | return; 876 | } 877 | 878 | $this->translatedRoutes = unserialize(base64_decode($serializedRoutes)); 879 | } 880 | 881 | /** 882 | * Extract attributes for current url. 883 | * 884 | * @param bool|false|null|string $url to extract attributes, if not present, the system will look for attributes in the current call 885 | * @param string $locale 886 | * 887 | * @return array Array with attributes 888 | */ 889 | protected function extractAttributes($url = false, $locale = '') 890 | { 891 | if (!empty($url)) { 892 | $attributes = []; 893 | $parse = parse_url($url); 894 | if (isset($parse['path'])) { 895 | $parse['path'] = trim(str_replace('/'.$this->currentLocale.'/', '', $parse['path']), "/"); 896 | $url = explode('/', trim($parse['path'], '/')); 897 | } else { 898 | $url = []; 899 | } 900 | 901 | foreach ($this->router->getRoutes() as $route) { 902 | $attributes = []; 903 | $path = method_exists($route, 'uri') ? $route->uri() : $route->getUri(); 904 | 905 | if (!preg_match("/{[\w]+\??}/", $path)) { 906 | continue; 907 | } 908 | 909 | $path = explode('/', $path); 910 | $i = 0; 911 | 912 | // The system's route can't be smaller 913 | // only the $url can be missing segments (optional parameters) 914 | // We can assume it's the wrong route 915 | if (count($path) < count($url)) { 916 | continue; 917 | } 918 | 919 | $match = true; 920 | foreach ($path as $j => $segment) { 921 | if (isset($url[$i])) { 922 | if ($segment === $url[$i]) { 923 | $i++; 924 | continue; 925 | } elseif (preg_match("/{[\w]+}/", $segment)) { 926 | // must-have parameters 927 | $attribute_name = preg_replace(['/}/', '/{/', "/\?/"], '', $segment); 928 | $attributes[$attribute_name] = $url[$i]; 929 | $i++; 930 | continue; 931 | } elseif (preg_match("/{[\w]+\?}/", $segment)) { 932 | // optional parameters 933 | if (!isset($path[$j + 1]) || $path[$j + 1] !== $url[$i]) { 934 | // optional parameter taken 935 | $attribute_name = preg_replace(['/}/', '/{/', "/\?/"], '', $segment); 936 | $attributes[$attribute_name] = $url[$i]; 937 | $i++; 938 | continue; 939 | } else { 940 | $match = false; 941 | break; 942 | } 943 | } else { 944 | // As soon as one segment doesn't match, then we have the wrong route 945 | $match = false; 946 | break; 947 | } 948 | } elseif (preg_match("/{[\w]+\?}/", $segment)) { 949 | $attribute_name = preg_replace(['/}/', '/{/', "/\?/"], '', $segment); 950 | $attributes[$attribute_name] = null; 951 | $i++; 952 | } else { 953 | // no optional parameters but no more $url given 954 | // this route does not match the url 955 | $match = false; 956 | break; 957 | } 958 | } 959 | 960 | if (isset($url[$i + 1])) { 961 | $match = false; 962 | } 963 | 964 | if ($match) { 965 | return $attributes; 966 | } 967 | } 968 | } else { 969 | if (!$this->router->current()) { 970 | return []; 971 | } 972 | 973 | $attributes = $this->normalizeAttributes($this->router->current()->parameters()); 974 | $response = event('routes.translation', [$locale, $attributes]); 975 | 976 | if (!empty($response)) { 977 | $response = array_shift($response); 978 | } 979 | 980 | if (\is_array($response)) { 981 | $attributes = array_merge($attributes, $response); 982 | } 983 | } 984 | 985 | return $attributes; 986 | } 987 | 988 | /** 989 | * Build URL using array data from parse_url. 990 | * 991 | * @param array|false $parsed_url Array of data from parse_url function 992 | * 993 | * @return string Returns URL as string. 994 | */ 995 | protected function unparseUrl($parsed_url) 996 | { 997 | if (empty($parsed_url)) { 998 | return ''; 999 | } 1000 | 1001 | $url = ''; 1002 | $url .= isset($parsed_url['scheme']) ? $parsed_url['scheme'].'://' : ''; 1003 | $url .= $parsed_url['host'] ?? ''; 1004 | $url .= isset($parsed_url['port']) ? ':'.$parsed_url['port'] : ''; 1005 | $user = $parsed_url['user'] ?? ''; 1006 | $pass = isset($parsed_url['pass']) ? ':'.$parsed_url['pass'] : ''; 1007 | $url .= $user.(($user || $pass) ? "$pass@" : ''); 1008 | 1009 | if (!empty($url)) { 1010 | $url .= isset($parsed_url['path']) ? '/'.ltrim($parsed_url['path'], '/') : ''; 1011 | } else { 1012 | $url .= $parsed_url['path'] ?? ''; 1013 | } 1014 | 1015 | $url .= isset($parsed_url['query']) ? '?'.$parsed_url['query'] : ''; 1016 | $url .= isset($parsed_url['fragment']) ? '#'.$parsed_url['fragment'] : ''; 1017 | 1018 | return $url; 1019 | } 1020 | 1021 | /** 1022 | * Normalize attributes gotten from request parameters. 1023 | * 1024 | * @param array $attributes The attributes 1025 | * @return array The normalized attributes 1026 | */ 1027 | protected function normalizeAttributes($attributes) 1028 | { 1029 | if (array_key_exists('data', $attributes) && \is_array($attributes['data']) && ! \count($attributes['data'])) { 1030 | $attributes['data'] = null; 1031 | return $attributes; 1032 | } 1033 | return $attributes; 1034 | } 1035 | 1036 | /** 1037 | * Returns the forced environment set route locale. 1038 | * 1039 | * @return string|null 1040 | */ 1041 | protected function getForcedLocale() 1042 | { 1043 | if (version_compare($this->app->version(), '6') >= 0) { 1044 | return Env::get(static::ENV_ROUTE_KEY, function () { 1045 | $value = getenv(static::ENV_ROUTE_KEY); 1046 | 1047 | if ($value !== false) { 1048 | return $value; 1049 | } 1050 | }); 1051 | } else { 1052 | return env(static::ENV_ROUTE_KEY, function () { 1053 | $value = getenv(static::ENV_ROUTE_KEY); 1054 | 1055 | if ($value !== false) { 1056 | return $value; 1057 | } 1058 | }); 1059 | } 1060 | } 1061 | } 1062 | -------------------------------------------------------------------------------- /src/Mcamara/LaravelLocalization/LaravelLocalizationServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 17 | __DIR__.'/../../config/config.php' => config_path('laravellocalization.php'), 18 | ], 'config'); 19 | } 20 | 21 | /** 22 | * Get the services provided by the provider. 23 | * 24 | * @return array 25 | */ 26 | public function provides() 27 | { 28 | return ['modules.handler', 'modules']; 29 | } 30 | 31 | /** 32 | * Register the service provider. 33 | * 34 | * @return void 35 | */ 36 | public function register() 37 | { 38 | $packageConfigFile = __DIR__.'/../../config/config.php'; 39 | 40 | $this->mergeConfigFrom( 41 | $packageConfigFile, 'laravellocalization' 42 | ); 43 | 44 | $this->registerBindings(); 45 | 46 | $this->registerCommands(); 47 | } 48 | 49 | /** 50 | * Registers app bindings and aliases. 51 | */ 52 | protected function registerBindings() 53 | { 54 | $this->app->singleton(LaravelLocalization::class); 55 | 56 | $this->app->alias(LaravelLocalization::class, 'laravellocalization'); 57 | } 58 | 59 | /** 60 | * Registers route caching commands. 61 | */ 62 | protected function registerCommands() 63 | { 64 | $this->app->singleton('laravellocalizationroutecache.cache', Commands\RouteTranslationsCacheCommand::class); 65 | $this->app->singleton('laravellocalizationroutecache.clear', Commands\RouteTranslationsClearCommand::class); 66 | $this->app->singleton('laravellocalizationroutecache.list', Commands\RouteTranslationsListCommand::class); 67 | 68 | $this->commands([ 69 | 'laravellocalizationroutecache.cache', 70 | 'laravellocalizationroutecache.clear', 71 | 'laravellocalizationroutecache.list', 72 | ]); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Mcamara/LaravelLocalization/Middleware/LaravelLocalizationMiddlewareBase.php: -------------------------------------------------------------------------------- 1 | method(), config('laravellocalization.httpMethodsIgnored'))) { 24 | return true; 25 | } 26 | $this->except = $this->except ?? config('laravellocalization.urlsIgnored', []); 27 | foreach ($this->except as $except) { 28 | if ($except !== '/') { 29 | $except = trim($except, '/'); 30 | } 31 | 32 | if ($request->is($except)) { 33 | return true; 34 | } 35 | } 36 | 37 | return false; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Mcamara/LaravelLocalization/Middleware/LaravelLocalizationRedirectFilter.php: -------------------------------------------------------------------------------- 1 | shouldIgnore($request)) { 22 | return $next($request); 23 | } 24 | 25 | $params = explode('/', $request->getPathInfo()); 26 | 27 | // Dump the first element (empty string) as getPathInfo() always returns a leading slash 28 | array_shift($params); 29 | 30 | if (\count($params) > 0) { 31 | $locale = $params[0]; 32 | 33 | if (app('laravellocalization')->checkLocaleInSupportedLocales($locale)) { 34 | if (app('laravellocalization')->isHiddenDefault($locale)) { 35 | $redirection = app('laravellocalization')->getNonLocalizedURL(); 36 | 37 | // Save any flashed data for redirect 38 | app('session')->reflash(); 39 | 40 | return new RedirectResponse($redirection, 302, ['Vary' => 'Accept-Language']); 41 | } 42 | } 43 | } 44 | 45 | return $next($request); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Mcamara/LaravelLocalization/Middleware/LaravelLocalizationRoutes.php: -------------------------------------------------------------------------------- 1 | shouldIgnore($request)) { 21 | return $next($request); 22 | } 23 | 24 | $app = app(); 25 | 26 | $routeName = $app['laravellocalization']->getRouteNameFromAPath($request->getUri()); 27 | 28 | $app['laravellocalization']->setRouteName($routeName); 29 | 30 | return $next($request); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Mcamara/LaravelLocalization/Middleware/LaravelLocalizationViewPath.php: -------------------------------------------------------------------------------- 1 | shouldIgnore($request)) { 22 | return $next($request); 23 | } 24 | 25 | $app = app(); 26 | 27 | $currentLocale = app('laravellocalization')->getCurrentLocale(); 28 | $viewPath = resource_path('views/' . $currentLocale); 29 | 30 | // Add current locale-code to view-paths 31 | View::addLocation($viewPath); 32 | 33 | return $next($request); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/Mcamara/LaravelLocalization/Middleware/LocaleCookieRedirect.php: -------------------------------------------------------------------------------- 1 | shouldIgnore($request)) { 23 | return $next($request); 24 | } 25 | 26 | $params = explode('/', $request->path()); 27 | $locale = $request->cookie('locale', false); 28 | 29 | if (\count($params) > 0 && app('laravellocalization')->checkLocaleInSupportedLocales($params[0])) { 30 | return $next($request)->withCookie(cookie()->forever('locale', $params[0])); 31 | } 32 | 33 | if (empty($locale) && app('laravellocalization')->hideUrlAndAcceptHeader()){ 34 | // When default locale is hidden and accept language header is true, 35 | // then compute browser language when no session has been set. 36 | // Once the session has been set, there is no need 37 | // to negotiate language from browser again. 38 | $negotiator = new LanguageNegotiator( 39 | app('laravellocalization')->getDefaultLocale(), 40 | app('laravellocalization')->getSupportedLocales(), 41 | $request 42 | ); 43 | $locale = $negotiator->negotiateLanguage(); 44 | Cookie::queue(Cookie::forever('locale', $locale)); 45 | } 46 | 47 | if ($locale === false){ 48 | $locale = app('laravellocalization')->getCurrentLocale(); 49 | } 50 | 51 | if ( 52 | $locale && 53 | app('laravellocalization')->checkLocaleInSupportedLocales($locale) && 54 | !(app('laravellocalization')->isHiddenDefault($locale)) 55 | ) { 56 | $redirection = app('laravellocalization')->getLocalizedURL($locale); 57 | $redirectResponse = new RedirectResponse($redirection, 302, ['Vary' => 'Accept-Language']); 58 | 59 | return $redirectResponse->withCookie(cookie()->forever('locale', $locale)); 60 | } 61 | 62 | return $next($request); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Mcamara/LaravelLocalization/Middleware/LocaleSessionRedirect.php: -------------------------------------------------------------------------------- 1 | shouldIgnore($request)) { 23 | return $next($request); 24 | } 25 | 26 | $params = explode('/', $request->path()); 27 | $locale = session('locale', false); 28 | 29 | if (\count($params) > 0 && app('laravellocalization')->checkLocaleInSupportedLocales($params[0])) { 30 | session(['locale' => $params[0]]); 31 | 32 | return $next($request); 33 | } 34 | 35 | if (empty($locale) && app('laravellocalization')->hideUrlAndAcceptHeader()){ 36 | // When default locale is hidden and accept language header is true, 37 | // then compute browser language when no session has been set. 38 | // Once the session has been set, there is no need 39 | // to negotiate language from browser again. 40 | $negotiator = new LanguageNegotiator( 41 | app('laravellocalization')->getDefaultLocale(), 42 | app('laravellocalization')->getSupportedLocales(), 43 | $request 44 | ); 45 | $locale = $negotiator->negotiateLanguage(); 46 | session(['locale' => $locale]); 47 | } 48 | 49 | if ($locale === false){ 50 | $locale = app('laravellocalization')->getCurrentLocale(); 51 | } 52 | 53 | if ( 54 | $locale && 55 | app('laravellocalization')->checkLocaleInSupportedLocales($locale) && 56 | !(app('laravellocalization')->isHiddenDefault($locale)) 57 | ) { 58 | app('session')->reflash(); 59 | $redirection = app('laravellocalization')->getLocalizedURL($locale); 60 | 61 | return new RedirectResponse($redirection, 302, ['Vary' => 'Accept-Language']); 62 | } 63 | 64 | return $next($request); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Mcamara/LaravelLocalization/Traits/LoadsTranslatedCachedRoutes.php: -------------------------------------------------------------------------------- 1 | getLaravelLocalization(); 24 | 25 | // compute $locale from url. 26 | // It is null if url does not contain locale. 27 | $locale = $localization->getInversedLocaleFromMapping( 28 | $localization->setLocale() 29 | ); 30 | 31 | $localeKeys = $localization->getSupportedLanguagesKeys(); 32 | 33 | // First, try to load the routes specifically cached for this locale 34 | // if they do not exist, write a warning to the log and load the default 35 | // routes instead. Note that this is guaranteed to exist, because the 36 | // 'cached routes' check in the Application checks its existence. 37 | 38 | $path = $this->makeLocaleRoutesPath($locale, $localeKeys); 39 | 40 | if ( ! file_exists($path)) { 41 | 42 | Log::warning("Routes cached, but no cached routes found for locale '{$locale}'!"); 43 | 44 | $path = $this->getDefaultCachedRoutePath(); 45 | } 46 | 47 | $this->app->booted(function () use ($path) { 48 | require $path; 49 | }); 50 | } 51 | 52 | /** 53 | * Returns the path to the cached routes file for a given locale. 54 | * 55 | * @param string $locale 56 | * @param string[] $localeKeys 57 | * @return string 58 | */ 59 | protected function makeLocaleRoutesPath($locale, $localeKeys) 60 | { 61 | $path = $this->getDefaultCachedRoutePath(); 62 | 63 | if ( ! $locale || ! in_array($locale, $localeKeys)) { 64 | return $path; 65 | } 66 | 67 | return substr($path, 0, -4) . '_' . $locale . '.php'; 68 | } 69 | 70 | /** 71 | * Returns the path to the standard cached routes file. 72 | * 73 | * @return string 74 | */ 75 | protected function getDefaultCachedRoutePath() 76 | { 77 | return $this->app->getCachedRoutesPath(); 78 | } 79 | 80 | /** 81 | * @return string|null 82 | */ 83 | protected function getLocaleFromRequest() 84 | { 85 | return request()->segment(1); 86 | } 87 | 88 | /** 89 | * @return \Mcamara\LaravelLocalization\LaravelLocalization 90 | */ 91 | protected function getLaravelLocalization() 92 | { 93 | return app('laravellocalization'); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Mcamara/LaravelLocalization/Traits/TranslatedRouteCommandContext.php: -------------------------------------------------------------------------------- 1 | getSupportedLocales()); 16 | } 17 | 18 | /** 19 | * @return string[] 20 | */ 21 | protected function getSupportedLocales() 22 | { 23 | return $this->getLaravelLocalization()->getSupportedLanguagesKeys(); 24 | } 25 | 26 | /** 27 | * @return \Mcamara\LaravelLocalization\LaravelLocalization 28 | */ 29 | protected function getLaravelLocalization() 30 | { 31 | return $this->laravel->make('laravellocalization'); 32 | } 33 | 34 | /** 35 | * @return string 36 | */ 37 | protected function getBootstrapPath() 38 | { 39 | if (method_exists($this->laravel, 'bootstrapPath')) { 40 | return $this->laravel->bootstrapPath(); 41 | } 42 | 43 | return $this->laravel->basePath() . DIRECTORY_SEPARATOR . 'bootstrap'; 44 | } 45 | 46 | /** 47 | * @param string $locale 48 | * @return string 49 | */ 50 | protected function makeLocaleRoutesPath($locale = '') 51 | { 52 | $path = $this->laravel->getCachedRoutesPath(); 53 | 54 | if ( ! $locale ) { 55 | return $path; 56 | } 57 | 58 | return substr($path, 0, -4) . '_' . $locale . '.php'; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/stubs/routes.stub: -------------------------------------------------------------------------------- 1 | setRoutes( 17 | unserialize(base64_decode('{{routes}}')) 18 | ); 19 | 20 | app('laravellocalization')->setSerializedTranslatedRoutes('{{translatedRoutes}}'); 21 | -------------------------------------------------------------------------------- /tests/CustomTranslatorTest.php: -------------------------------------------------------------------------------- 1 | app, 22 | $this->app['config'], 23 | $translator, 24 | $this->app['router'], 25 | $this->app['request'], 26 | $this->app['url'] 27 | ); 28 | 29 | $localization->setLocale('es'); 30 | 31 | $this->assertEquals('es', $translator->getLocale()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/LaravelLocalizationTest.php: -------------------------------------------------------------------------------- 1 | setLocale($locale); 26 | } 27 | 28 | app('router')->group([ 29 | 'prefix' => app('laravellocalization')->setLocale(), 30 | 'middleware' => [ 31 | 'Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRoutes', 32 | 'Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRedirectFilter', 33 | ], 34 | ], function () { 35 | app('router')->get('/', ['as'=> 'index', function () { 36 | return app('translator')->get('LaravelLocalization::routes.hello'); 37 | }, ]); 38 | 39 | app('router')->get('test', ['as'=> 'test', function () { 40 | return app('translator')->get('LaravelLocalization::routes.test_text'); 41 | }, ]); 42 | 43 | app('router')->get(app('laravellocalization')->transRoute('LaravelLocalization::routes.about'), ['as'=> 'about', function () { 44 | return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available'; 45 | }, ]); 46 | 47 | app('router')->get(app('laravellocalization')->transRoute('LaravelLocalization::routes.view'), ['as'=> 'view', function () { 48 | return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available'; 49 | }, ]); 50 | 51 | app('router')->get(app('laravellocalization')->transRoute('LaravelLocalization::routes.view_project'), ['as'=> 'view_project', function () { 52 | return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available'; 53 | }, ]); 54 | 55 | app('router')->get(app('laravellocalization')->transRoute('LaravelLocalization::routes.manage'), ['as'=> 'manage', function () { 56 | return app('laravellocalization')->getLocalizedURL('es') ?: 'Not url available'; 57 | }, ]); 58 | }); 59 | 60 | app('router')->get('/skipped', ['as'=> 'skipped', function () { 61 | return Request::url(); 62 | }, ]); 63 | } 64 | 65 | /** 66 | * Refresh routes and refresh application. 67 | * 68 | * @param bool|string $locale 69 | */ 70 | protected function refreshApplication($locale = false) 71 | { 72 | parent::refreshApplication(); 73 | $this->setRoutes($locale); 74 | } 75 | 76 | /** 77 | * Create fake request 78 | * @param [type] $method [description] 79 | * @param [type] $content [description] 80 | * @param string $uri [description] 81 | * @param array $server [description] 82 | * @param array $parameters [description] 83 | * @param array $cookies [description] 84 | * @param array $files [description] 85 | * @return [type] [description] 86 | */ 87 | protected function createRequest( 88 | $uri = '/test', 89 | $method = 'GET', 90 | $parameters = [], 91 | $cookies = [], 92 | $files = [], 93 | $server = ['CONTENT_TYPE' => 'application/json'], 94 | $content = null 95 | ) 96 | { 97 | $request = new \Illuminate\Http\Request; 98 | return $request->createFromBase( 99 | \Symfony\Component\HttpFoundation\Request::create( 100 | $uri, 101 | 'GET', 102 | [], 103 | [], 104 | [], 105 | $server, 106 | $content 107 | ) 108 | ); 109 | } 110 | 111 | /** 112 | * Define environment setup. 113 | * 114 | * @param Illuminate\Foundation\Application $app 115 | * 116 | * @return void 117 | */ 118 | protected function getEnvironmentSetUp($app) 119 | { 120 | app('config')->set('app.url', self::TEST_URL); 121 | 122 | app('config')->set('app.locale', $this->defaultLocale); 123 | 124 | $packageConfigFile = __DIR__.'/../src/config/config.php'; 125 | $config = app('files')->getRequire($packageConfigFile); 126 | 127 | app('config')->set('laravellocalization', $config); 128 | 129 | $this->supportedLocales = app('config')->get('laravellocalization.supportedLocales'); 130 | 131 | app('translator')->getLoader()->addNamespace('LaravelLocalization', realpath(dirname(__FILE__)).'/lang'); 132 | 133 | app('translator')->load('LaravelLocalization', 'routes', 'es'); 134 | app('translator')->load('LaravelLocalization', 'routes', 'en'); 135 | 136 | app('laravellocalization')->setBaseUrl(self::TEST_URL); 137 | 138 | $this->setRoutes(); 139 | } 140 | 141 | public function testSetLocale(): void 142 | { 143 | $this->assertEquals(route('about'), 'http://localhost/about'); 144 | 145 | $this->refreshApplication('es'); 146 | $this->assertEquals('es', app('laravellocalization')->setLocale('es')); 147 | $this->assertEquals('es', app('laravellocalization')->getCurrentLocale()); 148 | $this->assertEquals(route('about'), 'http://localhost/acerca'); 149 | 150 | $this->refreshApplication(); 151 | 152 | $this->assertEquals('en', app('laravellocalization')->setLocale('en')); 153 | 154 | $this->assertEquals(route('about'), 'http://localhost/about'); 155 | 156 | $this->assertNull(app('laravellocalization')->setLocale('de')); 157 | $this->assertEquals('en', app('laravellocalization')->getCurrentLocale()); 158 | } 159 | 160 | public function testLocalizeURL(): void 161 | { 162 | $this->assertEquals( 163 | self::TEST_URL.app('laravellocalization')->getCurrentLocale(), 164 | app('laravellocalization')->localizeURL() 165 | ); 166 | 167 | // Missing trailing slash in a URL 168 | $this->assertEquals( 169 | self::TEST_URL.app('laravellocalization')->getCurrentLocale(), 170 | app('laravellocalization')->localizeURL() 171 | ); 172 | 173 | app('config')->set('laravellocalization.hideDefaultLocaleInURL', true); 174 | 175 | // testing hide default locale option 176 | $this->assertNotEquals( 177 | self::TEST_URL.app('laravellocalization')->getDefaultLocale(), 178 | app('laravellocalization')->localizeURL() 179 | ); 180 | 181 | $this->assertEquals( 182 | self::TEST_URL, 183 | app('laravellocalization')->localizeURL() 184 | ); 185 | 186 | app('laravellocalization')->setLocale('es'); 187 | 188 | $this->assertEquals( 189 | self::TEST_URL.'es', 190 | app('laravellocalization')->localizeURL() 191 | ); 192 | 193 | $this->assertEquals( 194 | self::TEST_URL.'about', 195 | app('laravellocalization')->localizeURL(self::TEST_URL.'about', 'en') 196 | ); 197 | 198 | $this->assertNotEquals( 199 | self::TEST_URL.'en/about', 200 | app('laravellocalization')->localizeURL(self::TEST_URL.'about', 'en') 201 | ); 202 | 203 | app('config')->set('laravellocalization.hideDefaultLocaleInURL', false); 204 | 205 | $this->assertEquals( 206 | self::TEST_URL.'en/about', 207 | app('laravellocalization')->localizeURL(self::TEST_URL.'about', 'en') 208 | ); 209 | 210 | $this->assertNotEquals( 211 | self::TEST_URL.'about', 212 | app('laravellocalization')->localizeURL(self::TEST_URL.'about', 'en') 213 | ); 214 | } 215 | 216 | public function testGetLocalizedURL(): void 217 | { 218 | $this->assertEquals( 219 | self::TEST_URL.app('laravellocalization')->getCurrentLocale(), 220 | app('laravellocalization')->getLocalizedURL() 221 | ); 222 | 223 | app('config')->set('laravellocalization.hideDefaultLocaleInURL', true); 224 | // testing default language hidden 225 | 226 | $this->assertNotEquals( 227 | self::TEST_URL.app('laravellocalization')->getDefaultLocale(), 228 | app('laravellocalization')->getLocalizedURL() 229 | ); 230 | 231 | app('laravellocalization')->setLocale('es'); 232 | 233 | $this->assertNotEquals( 234 | self::TEST_URL, 235 | app('laravellocalization')->getLocalizedURL() 236 | ); 237 | 238 | $this->assertNotEquals( 239 | self::TEST_URL.app('laravellocalization')->getDefaultLocale(), 240 | app('laravellocalization')->getLocalizedURL() 241 | ); 242 | 243 | $this->assertEquals( 244 | self::TEST_URL.app('laravellocalization')->getCurrentLocale(), 245 | app('laravellocalization')->getLocalizedURL() 246 | ); 247 | 248 | $this->assertEquals( 249 | self::TEST_URL.'es/acerca', 250 | app('laravellocalization')->getLocalizedURL('es', self::TEST_URL.'about') 251 | ); 252 | 253 | app('laravellocalization')->setLocale('en'); 254 | 255 | $crawler = $this->call( 256 | 'GET', 257 | self::TEST_URL.'about', 258 | [], 259 | [], 260 | [], 261 | ['HTTP_ACCEPT_LANGUAGE' => 'en,es'] 262 | ); 263 | 264 | $this->assertResponseOk(); 265 | $this->assertEquals( 266 | self::TEST_URL.'es/acerca', 267 | $crawler->getContent() 268 | ); 269 | 270 | $this->refreshApplication(); 271 | 272 | app('config')->set('laravellocalization.hideDefaultLocaleInURL', true); 273 | 274 | $this->assertEquals( 275 | self::TEST_URL.'test', 276 | app('laravellocalization')->getLocalizedURL('en', self::TEST_URL.'test') 277 | ); 278 | 279 | $this->assertEquals( 280 | self::TEST_URL.'test?a=1', 281 | app('laravellocalization')->getLocalizedURL('en', self::TEST_URL.'test?a=1') 282 | ); 283 | 284 | $crawler = $this->call( 285 | 'GET', 286 | app('laravellocalization')->getLocalizedURL('en', self::TEST_URL.'test'), 287 | [], 288 | [], 289 | [], 290 | ['HTTP_ACCEPT_LANGUAGE' => 'en,es'] 291 | ); 292 | 293 | $this->assertResponseOk(); 294 | $this->assertEquals( 295 | 'Test text', 296 | $crawler->getContent() 297 | ); 298 | 299 | $this->refreshApplication('es'); 300 | 301 | $this->assertEquals( 302 | self::TEST_URL.'es/test', 303 | app('laravellocalization')->getLocalizedURL('es', self::TEST_URL.'test') 304 | ); 305 | 306 | $this->assertEquals( 307 | self::TEST_URL.'es/test?a=1', 308 | app('laravellocalization')->getLocalizedURL('es', self::TEST_URL.'test?a=1') 309 | ); 310 | } 311 | 312 | public function testGetLocalizedURLWithQueryStringAndhideDefaultLocaleInURL(): void 313 | { 314 | app('config')->set('laravellocalization.hideDefaultLocaleInURL', true); 315 | $request = $this->createRequest( 316 | $uri = 'en/about?q=2' 317 | ); 318 | $laravelLocalization = app(LaravelLocalization::class, ['request' => $request]); 319 | $laravelLocalization->transRoute('LaravelLocalization::routes.about'); 320 | 321 | $this->assertEquals( 322 | self::TEST_URL . 'about?q=2', 323 | $laravelLocalization->getLocalizedURL() 324 | ); 325 | } 326 | 327 | public function testGetLocalizedURLWithQueryStringAndNotTranslatedRoute(): void 328 | { 329 | $request = $this->createRequest( 330 | $uri = 'en/about?q=2' 331 | ); 332 | $laravelLocalization = app(LaravelLocalization::class, ['request' => $request]); 333 | 334 | $this->assertEquals( 335 | self::TEST_URL . 'en/about?q=2', 336 | $laravelLocalization->getLocalizedURL() 337 | ); 338 | } 339 | 340 | /** 341 | * @param string $path 342 | * @param string|bool $expectedRouteName 343 | */ 344 | #[DataProvider('getRouteNameFromAPathDataProvider')] 345 | public function testGetRouteNameFromAPath($path, $expectedRouteName): void 346 | { 347 | $this->assertEquals( 348 | $expectedRouteName, 349 | app('laravellocalization')->getRouteNameFromAPath($path) 350 | ); 351 | } 352 | 353 | public static function getRouteNameFromAPathDataProvider(): array 354 | { 355 | return [ 356 | [self::TEST_URL, false], 357 | [self::TEST_URL.'es', false], 358 | [self::TEST_URL.'en/about', 'LaravelLocalization::routes.about'], 359 | [self::TEST_URL.'ver/1', false], 360 | [self::TEST_URL.'view/1', 'LaravelLocalization::routes.view'], 361 | [self::TEST_URL.'view/1/project', 'LaravelLocalization::routes.view_project'], 362 | [self::TEST_URL.'view/1/project/1', 'LaravelLocalization::routes.view_project'], 363 | [self::TEST_URL.'en/view/1/project/1', 'LaravelLocalization::routes.view_project'], 364 | [self::TEST_URL.'manage/1', 'LaravelLocalization::routes.manage'], 365 | [self::TEST_URL.'manage', 'LaravelLocalization::routes.manage'], 366 | [self::TEST_URL.'manage/', 'LaravelLocalization::routes.manage'], 367 | [self::TEST_URL.'manage/0', 'LaravelLocalization::routes.manage'], 368 | [self::TEST_URL.'manage/0?ex=2&ex2=a', 'LaravelLocalization::routes.manage'], 369 | ]; 370 | } 371 | 372 | public function testGetLocalizedUrlForIgnoredUrls(): void { 373 | $crawler = $this->call( 374 | 'GET', 375 | self::TEST_URL.'skipped', 376 | [], 377 | [], 378 | [], 379 | ['HTTP_ACCEPT_LANGUAGE' => 'en,es'] 380 | ); 381 | 382 | $this->assertResponseOk(); 383 | $this->assertEquals( 384 | self::TEST_URL.'skipped', 385 | $crawler->getContent() 386 | ); 387 | } 388 | 389 | /** 390 | * @param bool $hideDefaultLocaleInURL 391 | * @param bool $forceDefault 392 | * @param string $locale 393 | * @param string $path 394 | * @param string $expectedURL 395 | */ 396 | #[DataProvider('getLocalizedURLDataProvider')] 397 | public function testGetLocalizedURLFormat($hideDefaultLocaleInURL, $forceDefault, $locale, $path, $expectedURL): void 398 | { 399 | app('config')->set('laravellocalization.hideDefaultLocaleInURL', $hideDefaultLocaleInURL); 400 | $this->assertEquals( 401 | $expectedURL, 402 | app('laravellocalization')->getLocalizedURL($locale, $path, [], $forceDefault) 403 | ); 404 | 405 | } 406 | 407 | public static function getLocalizedURLDataProvider(): array 408 | { 409 | return [ 410 | // Do not hide default 411 | [false, false, 'es', self::TEST_URL, self::TEST_URL.'es'], 412 | [false, false, 'es', self::TEST_URL.'es', self::TEST_URL.'es'], 413 | [false, false, 'es', self::TEST_URL.'en/about', self::TEST_URL.'es/acerca'], 414 | [false, false, 'es', self::TEST_URL.'ver/1', self::TEST_URL.'es/ver/1'], 415 | [false, false, 'es', self::TEST_URL.'view/1/project', self::TEST_URL.'es/ver/1/proyecto'], 416 | [false, false, 'es', self::TEST_URL.'view/1/project/1', self::TEST_URL.'es/ver/1/proyecto/1'], 417 | [false, false, 'es', self::TEST_URL.'en/view/1/project/1', self::TEST_URL.'es/ver/1/proyecto/1'], 418 | [false, false, 'es', self::TEST_URL.'manage/1', self::TEST_URL.'es/administrar/1'], 419 | [false, false, 'es', self::TEST_URL.'manage', self::TEST_URL.'es/administrar'], 420 | [false, false, 'es', self::TEST_URL.'manage/', self::TEST_URL.'es/administrar'], 421 | [false, false, 'es', self::TEST_URL.'manage/0', self::TEST_URL.'es/administrar/0'], 422 | [false, false, 'es', self::TEST_URL.'manage/0?ex=2&ex2=a', self::TEST_URL.'es/administrar/0?ex=2&ex2=a'], 423 | 424 | // Do not hide default 425 | [false, false, 'en', self::TEST_URL.'en', self::TEST_URL.'en'], 426 | [false, false, 'en', self::TEST_URL.'about', self::TEST_URL.'en/about'], 427 | [false, false, 'en', self::TEST_URL.'ver/1', self::TEST_URL.'en/ver/1'], 428 | [false, false, 'en', self::TEST_URL.'view/1/project', self::TEST_URL.'en/view/1/project'], 429 | [false, false, 'en', self::TEST_URL.'view/1/project/1', self::TEST_URL.'en/view/1/project/1'], 430 | [false, false, 'en', self::TEST_URL.'en/view/1/project/1', self::TEST_URL.'en/view/1/project/1'], 431 | [false, false, 'en', self::TEST_URL.'manage/1', self::TEST_URL.'en/manage/1'], 432 | [false, false, 'en', self::TEST_URL.'manage', self::TEST_URL.'en/manage'], 433 | [false, false, 'en', self::TEST_URL.'manage/', self::TEST_URL.'en/manage'], 434 | [false, false, 'en', self::TEST_URL.'manage/0', self::TEST_URL.'en/manage/0'], 435 | [false, false, 'en', self::TEST_URL.'manage/0?ex=2&ex2=a', self::TEST_URL.'en/manage/0?ex=2&ex2=a'], 436 | 437 | // Hide default 438 | [true, false, 'es', self::TEST_URL, self::TEST_URL.'es'], 439 | [true, false, 'es', self::TEST_URL.'es', self::TEST_URL.'es'], 440 | [true, false, 'es', self::TEST_URL.'en/about', self::TEST_URL.'es/acerca'], 441 | [true, false, 'es', self::TEST_URL.'ver/1', self::TEST_URL.'es/ver/1'], 442 | [true, false, 'es', self::TEST_URL.'view/1/project', self::TEST_URL.'es/ver/1/proyecto'], 443 | [true, false, 'es', self::TEST_URL.'view/1/project/1', self::TEST_URL.'es/ver/1/proyecto/1'], 444 | [true, false, 'es', self::TEST_URL.'en/view/1/project/1', self::TEST_URL.'es/ver/1/proyecto/1'], 445 | [true, false, 'es', self::TEST_URL.'manage/1', self::TEST_URL.'es/administrar/1'], 446 | [true, false, 'es', self::TEST_URL.'manage', self::TEST_URL.'es/administrar'], 447 | [true, false, 'es', self::TEST_URL.'manage/', self::TEST_URL.'es/administrar'], 448 | [true, false, 'es', self::TEST_URL.'manage/0', self::TEST_URL.'es/administrar/0'], 449 | [true, false, 'es', self::TEST_URL.'manage/0?ex=2&ex2=a', self::TEST_URL.'es/administrar/0?ex=2&ex2=a'], 450 | 451 | // Hide default 452 | [true, false, 'en', self::TEST_URL.'en', self::TEST_URL.''], 453 | [true, false, 'en', self::TEST_URL.'about', self::TEST_URL.'about'], 454 | [true, false, 'en', self::TEST_URL.'ver/1', self::TEST_URL.'ver/1'], 455 | [true, false, 'en', self::TEST_URL.'view/1/project', self::TEST_URL.'view/1/project'], 456 | [true, false, 'en', self::TEST_URL.'view/1/project/1', self::TEST_URL.'view/1/project/1'], 457 | [true, false, 'en', self::TEST_URL.'en/view/1/project/1', self::TEST_URL.'view/1/project/1'], 458 | [true, false, 'en', self::TEST_URL.'manage/1', self::TEST_URL.'manage/1'], 459 | [true, false, 'en', self::TEST_URL.'manage', self::TEST_URL.'manage'], 460 | [true, false, 'en', self::TEST_URL.'manage/', self::TEST_URL.'manage'], 461 | [true, false, 'en', self::TEST_URL.'manage/0', self::TEST_URL.'manage/0'], 462 | [true, false, 'en', self::TEST_URL.'manage/0?ex=2&ex2=a', self::TEST_URL.'manage/0?ex=2&ex2=a'], 463 | 464 | // Do not hide default FORCE SHOWING 465 | [false, true, 'es', self::TEST_URL, self::TEST_URL.'es'], 466 | [false, true, 'es', self::TEST_URL.'es', self::TEST_URL.'es'], 467 | [false, true, 'es', self::TEST_URL.'en/about', self::TEST_URL.'es/acerca'], 468 | [false, true, 'es', self::TEST_URL.'ver/1', self::TEST_URL.'es/ver/1'], 469 | [false, true, 'es', self::TEST_URL.'view/1/project', self::TEST_URL.'es/ver/1/proyecto'], 470 | [false, true, 'es', self::TEST_URL.'view/1/project/1', self::TEST_URL.'es/ver/1/proyecto/1'], 471 | [false, true, 'es', self::TEST_URL.'en/view/1/project/1', self::TEST_URL.'es/ver/1/proyecto/1'], 472 | [false, true, 'es', self::TEST_URL.'manage/1', self::TEST_URL.'es/administrar/1'], 473 | [false, true, 'es', self::TEST_URL.'manage', self::TEST_URL.'es/administrar'], 474 | [false, true, 'es', self::TEST_URL.'manage/', self::TEST_URL.'es/administrar'], 475 | [false, true, 'es', self::TEST_URL.'manage/0', self::TEST_URL.'es/administrar/0'], 476 | [false, true, 'es', self::TEST_URL.'manage/0?ex=2&ex2=a', self::TEST_URL.'es/administrar/0?ex=2&ex2=a'], 477 | 478 | // Do not hide default FORCE SHOWING 479 | [false, true, 'en', self::TEST_URL.'en', self::TEST_URL.'en'], 480 | [false, true, 'en', self::TEST_URL.'about', self::TEST_URL.'en/about'], 481 | [false, true, 'en', self::TEST_URL.'ver/1', self::TEST_URL.'en/ver/1'], 482 | [false, true, 'en', self::TEST_URL.'view/1/project', self::TEST_URL.'en/view/1/project'], 483 | [false, true, 'en', self::TEST_URL.'view/1/project/1', self::TEST_URL.'en/view/1/project/1'], 484 | [false, true, 'en', self::TEST_URL.'en/view/1/project/1', self::TEST_URL.'en/view/1/project/1'], 485 | [false, true, 'en', self::TEST_URL.'manage/1', self::TEST_URL.'en/manage/1'], 486 | [false, true, 'en', self::TEST_URL.'manage', self::TEST_URL.'en/manage'], 487 | [false, true, 'en', self::TEST_URL.'manage/', self::TEST_URL.'en/manage'], 488 | [false, true, 'en', self::TEST_URL.'manage/0', self::TEST_URL.'en/manage/0'], 489 | [false, true, 'en', self::TEST_URL.'manage/0?ex=2&ex2=a', self::TEST_URL.'en/manage/0?ex=2&ex2=a'], 490 | 491 | // Hide default FORCE SHOWING 492 | [true, true, 'es', self::TEST_URL, self::TEST_URL.'es'], 493 | [true, true, 'es', self::TEST_URL.'es', self::TEST_URL.'es'], 494 | [true, true, 'es', self::TEST_URL.'en/about', self::TEST_URL.'es/acerca'], 495 | [true, true, 'es', self::TEST_URL.'ver/1', self::TEST_URL.'es/ver/1'], 496 | [true, true, 'es', self::TEST_URL.'view/1/project', self::TEST_URL.'es/ver/1/proyecto'], 497 | [true, true, 'es', self::TEST_URL.'view/1/project/1', self::TEST_URL.'es/ver/1/proyecto/1'], 498 | [true, true, 'es', self::TEST_URL.'en/view/1/project/1', self::TEST_URL.'es/ver/1/proyecto/1'], 499 | [true, true, 'es', self::TEST_URL.'manage/1', self::TEST_URL.'es/administrar/1'], 500 | [true, true, 'es', self::TEST_URL.'manage', self::TEST_URL.'es/administrar'], 501 | [true, true, 'es', self::TEST_URL.'manage/', self::TEST_URL.'es/administrar'], 502 | [true, true, 'es', self::TEST_URL.'manage/0', self::TEST_URL.'es/administrar/0'], 503 | [true, true, 'es', self::TEST_URL.'manage/0?ex=2&ex2=a', self::TEST_URL.'es/administrar/0?ex=2&ex2=a'], 504 | 505 | // Hide default FORCE SHOWING 506 | [true, true, 'en', self::TEST_URL.'en', self::TEST_URL.'en'], 507 | [true, true, 'en', self::TEST_URL.'about', self::TEST_URL.'en/about'], 508 | [true, true, 'en', self::TEST_URL.'ver/1', self::TEST_URL.'en/ver/1'], 509 | [true, true, 'en', self::TEST_URL.'view/1/project', self::TEST_URL.'en/view/1/project'], 510 | [true, true, 'en', self::TEST_URL.'view/1/project/1', self::TEST_URL.'en/view/1/project/1'], 511 | [true, true, 'en', self::TEST_URL.'en/view/1/project/1', self::TEST_URL.'en/view/1/project/1'], 512 | [true, true, 'en', self::TEST_URL.'manage/1', self::TEST_URL.'en/manage/1'], 513 | [true, true, 'en', self::TEST_URL.'manage', self::TEST_URL.'en/manage'], 514 | [true, true, 'en', self::TEST_URL.'manage/', self::TEST_URL.'en/manage'], 515 | [true, true, 'en', self::TEST_URL.'manage/0', self::TEST_URL.'en/manage/0'], 516 | [true, true, 'en', self::TEST_URL.'manage/0?ex=2&ex2=a', self::TEST_URL.'en/manage/0?ex=2&ex2=a'], 517 | ]; 518 | } 519 | 520 | public function testGetURLFromRouteNameTranslated(): void 521 | { 522 | $this->assertEquals( 523 | self::TEST_URL.'es/acerca', 524 | app('laravellocalization')->getURLFromRouteNameTranslated('es', 'LaravelLocalization::routes.about') 525 | ); 526 | 527 | $this->assertEquals( 528 | self::TEST_URL.'en/about', 529 | app('laravellocalization')->getURLFromRouteNameTranslated('en', 'LaravelLocalization::routes.about') 530 | ); 531 | 532 | $this->assertEquals( 533 | self::TEST_URL.'en/view/1', 534 | app('laravellocalization')->getURLFromRouteNameTranslated('en', 'LaravelLocalization::routes.view', ['id' => 1]) 535 | ); 536 | 537 | app('config')->set('laravellocalization.hideDefaultLocaleInURL', true); 538 | 539 | $this->assertEquals( 540 | self::TEST_URL.'about', 541 | app('laravellocalization')->getURLFromRouteNameTranslated('en', 'LaravelLocalization::routes.about') 542 | ); 543 | 544 | $this->assertEquals( 545 | self::TEST_URL.'es/acerca', 546 | app('laravellocalization')->getURLFromRouteNameTranslated('es', 'LaravelLocalization::routes.about') 547 | ); 548 | 549 | $this->assertEquals( 550 | self::TEST_URL.'es/ver/1', 551 | app('laravellocalization')->getURLFromRouteNameTranslated('es', 'LaravelLocalization::routes.view', ['id' => 1]) 552 | ); 553 | 554 | $this->assertEquals( 555 | self::TEST_URL.'view/1', 556 | app('laravellocalization')->getURLFromRouteNameTranslated('en', 'LaravelLocalization::routes.view', ['id' => 1]) 557 | ); 558 | 559 | $this->assertNotEquals( 560 | self::TEST_URL.'en/view/1', 561 | app('laravellocalization')->getURLFromRouteNameTranslated('en', 'LaravelLocalization::routes.view', ['id' => 1]) 562 | ); 563 | 564 | app('config')->set('laravellocalization.hideDefaultLocaleInURL', false); 565 | 566 | $this->assertNotEquals( 567 | self::TEST_URL.'view/1', 568 | app('laravellocalization')->getURLFromRouteNameTranslated('en', 'LaravelLocalization::routes.view', ['id' => 1]) 569 | ); 570 | 571 | $this->assertEquals( 572 | self::TEST_URL.'en/view/1', 573 | app('laravellocalization')->getURLFromRouteNameTranslated('en', 'LaravelLocalization::routes.view', ['id' => 1]) 574 | ); 575 | } 576 | 577 | public function testLocalizedParameterFromTranslateUrl(): void 578 | { 579 | $model = new ModelWithTranslatableRoutes(); 580 | 581 | $this->assertEquals( 582 | self::TEST_URL.'en/view/company', 583 | app('laravellocalization')->getURLFromRouteNameTranslated('en', 'LaravelLocalization::routes.view', ['id' => $model]) 584 | ); 585 | 586 | $this->assertEquals( 587 | self::TEST_URL.'es/ver/empresa', 588 | app('laravellocalization')->getURLFromRouteNameTranslated('es', 'LaravelLocalization::routes.view', ['id' => $model]) 589 | ); 590 | } 591 | 592 | public function testGetNonLocalizedURL(): void 593 | { 594 | $this->assertEquals( 595 | self::TEST_URL, 596 | app('laravellocalization')->getNonLocalizedURL(self::TEST_URL.'en') 597 | ); 598 | $this->assertEquals( 599 | self::TEST_URL, 600 | app('laravellocalization')->getNonLocalizedURL(self::TEST_URL.'es') 601 | ); 602 | $this->assertEquals( 603 | self::TEST_URL.'view/1', 604 | app('laravellocalization')->getNonLocalizedURL(self::TEST_URL.'en/view/1') 605 | ); 606 | $this->assertEquals( 607 | self::TEST_URL.'ver/1', 608 | app('laravellocalization')->getNonLocalizedURL(self::TEST_URL.'es/ver/1') 609 | ); 610 | } 611 | 612 | public function testGetDefaultLocale(): void 613 | { 614 | $this->assertEquals( 615 | 'en', 616 | app('laravellocalization')->getDefaultLocale() 617 | ); 618 | 619 | app('laravellocalization')->setLocale('es'); 620 | $this->refreshApplication('es'); 621 | 622 | $this->assertEquals( 623 | 'en', 624 | app('laravellocalization')->getDefaultLocale() 625 | ); 626 | } 627 | 628 | public function testGetSupportedLocales(): void 629 | { 630 | $this->assertEquals( 631 | $this->supportedLocales, 632 | app('laravellocalization')->getSupportedLocales() 633 | ); 634 | } 635 | 636 | public function testGetCurrentLocaleName(): void 637 | { 638 | $this->assertEquals( 639 | 'English', 640 | app('laravellocalization')->getCurrentLocaleName() 641 | ); 642 | 643 | $this->refreshApplication('es'); 644 | 645 | $this->assertEquals( 646 | 'Spanish', 647 | app('laravellocalization')->getCurrentLocaleName() 648 | ); 649 | } 650 | 651 | public function testGetCurrentLocaleRegional(): void 652 | { 653 | $this->assertEquals( 654 | 'en_GB', 655 | app('laravellocalization')->getCurrentLocaleRegional() 656 | ); 657 | 658 | $this->refreshApplication('es'); 659 | 660 | $this->assertEquals( 661 | 'es_ES', 662 | app('laravellocalization')->getCurrentLocaleRegional() 663 | ); 664 | } 665 | 666 | public function testGetCurrentLocaleDirection(): void 667 | { 668 | $this->assertEquals( 669 | 'ltr', 670 | app('laravellocalization')->getCurrentLocaleDirection() 671 | ); 672 | 673 | app('laravellocalization')->setLocale('es'); 674 | $this->refreshApplication('es'); 675 | 676 | $this->assertEquals( 677 | 'ltr', 678 | app('laravellocalization')->getCurrentLocaleDirection() 679 | ); 680 | } 681 | 682 | public function testGetCurrentLocaleScript(): void 683 | { 684 | app('laravellocalization')->setLocale('es'); 685 | $this->refreshApplication('es'); 686 | 687 | $this->assertEquals( 688 | 'Latn', 689 | app('laravellocalization')->getCurrentLocaleScript() 690 | ); 691 | 692 | app('laravellocalization')->setLocale('en'); 693 | $this->refreshApplication('en'); 694 | 695 | $this->assertEquals( 696 | 'Latn', 697 | app('laravellocalization')->getCurrentLocaleScript() 698 | ); 699 | } 700 | 701 | public function testGetCurrentLocaleNativeReading(): void 702 | { 703 | $this->assertEquals( 704 | 'English', 705 | app('laravellocalization')->getCurrentLocaleNativeReading() 706 | ); 707 | 708 | app('laravellocalization')->setLocale('es'); 709 | $this->refreshApplication('es'); 710 | 711 | $this->assertEquals( 712 | 'español', 713 | app('laravellocalization')->getCurrentLocaleNativeReading() 714 | ); 715 | } 716 | 717 | public function testGetCurrentLocale(): void 718 | { 719 | $this->assertEquals( 720 | 'en', 721 | app('laravellocalization')->getCurrentLocale() 722 | ); 723 | 724 | app('laravellocalization')->setLocale('es'); 725 | $this->refreshApplication('es'); 726 | 727 | $this->assertEquals( 728 | 'es', 729 | app('laravellocalization')->getCurrentLocale() 730 | ); 731 | 732 | $this->assertNotEquals( 733 | 'en', 734 | app('laravellocalization')->getCurrentLocale() 735 | ); 736 | } 737 | 738 | public function testGetSupportedLanguagesKeys(): void 739 | { 740 | $this->assertEquals( 741 | ['en', 'es'], 742 | app('laravellocalization')->getSupportedLanguagesKeys() 743 | ); 744 | } 745 | 746 | public function testGetConfigRepository(): void 747 | { 748 | $this->assertEquals( 749 | app('config'), 750 | app('laravellocalization')->getConfigRepository('/view/1') 751 | ); 752 | } 753 | 754 | public function testCreateUrlFromUri(): void 755 | { 756 | $this->assertEquals( 757 | 'http://localhost/view/1', 758 | app('laravellocalization')->createUrlFromUri('/view/1') 759 | ); 760 | 761 | app('laravellocalization')->setLocale('es'); 762 | $this->refreshApplication('es'); 763 | 764 | $this->assertEquals( 765 | 'http://localhost/ver/1', 766 | app('laravellocalization')->createUrlFromUri('/ver/1') 767 | ); 768 | } 769 | 770 | 771 | #[DataProvider('accept_language_variations_data')] 772 | public function testLanguageNegotiation($accept_string, $must_resolve_to, $asd = null): void { 773 | 774 | $full_config = include __DIR__ . '/full-config/config.php'; 775 | 776 | $request = $this->createMock(\Illuminate\Http\Request::class); 777 | $request->expects($this->any())->method('header')->with('Accept-Language')->willReturn($accept_string); 778 | 779 | $negotiator = app(\Mcamara\LaravelLocalization\LanguageNegotiator::class, 780 | [ 781 | 'defaultLocale' => 'wrong', 782 | 'supportedLanguages' => $full_config['supportedLocales'], 783 | 'request' => $request 784 | ] 785 | ); 786 | 787 | $language = $negotiator->negotiateLanguage(); 788 | 789 | 790 | $this->assertEquals($must_resolve_to, $language); 791 | } 792 | 793 | 794 | public static function accept_language_variations_data(): array { 795 | $variations = [ 796 | ['en-GB', 'en-GB'], 797 | ['en-US', 'en-US'], 798 | ['en-ZA', 'en'], 799 | ['en', 'en'], 800 | ['az-AZ', 'az'], 801 | ['fr-CA,fr;q=0.8', 'fr-CA'], 802 | ['fr-150', 'fr'], 803 | ['zh-Hant-cn', 'zh-Hant'], 804 | ['zh-cn', 'zh'], 805 | ]; 806 | 807 | $dataset = []; 808 | foreach ($variations as $variation) { 809 | $dataset[$variation[0]] = $variation; 810 | } 811 | 812 | return $dataset; 813 | } 814 | 815 | public function testLanguageNegotiationWithMapping(): void { 816 | 817 | $accept_string = 'en-GB'; 818 | $must_resolve_to = 'uk'; 819 | 820 | $mapping = [ 821 | $accept_string => $must_resolve_to 822 | ]; 823 | 824 | $full_config = include __DIR__ . '/full-config/config.php'; 825 | 826 | $full_config['supportedLocales']['uk'] = $full_config['supportedLocales']['en-GB']; 827 | unset($full_config['supportedLocales']['en-GB']); 828 | 829 | app('config')->set('laravellocalization.localesMapping', $mapping); 830 | 831 | $request = $this->createMock(\Illuminate\Http\Request::class); 832 | $request->expects($this->any())->method('header')->with('Accept-Language')->willReturn($accept_string); 833 | 834 | $negotiator = app(\Mcamara\LaravelLocalization\LanguageNegotiator::class, 835 | [ 836 | 'defaultLocale' => 'wrong', 837 | 'supportedLanguages' => $full_config['supportedLocales'], 838 | 'request' => $request 839 | ] 840 | ); 841 | 842 | $language = $negotiator->negotiateLanguage(); 843 | 844 | $this->assertEquals($must_resolve_to, $language); 845 | } 846 | 847 | public function testSetLocaleWithMapping(): void 848 | { 849 | app('config')->set('laravellocalization.localesMapping', [ 850 | 'en' => 'custom', 851 | ]); 852 | 853 | $this->assertEquals('custom', app('laravellocalization')->setLocale('custom')); 854 | $this->assertEquals('en', app('laravellocalization')->getCurrentLocale()); 855 | 856 | $this->assertTrue(app('laravellocalization')->checkLocaleInSupportedLocales('en')); 857 | $this->assertTrue(app('laravellocalization')->checkLocaleInSupportedLocales('custom')); 858 | 859 | $this->assertEquals('http://localhost/custom/some-route', app('laravellocalization')->localizeURL('some-route', 'en')); 860 | $this->assertEquals('http://localhost/custom/some-route', app('laravellocalization')->localizeURL('some-route', 'custom')); 861 | $this->assertEquals('http://localhost/custom', app('laravellocalization')->localizeURL('http://localhost/custom', 'en')); 862 | } 863 | 864 | 865 | 866 | public function testRedirectWithHiddenDefaultLocaleInUrlAndSavedLocale() 867 | { 868 | app('router')->group([ 869 | 'prefix' => app('laravellocalization')->setLocale(), 870 | 'middleware' => [ 871 | 'Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRedirectFilter', 872 | 'Mcamara\LaravelLocalization\Middleware\LocaleCookieRedirect', 873 | ], 874 | ], function (){ 875 | app('router')->get('/', ['as'=> 'index', function () { 876 | return 'Index page'; 877 | }, ]); 878 | }); 879 | 880 | app('config')->set('laravellocalization.hideDefaultLocaleInURL', true); 881 | 882 | $savedLocale = 'es'; 883 | 884 | $crawler = $this->call( 885 | 'GET', 886 | self::TEST_URL, 887 | [], 888 | ['locale' => $savedLocale], 889 | [], 890 | [] 891 | ); 892 | 893 | $this->assertResponseStatus(302); 894 | $this->assertRedirectedTo(self::TEST_URL . $savedLocale); 895 | 896 | $localeCookie = $crawler->headers->getCookies()[0]; 897 | $this->assertEquals($savedLocale, $localeCookie->getValue()); 898 | } 899 | } 900 | -------------------------------------------------------------------------------- /tests/ModelWithTranslatableRoutes.php: -------------------------------------------------------------------------------- 1 | LaravelLocalization::class, 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/full-config/config.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'ace' => ['name' => 'Achinese', 'script' => 'Latn', 'native' => 'Aceh', 'regional' => ''], 10 | 'af' => ['name' => 'Afrikaans', 'script' => 'Latn', 'native' => 'Afrikaans', 'regional' => 'af_ZA'], 11 | 'agq' => ['name' => 'Aghem', 'script' => 'Latn', 'native' => 'Aghem', 'regional' => ''], 12 | 'ak' => ['name' => 'Akan', 'script' => 'Latn', 'native' => 'Akan', 'regional' => 'ak_GH'], 13 | 'an' => ['name' => 'Aragonese', 'script' => 'Latn', 'native' => 'aragonés', 'regional' => 'an_ES'], 14 | 'cch' => ['name' => 'Atsam', 'script' => 'Latn', 'native' => 'Atsam', 'regional' => ''], 15 | 'gn' => ['name' => 'Guaraní', 'script' => 'Latn', 'native' => 'Avañe’ẽ', 'regional' => ''], 16 | 'ae' => ['name' => 'Avestan', 'script' => 'Latn', 'native' => 'avesta', 'regional' => ''], 17 | 'ay' => ['name' => 'Aymara', 'script' => 'Latn', 'native' => 'aymar aru', 'regional' => 'ay_PE'], 18 | 'az' => ['name' => 'Azerbaijani (Latin)', 'script' => 'Latn', 'native' => 'azərbaycanca', 'regional' => 'az_AZ'], 19 | 'id' => ['name' => 'Indonesian', 'script' => 'Latn', 'native' => 'Bahasa Indonesia', 'regional' => 'id_ID'], 20 | 'ms' => ['name' => 'Malay', 'script' => 'Latn', 'native' => 'Bahasa Melayu', 'regional' => 'ms_MY'], 21 | 'bm' => ['name' => 'Bambara', 'script' => 'Latn', 'native' => 'bamanakan', 'regional' => ''], 22 | 'jv' => ['name' => 'Javanese (Latin)', 'script' => 'Latn', 'native' => 'Basa Jawa', 'regional' => ''], 23 | 'su' => ['name' => 'Sundanese', 'script' => 'Latn', 'native' => 'Basa Sunda', 'regional' => ''], 24 | 'bh' => ['name' => 'Bihari', 'script' => 'Latn', 'native' => 'Bihari', 'regional' => ''], 25 | 'bi' => ['name' => 'Bislama', 'script' => 'Latn', 'native' => 'Bislama', 'regional' => ''], 26 | 'nb' => ['name' => 'Norwegian Bokmål', 'script' => 'Latn', 'native' => 'Bokmål', 'regional' => 'nb_NO'], 27 | 'bs' => ['name' => 'Bosnian', 'script' => 'Latn', 'native' => 'bosanski', 'regional' => 'bs_BA'], 28 | 'br' => ['name' => 'Breton', 'script' => 'Latn', 'native' => 'brezhoneg', 'regional' => 'br_FR'], 29 | 'ca' => ['name' => 'Catalan', 'script' => 'Latn', 'native' => 'català', 'regional' => 'ca_ES'], 30 | 'ch' => ['name' => 'Chamorro', 'script' => 'Latn', 'native' => 'Chamoru', 'regional' => ''], 31 | 'ny' => ['name' => 'Chewa', 'script' => 'Latn', 'native' => 'chiCheŵa', 'regional' => ''], 32 | 'kde' => ['name' => 'Makonde', 'script' => 'Latn', 'native' => 'Chimakonde', 'regional' => ''], 33 | 'sn' => ['name' => 'Shona', 'script' => 'Latn', 'native' => 'chiShona', 'regional' => ''], 34 | 'co' => ['name' => 'Corsican', 'script' => 'Latn', 'native' => 'corsu', 'regional' => ''], 35 | 'cy' => ['name' => 'Welsh', 'script' => 'Latn', 'native' => 'Cymraeg', 'regional' => 'cy_GB'], 36 | 'da' => ['name' => 'Danish', 'script' => 'Latn', 'native' => 'dansk', 'regional' => 'da_DK'], 37 | 'se' => ['name' => 'Northern Sami', 'script' => 'Latn', 'native' => 'davvisámegiella', 'regional' => 'se_NO'], 38 | 'de' => ['name' => 'German', 'script' => 'Latn', 'native' => 'Deutsch', 'regional' => 'de_DE'], 39 | 'luo' => ['name' => 'Luo', 'script' => 'Latn', 'native' => 'Dholuo', 'regional' => ''], 40 | 'nv' => ['name' => 'Navajo', 'script' => 'Latn', 'native' => 'Diné bizaad', 'regional' => ''], 41 | 'dua' => ['name' => 'Duala', 'script' => 'Latn', 'native' => 'duálá', 'regional' => ''], 42 | 'et' => ['name' => 'Estonian', 'script' => 'Latn', 'native' => 'eesti', 'regional' => 'et_EE'], 43 | 'na' => ['name' => 'Nauru', 'script' => 'Latn', 'native' => 'Ekakairũ Naoero', 'regional' => ''], 44 | 'guz' => ['name' => 'Ekegusii', 'script' => 'Latn', 'native' => 'Ekegusii', 'regional' => ''], 45 | 'en' => ['name' => 'English', 'script' => 'Latn', 'native' => 'English', 'regional' => 'en_GB'], 46 | 'en-AU' => ['name' => 'Australian English', 'script' => 'Latn', 'native' => 'Australian English', 'regional' => 'en_AU'], 47 | 'en-GB' => ['name' => 'British English', 'script' => 'Latn', 'native' => 'British English', 'regional' => 'en_GB'], 48 | 'en-US' => ['name' => 'U.S. English', 'script' => 'Latn', 'native' => 'U.S. English', 'regional' => 'en_US'], 49 | 'es' => ['name' => 'Spanish', 'script' => 'Latn', 'native' => 'español', 'regional' => 'es_ES'], 50 | 'eo' => ['name' => 'Esperanto', 'script' => 'Latn', 'native' => 'esperanto', 'regional' => ''], 51 | 'eu' => ['name' => 'Basque', 'script' => 'Latn', 'native' => 'euskara', 'regional' => 'eu_ES'], 52 | 'ewo' => ['name' => 'Ewondo', 'script' => 'Latn', 'native' => 'ewondo', 'regional' => ''], 53 | 'ee' => ['name' => 'Ewe', 'script' => 'Latn', 'native' => 'eʋegbe', 'regional' => ''], 54 | 'fil' => ['name' => 'Filipino', 'script' => 'Latn', 'native' => 'Filipino', 'regional' => 'fil_PH'], 55 | 'fr' => ['name' => 'French', 'script' => 'Latn', 'native' => 'français', 'regional' => 'fr_FR'], 56 | 'fr-CA' => ['name' => 'Canadian French', 'script' => 'Latn', 'native' => 'français canadien', 'regional' => 'fr_CA'], 57 | 'fy' => ['name' => 'Western Frisian', 'script' => 'Latn', 'native' => 'frysk', 'regional' => 'fy_DE'], 58 | 'fur' => ['name' => 'Friulian', 'script' => 'Latn', 'native' => 'furlan', 'regional' => 'fur_IT'], 59 | 'fo' => ['name' => 'Faroese', 'script' => 'Latn', 'native' => 'føroyskt', 'regional' => 'fo_FO'], 60 | 'gaa' => ['name' => 'Ga', 'script' => 'Latn', 'native' => 'Ga', 'regional' => ''], 61 | 'ga' => ['name' => 'Irish', 'script' => 'Latn', 'native' => 'Gaeilge', 'regional' => 'ga_IE'], 62 | 'gv' => ['name' => 'Manx', 'script' => 'Latn', 'native' => 'Gaelg', 'regional' => 'gv_GB'], 63 | 'sm' => ['name' => 'Samoan', 'script' => 'Latn', 'native' => 'Gagana fa’a Sāmoa', 'regional' => ''], 64 | 'gl' => ['name' => 'Galician', 'script' => 'Latn', 'native' => 'galego', 'regional' => 'gl_ES'], 65 | 'ki' => ['name' => 'Kikuyu', 'script' => 'Latn', 'native' => 'Gikuyu', 'regional' => ''], 66 | 'gd' => ['name' => 'Scottish Gaelic', 'script' => 'Latn', 'native' => 'Gàidhlig', 'regional' => 'gd_GB'], 67 | 'ha' => ['name' => 'Hausa', 'script' => 'Latn', 'native' => 'Hausa', 'regional' => 'ha_NG'], 68 | 'bez' => ['name' => 'Bena', 'script' => 'Latn', 'native' => 'Hibena', 'regional' => ''], 69 | 'ho' => ['name' => 'Hiri Motu', 'script' => 'Latn', 'native' => 'Hiri Motu', 'regional' => ''], 70 | 'hr' => ['name' => 'Croatian', 'script' => 'Latn', 'native' => 'hrvatski', 'regional' => 'hr_HR'], 71 | 'bem' => ['name' => 'Bemba', 'script' => 'Latn', 'native' => 'Ichibemba', 'regional' => 'bem_ZM'], 72 | 'io' => ['name' => 'Ido', 'script' => 'Latn', 'native' => 'Ido', 'regional' => ''], 73 | 'ig' => ['name' => 'Igbo', 'script' => 'Latn', 'native' => 'Igbo', 'regional' => 'ig_NG'], 74 | 'rn' => ['name' => 'Rundi', 'script' => 'Latn', 'native' => 'Ikirundi', 'regional' => ''], 75 | 'ia' => ['name' => 'Interlingua', 'script' => 'Latn', 'native' => 'interlingua', 'regional' => 'ia_FR'], 76 | 'iu-Latn' => ['name' => 'Inuktitut (Latin)', 'script' => 'Latn', 'native' => 'Inuktitut', 'regional' => 'iu_CA'], 77 | 'sbp' => ['name' => 'Sileibi', 'script' => 'Latn', 'native' => 'Ishisangu', 'regional' => ''], 78 | 'nd' => ['name' => 'North Ndebele', 'script' => 'Latn', 'native' => 'isiNdebele', 'regional' => ''], 79 | 'nr' => ['name' => 'South Ndebele', 'script' => 'Latn', 'native' => 'isiNdebele', 'regional' => 'nr_ZA'], 80 | 'xh' => ['name' => 'Xhosa', 'script' => 'Latn', 'native' => 'isiXhosa', 'regional' => 'xh_ZA'], 81 | 'zu' => ['name' => 'Zulu', 'script' => 'Latn', 'native' => 'isiZulu', 'regional' => 'zu_ZA'], 82 | 'it' => ['name' => 'Italian', 'script' => 'Latn', 'native' => 'italiano', 'regional' => 'it_IT'], 83 | 'ik' => ['name' => 'Inupiaq', 'script' => 'Latn', 'native' => 'Iñupiaq', 'regional' => 'ik_CA'], 84 | 'dyo' => ['name' => 'Jola-Fonyi', 'script' => 'Latn', 'native' => 'joola', 'regional' => ''], 85 | 'kea' => ['name' => 'Kabuverdianu', 'script' => 'Latn', 'native' => 'kabuverdianu', 'regional' => ''], 86 | 'kaj' => ['name' => 'Jju', 'script' => 'Latn', 'native' => 'Kaje', 'regional' => ''], 87 | 'mh' => ['name' => 'Marshallese', 'script' => 'Latn', 'native' => 'Kajin M̧ajeļ', 'regional' => 'mh_MH'], 88 | 'kl' => ['name' => 'Kalaallisut', 'script' => 'Latn', 'native' => 'kalaallisut', 'regional' => 'kl_GL'], 89 | 'kln' => ['name' => 'Kalenjin', 'script' => 'Latn', 'native' => 'Kalenjin', 'regional' => ''], 90 | 'kr' => ['name' => 'Kanuri', 'script' => 'Latn', 'native' => 'Kanuri', 'regional' => ''], 91 | 'kcg' => ['name' => 'Tyap', 'script' => 'Latn', 'native' => 'Katab', 'regional' => ''], 92 | 'kw' => ['name' => 'Cornish', 'script' => 'Latn', 'native' => 'kernewek', 'regional' => 'kw_GB'], 93 | 'naq' => ['name' => 'Nama', 'script' => 'Latn', 'native' => 'Khoekhoegowab', 'regional' => ''], 94 | 'rof' => ['name' => 'Rombo', 'script' => 'Latn', 'native' => 'Kihorombo', 'regional' => ''], 95 | 'kam' => ['name' => 'Kamba', 'script' => 'Latn', 'native' => 'Kikamba', 'regional' => ''], 96 | 'kg' => ['name' => 'Kongo', 'script' => 'Latn', 'native' => 'Kikongo', 'regional' => ''], 97 | 'jmc' => ['name' => 'Machame', 'script' => 'Latn', 'native' => 'Kimachame', 'regional' => ''], 98 | 'rw' => ['name' => 'Kinyarwanda', 'script' => 'Latn', 'native' => 'Kinyarwanda', 'regional' => 'rw_RW'], 99 | 'asa' => ['name' => 'Kipare', 'script' => 'Latn', 'native' => 'Kipare', 'regional' => ''], 100 | 'rwk' => ['name' => 'Rwa', 'script' => 'Latn', 'native' => 'Kiruwa', 'regional' => ''], 101 | 'saq' => ['name' => 'Samburu', 'script' => 'Latn', 'native' => 'Kisampur', 'regional' => ''], 102 | 'ksb' => ['name' => 'Shambala', 'script' => 'Latn', 'native' => 'Kishambaa', 'regional' => ''], 103 | 'swc' => ['name' => 'Congo Swahili', 'script' => 'Latn', 'native' => 'Kiswahili ya Kongo', 'regional' => ''], 104 | 'sw' => ['name' => 'Swahili', 'script' => 'Latn', 'native' => 'Kiswahili', 'regional' => 'sw_KE'], 105 | 'dav' => ['name' => 'Dawida', 'script' => 'Latn', 'native' => 'Kitaita', 'regional' => ''], 106 | 'teo' => ['name' => 'Teso', 'script' => 'Latn', 'native' => 'Kiteso', 'regional' => ''], 107 | 'khq' => ['name' => 'Koyra Chiini', 'script' => 'Latn', 'native' => 'Koyra ciini', 'regional' => ''], 108 | 'ses' => ['name' => 'Songhay', 'script' => 'Latn', 'native' => 'Koyraboro senni', 'regional' => ''], 109 | 'mfe' => ['name' => 'Morisyen', 'script' => 'Latn', 'native' => 'kreol morisien', 'regional' => ''], 110 | 'ht' => ['name' => 'Haitian', 'script' => 'Latn', 'native' => 'Kreyòl ayisyen', 'regional' => 'ht_HT'], 111 | 'kj' => ['name' => 'Kuanyama', 'script' => 'Latn', 'native' => 'Kwanyama', 'regional' => ''], 112 | 'ksh' => ['name' => 'Kölsch', 'script' => 'Latn', 'native' => 'Kölsch', 'regional' => ''], 113 | 'ebu' => ['name' => 'Kiembu', 'script' => 'Latn', 'native' => 'Kĩembu', 'regional' => ''], 114 | 'mer' => ['name' => 'Kimîîru', 'script' => 'Latn', 'native' => 'Kĩmĩrũ', 'regional' => ''], 115 | 'lag' => ['name' => 'Langi', 'script' => 'Latn', 'native' => 'Kɨlaangi', 'regional' => ''], 116 | 'lah' => ['name' => 'Lahnda', 'script' => 'Latn', 'native' => 'Lahnda', 'regional' => ''], 117 | 'la' => ['name' => 'Latin', 'script' => 'Latn', 'native' => 'latine', 'regional' => ''], 118 | 'lv' => ['name' => 'Latvian', 'script' => 'Latn', 'native' => 'latviešu', 'regional' => 'lv_LV'], 119 | 'to' => ['name' => 'Tongan', 'script' => 'Latn', 'native' => 'lea fakatonga', 'regional' => ''], 120 | 'lt' => ['name' => 'Lithuanian', 'script' => 'Latn', 'native' => 'lietuvių', 'regional' => 'lt_LT'], 121 | 'li' => ['name' => 'Limburgish', 'script' => 'Latn', 'native' => 'Limburgs', 'regional' => 'li_BE'], 122 | 'ln' => ['name' => 'Lingala', 'script' => 'Latn', 'native' => 'lingála', 'regional' => ''], 123 | 'lg' => ['name' => 'Ganda', 'script' => 'Latn', 'native' => 'Luganda', 'regional' => 'lg_UG'], 124 | 'luy' => ['name' => 'Oluluyia', 'script' => 'Latn', 'native' => 'Luluhia', 'regional' => ''], 125 | 'lb' => ['name' => 'Luxembourgish', 'script' => 'Latn', 'native' => 'Lëtzebuergesch', 'regional' => 'lb_LU'], 126 | 'hu' => ['name' => 'Hungarian', 'script' => 'Latn', 'native' => 'magyar', 'regional' => 'hu_HU'], 127 | 'mgh' => ['name' => 'Makhuwa-Meetto', 'script' => 'Latn', 'native' => 'Makua', 'regional' => ''], 128 | 'mg' => ['name' => 'Malagasy', 'script' => 'Latn', 'native' => 'Malagasy', 'regional' => 'mg_MG'], 129 | 'mt' => ['name' => 'Maltese', 'script' => 'Latn', 'native' => 'Malti', 'regional' => 'mt_MT'], 130 | 'mtr' => ['name' => 'Mewari', 'script' => 'Latn', 'native' => 'Mewari', 'regional' => ''], 131 | 'mua' => ['name' => 'Mundang', 'script' => 'Latn', 'native' => 'Mundang', 'regional' => ''], 132 | 'mi' => ['name' => 'Māori', 'script' => 'Latn', 'native' => 'Māori', 'regional' => 'mi_NZ'], 133 | 'nl' => ['name' => 'Dutch', 'script' => 'Latn', 'native' => 'Nederlands', 'regional' => 'nl_NL'], 134 | 'nmg' => ['name' => 'Kwasio', 'script' => 'Latn', 'native' => 'ngumba', 'regional' => ''], 135 | 'yav' => ['name' => 'Yangben', 'script' => 'Latn', 'native' => 'nuasue', 'regional' => ''], 136 | 'nn' => ['name' => 'Norwegian Nynorsk', 'script' => 'Latn', 'native' => 'nynorsk', 'regional' => 'nn_NO'], 137 | 'oc' => ['name' => 'Occitan', 'script' => 'Latn', 'native' => 'occitan', 'regional' => 'oc_FR'], 138 | 'ang' => ['name' => 'Old English', 'script' => 'Runr', 'native' => 'Old English', 'regional' => ''], 139 | 'xog' => ['name' => 'Soga', 'script' => 'Latn', 'native' => 'Olusoga', 'regional' => ''], 140 | 'om' => ['name' => 'Oromo', 'script' => 'Latn', 'native' => 'Oromoo', 'regional' => 'om_ET'], 141 | 'ng' => ['name' => 'Ndonga', 'script' => 'Latn', 'native' => 'OshiNdonga', 'regional' => ''], 142 | 'hz' => ['name' => 'Herero', 'script' => 'Latn', 'native' => 'Otjiherero', 'regional' => ''], 143 | 'uz-Latn' => ['name' => 'Uzbek (Latin)', 'script' => 'Latn', 'native' => 'oʼzbekcha', 'regional' => 'uz_UZ'], 144 | 'nds' => ['name' => 'Low German', 'script' => 'Latn', 'native' => 'Plattdüütsch', 'regional' => 'nds_DE'], 145 | 'pl' => ['name' => 'Polish', 'script' => 'Latn', 'native' => 'polski', 'regional' => 'pl_PL'], 146 | 'pt' => ['name' => 'Portuguese', 'script' => 'Latn', 'native' => 'português', 'regional' => 'pt_PT'], 147 | 'pt-BR' => ['name' => 'Brazilian Portuguese', 'script' => 'Latn', 'native' => 'português do Brasil', 'regional' => 'pt_BR'], 148 | 'ff' => ['name' => 'Fulah', 'script' => 'Latn', 'native' => 'Pulaar', 'regional' => 'ff_SN'], 149 | 'pi' => ['name' => 'Pahari-Potwari', 'script' => 'Latn', 'native' => 'Pāli', 'regional' => ''], 150 | 'aa' => ['name' => 'Afar', 'script' => 'Latn', 'native' => 'Qafar', 'regional' => 'aa_ER'], 151 | 'ty' => ['name' => 'Tahitian', 'script' => 'Latn', 'native' => 'Reo Māohi', 'regional' => ''], 152 | 'ksf' => ['name' => 'Bafia', 'script' => 'Latn', 'native' => 'rikpa', 'regional' => ''], 153 | 'ro' => ['name' => 'Romanian', 'script' => 'Latn', 'native' => 'română', 'regional' => 'ro_RO'], 154 | 'cgg' => ['name' => 'Chiga', 'script' => 'Latn', 'native' => 'Rukiga', 'regional' => ''], 155 | 'rm' => ['name' => 'Romansh', 'script' => 'Latn', 'native' => 'rumantsch', 'regional' => ''], 156 | 'qu' => ['name' => 'Quechua', 'script' => 'Latn', 'native' => 'Runa Simi', 'regional' => ''], 157 | 'nyn' => ['name' => 'Nyankole', 'script' => 'Latn', 'native' => 'Runyankore', 'regional' => ''], 158 | 'ssy' => ['name' => 'Saho', 'script' => 'Latn', 'native' => 'Saho', 'regional' => ''], 159 | 'sc' => ['name' => 'Sardinian', 'script' => 'Latn', 'native' => 'sardu', 'regional' => 'sc_IT'], 160 | 'de-CH' => ['name' => 'Swiss High German', 'script' => 'Latn', 'native' => 'Schweizer Hochdeutsch', 'regional' => 'de_CH'], 161 | 'gsw' => ['name' => 'Swiss German', 'script' => 'Latn', 'native' => 'Schwiizertüütsch', 'regional' => ''], 162 | 'trv' => ['name' => 'Taroko', 'script' => 'Latn', 'native' => 'Seediq', 'regional' => ''], 163 | 'seh' => ['name' => 'Sena', 'script' => 'Latn', 'native' => 'sena', 'regional' => ''], 164 | 'nso' => ['name' => 'Northern Sotho', 'script' => 'Latn', 'native' => 'Sesotho sa Leboa', 'regional' => 'nso_ZA'], 165 | 'st' => ['name' => 'Southern Sotho', 'script' => 'Latn', 'native' => 'Sesotho', 'regional' => 'st_ZA'], 166 | 'tn' => ['name' => 'Tswana', 'script' => 'Latn', 'native' => 'Setswana', 'regional' => 'tn_ZA'], 167 | 'sq' => ['name' => 'Albanian', 'script' => 'Latn', 'native' => 'shqip', 'regional' => 'sq_AL'], 168 | 'sid' => ['name' => 'Sidamo', 'script' => 'Latn', 'native' => 'Sidaamu Afo', 'regional' => 'sid_ET'], 169 | 'ss' => ['name' => 'Swati', 'script' => 'Latn', 'native' => 'Siswati', 'regional' => 'ss_ZA'], 170 | 'sk' => ['name' => 'Slovak', 'script' => 'Latn', 'native' => 'slovenčina', 'regional' => 'sk_SK'], 171 | 'sl' => ['name' => 'Slovene', 'script' => 'Latn', 'native' => 'slovenščina', 'regional' => 'sl_SI'], 172 | 'so' => ['name' => 'Somali', 'script' => 'Latn', 'native' => 'Soomaali', 'regional' => 'so_SO'], 173 | 'sr-Latn' => ['name' => 'Serbian (Latin)', 'script' => 'Latn', 'native' => 'Srpski', 'regional' => 'sr_RS'], 174 | 'sh' => ['name' => 'Serbo-Croatian', 'script' => 'Latn', 'native' => 'srpskohrvatski', 'regional' => ''], 175 | 'fi' => ['name' => 'Finnish', 'script' => 'Latn', 'native' => 'suomi', 'regional' => 'fi_FI'], 176 | 'sv' => ['name' => 'Swedish', 'script' => 'Latn', 'native' => 'svenska', 'regional' => 'sv_SE'], 177 | 'sg' => ['name' => 'Sango', 'script' => 'Latn', 'native' => 'Sängö', 'regional' => ''], 178 | 'tl' => ['name' => 'Tagalog', 'script' => 'Latn', 'native' => 'Tagalog', 'regional' => 'tl_PH'], 179 | 'tzm-Latn' => ['name' => 'Central Atlas Tamazight (Latin)', 'script' => 'Latn', 'native' => 'Tamazight', 'regional' => ''], 180 | 'kab' => ['name' => 'Kabyle', 'script' => 'Latn', 'native' => 'Taqbaylit', 'regional' => 'kab_DZ'], 181 | 'twq' => ['name' => 'Tasawaq', 'script' => 'Latn', 'native' => 'Tasawaq senni', 'regional' => ''], 182 | 'shi' => ['name' => 'Tachelhit (Latin)', 'script' => 'Latn', 'native' => 'Tashelhit', 'regional' => ''], 183 | 'nus' => ['name' => 'Nuer', 'script' => 'Latn', 'native' => 'Thok Nath', 'regional' => ''], 184 | 'vi' => ['name' => 'Vietnamese', 'script' => 'Latn', 'native' => 'Tiếng Việt', 'regional' => 'vi_VN'], 185 | 'tg-Latn' => ['name' => 'Tajik (Latin)', 'script' => 'Latn', 'native' => 'tojikī', 'regional' => 'tg_TJ'], 186 | 'lu' => ['name' => 'Luba-Katanga', 'script' => 'Latn', 'native' => 'Tshiluba', 'regional' => 've_ZA'], 187 | 've' => ['name' => 'Venda', 'script' => 'Latn', 'native' => 'Tshivenḓa', 'regional' => ''], 188 | 'tw' => ['name' => 'Twi', 'script' => 'Latn', 'native' => 'Twi', 'regional' => ''], 189 | 'tr' => ['name' => 'Turkish', 'script' => 'Latn', 'native' => 'Türkçe', 'regional' => 'tr_TR'], 190 | 'ale' => ['name' => 'Aleut', 'script' => 'Latn', 'native' => 'Unangax tunuu', 'regional' => ''], 191 | 'ca-valencia' => ['name' => 'Valencian', 'script' => 'Latn', 'native' => 'valencià', 'regional' => ''], 192 | 'vai-Latn' => ['name' => 'Vai (Latin)', 'script' => 'Latn', 'native' => 'Viyamíĩ', 'regional' => ''], 193 | 'vo' => ['name' => 'Volapük', 'script' => 'Latn', 'native' => 'Volapük', 'regional' => ''], 194 | 'fj' => ['name' => 'Fijian', 'script' => 'Latn', 'native' => 'vosa Vakaviti', 'regional' => ''], 195 | 'wa' => ['name' => 'Walloon', 'script' => 'Latn', 'native' => 'Walon', 'regional' => 'wa_BE'], 196 | 'wae' => ['name' => 'Walser', 'script' => 'Latn', 'native' => 'Walser', 'regional' => 'wae_CH'], 197 | 'wen' => ['name' => 'Sorbian', 'script' => 'Latn', 'native' => 'Wendic', 'regional' => ''], 198 | 'wo' => ['name' => 'Wolof', 'script' => 'Latn', 'native' => 'Wolof', 'regional' => 'wo_SN'], 199 | 'ts' => ['name' => 'Tsonga', 'script' => 'Latn', 'native' => 'Xitsonga', 'regional' => 'ts_ZA'], 200 | 'dje' => ['name' => 'Zarma', 'script' => 'Latn', 'native' => 'Zarmaciine', 'regional' => ''], 201 | 'yo' => ['name' => 'Yoruba', 'script' => 'Latn', 'native' => 'Èdè Yorùbá', 'regional' => 'yo_NG'], 202 | 'de-AT' => ['name' => 'Austrian German', 'script' => 'Latn', 'native' => 'Österreichisches Deutsch', 'regional' => 'de_AT'], 203 | 'is' => ['name' => 'Icelandic', 'script' => 'Latn', 'native' => 'íslenska', 'regional' => 'is_IS'], 204 | 'cs' => ['name' => 'Czech', 'script' => 'Latn', 'native' => 'čeština', 'regional' => 'cs_CZ'], 205 | 'bas' => ['name' => 'Basa', 'script' => 'Latn', 'native' => 'Ɓàsàa', 'regional' => ''], 206 | 'mas' => ['name' => 'Masai', 'script' => 'Latn', 'native' => 'ɔl-Maa', 'regional' => ''], 207 | 'haw' => ['name' => 'Hawaiian', 'script' => 'Latn', 'native' => 'ʻŌlelo Hawaiʻi', 'regional' => ''], 208 | 'el' => ['name' => 'Greek', 'script' => 'Grek', 'native' => 'Ελληνικά', 'regional' => 'el_GR'], 209 | 'uz' => ['name' => 'Uzbek (Cyrillic)', 'script' => 'Cyrl', 'native' => 'Ўзбек', 'regional' => 'uz_UZ'], 210 | 'az-Cyrl' => ['name' => 'Azerbaijani (Cyrillic)', 'script' => 'Cyrl', 'native' => 'Азәрбајҹан', 'regional' => 'uz_UZ'], 211 | 'ab' => ['name' => 'Abkhazian', 'script' => 'Cyrl', 'native' => 'Аҧсуа', 'regional' => ''], 212 | 'os' => ['name' => 'Ossetic', 'script' => 'Cyrl', 'native' => 'Ирон', 'regional' => 'os_RU'], 213 | 'ky' => ['name' => 'Kyrgyz', 'script' => 'Cyrl', 'native' => 'Кыргыз', 'regional' => 'ky_KG'], 214 | 'sr' => ['name' => 'Serbian (Cyrillic)', 'script' => 'Cyrl', 'native' => 'Српски', 'regional' => 'sr_RS'], 215 | 'av' => ['name' => 'Avaric', 'script' => 'Cyrl', 'native' => 'авар мацӀ', 'regional' => ''], 216 | 'ady' => ['name' => 'Adyghe', 'script' => 'Cyrl', 'native' => 'адыгэбзэ', 'regional' => ''], 217 | 'ba' => ['name' => 'Bashkir', 'script' => 'Cyrl', 'native' => 'башҡорт теле', 'regional' => ''], 218 | 'be' => ['name' => 'Belarusian', 'script' => 'Cyrl', 'native' => 'беларуская', 'regional' => 'be_BY'], 219 | 'bg' => ['name' => 'Bulgarian', 'script' => 'Cyrl', 'native' => 'български', 'regional' => 'bg_BG'], 220 | 'kv' => ['name' => 'Komi', 'script' => 'Cyrl', 'native' => 'коми кыв', 'regional' => ''], 221 | 'mk' => ['name' => 'Macedonian', 'script' => 'Cyrl', 'native' => 'македонски', 'regional' => 'mk_MK'], 222 | 'mn' => ['name' => 'Mongolian (Cyrillic)', 'script' => 'Cyrl', 'native' => 'монгол', 'regional' => 'mn_MN'], 223 | 'ce' => ['name' => 'Chechen', 'script' => 'Cyrl', 'native' => 'нохчийн мотт', 'regional' => 'ce_RU'], 224 | 'ru' => ['name' => 'Russian', 'script' => 'Cyrl', 'native' => 'русский', 'regional' => 'ru_RU'], 225 | 'sah' => ['name' => 'Yakut', 'script' => 'Cyrl', 'native' => 'саха тыла', 'regional' => ''], 226 | 'tt' => ['name' => 'Tatar', 'script' => 'Cyrl', 'native' => 'татар теле', 'regional' => 'tt_RU'], 227 | 'tg' => ['name' => 'Tajik (Cyrillic)', 'script' => 'Cyrl', 'native' => 'тоҷикӣ', 'regional' => 'tg_TJ'], 228 | 'tk' => ['name' => 'Turkmen', 'script' => 'Cyrl', 'native' => 'түркменче', 'regional' => 'tk_TM'], 229 | 'uk' => ['name' => 'Ukrainian', 'script' => 'Cyrl', 'native' => 'українська', 'regional' => 'uk_UA'], 230 | 'cv' => ['name' => 'Chuvash', 'script' => 'Cyrl', 'native' => 'чӑваш чӗлхи', 'regional' => 'cv_RU'], 231 | 'cu' => ['name' => 'Church Slavic', 'script' => 'Cyrl', 'native' => 'ѩзыкъ словѣньскъ', 'regional' => ''], 232 | 'kk' => ['name' => 'Kazakh', 'script' => 'Cyrl', 'native' => 'қазақ тілі', 'regional' => 'kk_KZ'], 233 | 'hy' => ['name' => 'Armenian', 'script' => 'Armn', 'native' => 'Հայերեն', 'regional' => 'hy_AM'], 234 | 'yi' => ['name' => 'Yiddish', 'script' => 'Hebr', 'native' => 'ייִדיש', 'regional' => 'yi_US'], 235 | 'he' => ['name' => 'Hebrew', 'script' => 'Hebr', 'native' => 'עברית', 'regional' => 'he_IL'], 236 | 'ug' => ['name' => 'Uyghur', 'script' => 'Arab', 'native' => 'ئۇيغۇرچە', 'regional' => 'ug_CN'], 237 | 'ur' => ['name' => 'Urdu', 'script' => 'Arab', 'native' => 'اردو', 'regional' => 'ur_PK'], 238 | 'ar' => ['name' => 'Arabic', 'script' => 'Arab', 'native' => 'العربية', 'regional' => 'ar_AE'], 239 | 'uz-Arab' => ['name' => 'Uzbek (Arabic)', 'script' => 'Arab', 'native' => 'اۉزبېک', 'regional' => ''], 240 | 'tg-Arab' => ['name' => 'Tajik (Arabic)', 'script' => 'Arab', 'native' => 'تاجیکی', 'regional' => 'tg_TJ'], 241 | 'sd' => ['name' => 'Sindhi', 'script' => 'Arab', 'native' => 'سنڌي', 'regional' => 'sd_IN'], 242 | 'fa' => ['name' => 'Persian', 'script' => 'Arab', 'native' => 'فارسی', 'regional' => 'fa_IR'], 243 | 'pa-Arab' => ['name' => 'Punjabi (Arabic)', 'script' => 'Arab', 'native' => 'پنجاب', 'regional' => 'pa_IN'], 244 | 'ps' => ['name' => 'Pashto', 'script' => 'Arab', 'native' => 'پښتو', 'regional' => 'ps_AF'], 245 | 'ks' => ['name' => 'Kashmiri (Arabic)', 'script' => 'Arab', 'native' => 'کأشُر', 'regional' => 'ks_IN'], 246 | 'ku' => ['name' => 'Kurdish', 'script' => 'Arab', 'native' => 'کوردی', 'regional' => 'ku_TR'], 247 | 'dv' => ['name' => 'Divehi', 'script' => 'Thaa', 'native' => 'ދިވެހިބަސް', 'regional' => 'dv_MV'], 248 | 'ks-Deva' => ['name' => 'Kashmiri (Devaganari)', 'script' => 'Deva', 'native' => 'कॉशुर', 'regional' => 'ks_IN'], 249 | 'kok' => ['name' => 'Konkani', 'script' => 'Deva', 'native' => 'कोंकणी', 'regional' => 'kok_IN'], 250 | 'doi' => ['name' => 'Dogri', 'script' => 'Deva', 'native' => 'डोगरी', 'regional' => 'doi_IN'], 251 | 'ne' => ['name' => 'Nepali', 'script' => 'Deva', 'native' => 'नेपाली', 'regional' => ''], 252 | 'pra' => ['name' => 'Prakrit', 'script' => 'Deva', 'native' => 'प्राकृत', 'regional' => ''], 253 | 'brx' => ['name' => 'Bodo', 'script' => 'Deva', 'native' => 'बड़ो', 'regional' => 'brx_IN'], 254 | 'bra' => ['name' => 'Braj', 'script' => 'Deva', 'native' => 'ब्रज भाषा', 'regional' => ''], 255 | 'mr' => ['name' => 'Marathi', 'script' => 'Deva', 'native' => 'मराठी', 'regional' => 'mr_IN'], 256 | 'mai' => ['name' => 'Maithili', 'script' => 'Tirh', 'native' => 'मैथिली', 'regional' => 'mai_IN'], 257 | 'raj' => ['name' => 'Rajasthani', 'script' => 'Deva', 'native' => 'राजस्थानी', 'regional' => ''], 258 | 'sa' => ['name' => 'Sanskrit', 'script' => 'Deva', 'native' => 'संस्कृतम्', 'regional' => 'sa_IN'], 259 | 'hi' => ['name' => 'Hindi', 'script' => 'Deva', 'native' => 'हिन्दी', 'regional' => 'hi_IN'], 260 | 'as' => ['name' => 'Assamese', 'script' => 'Beng', 'native' => 'অসমীয়া', 'regional' => 'as_IN'], 261 | 'bn' => ['name' => 'Bengali', 'script' => 'Beng', 'native' => 'বাংলা', 'regional' => 'bn_BD'], 262 | 'mni' => ['name' => 'Manipuri', 'script' => 'Beng', 'native' => 'মৈতৈ', 'regional' => 'mni_IN'], 263 | 'pa' => ['name' => 'Punjabi (Gurmukhi)', 'script' => 'Guru', 'native' => 'ਪੰਜਾਬੀ', 'regional' => 'pa_IN'], 264 | 'gu' => ['name' => 'Gujarati', 'script' => 'Gujr', 'native' => 'ગુજરાતી', 'regional' => 'gu_IN'], 265 | 'or' => ['name' => 'Oriya', 'script' => 'Orya', 'native' => 'ଓଡ଼ିଆ', 'regional' => 'or_IN'], 266 | 'ta' => ['name' => 'Tamil', 'script' => 'Taml', 'native' => 'தமிழ்', 'regional' => 'ta_IN'], 267 | 'te' => ['name' => 'Telugu', 'script' => 'Telu', 'native' => 'తెలుగు', 'regional' => 'te_IN'], 268 | 'kn' => ['name' => 'Kannada', 'script' => 'Knda', 'native' => 'ಕನ್ನಡ', 'regional' => 'kn_IN'], 269 | 'ml' => ['name' => 'Malayalam', 'script' => 'Mlym', 'native' => 'മലയാളം', 'regional' => 'ml_IN'], 270 | 'si' => ['name' => 'Sinhala', 'script' => 'Sinh', 'native' => 'සිංහල', 'regional' => 'si_LK'], 271 | 'th' => ['name' => 'Thai', 'script' => 'Thai', 'native' => 'ไทย', 'regional' => 'th_TH'], 272 | 'lo' => ['name' => 'Lao', 'script' => 'Laoo', 'native' => 'ລາວ', 'regional' => 'lo_LA'], 273 | 'bo' => ['name' => 'Tibetan', 'script' => 'Tibt', 'native' => 'པོད་སྐད་', 'regional' => 'bo_IN'], 274 | 'dz' => ['name' => 'Dzongkha', 'script' => 'Tibt', 'native' => 'རྫོང་ཁ', 'regional' => 'dz_BT'], 275 | 'my' => ['name' => 'Burmese', 'script' => 'Mymr', 'native' => 'မြန်မာဘာသာ', 'regional' => 'my_MM'], 276 | 'ka' => ['name' => 'Georgian', 'script' => 'Geor', 'native' => 'ქართული', 'regional' => 'ka_GE'], 277 | 'byn' => ['name' => 'Blin', 'script' => 'Ethi', 'native' => 'ብሊን', 'regional' => 'byn_ER'], 278 | 'tig' => ['name' => 'Tigre', 'script' => 'Ethi', 'native' => 'ትግረ', 'regional' => 'tig_ER'], 279 | 'ti' => ['name' => 'Tigrinya', 'script' => 'Ethi', 'native' => 'ትግርኛ', 'regional' => 'ti_ET'], 280 | 'am' => ['name' => 'Amharic', 'script' => 'Ethi', 'native' => 'አማርኛ', 'regional' => 'am_ET'], 281 | 'wal' => ['name' => 'Wolaytta', 'script' => 'Ethi', 'native' => 'ወላይታቱ', 'regional' => 'wal_ET'], 282 | 'chr' => ['name' => 'Cherokee', 'script' => 'Cher', 'native' => 'ᏣᎳᎩ', 'regional' => ''], 283 | 'iu' => ['name' => 'Inuktitut (Canadian Aboriginal Syllabics)', 'script' => 'Cans', 'native' => 'ᐃᓄᒃᑎᑐᑦ', 'regional' => 'iu_CA'], 284 | 'oj' => ['name' => 'Ojibwa', 'script' => 'Cans', 'native' => 'ᐊᓂᔑᓈᐯᒧᐎᓐ', 'regional' => ''], 285 | 'cr' => ['name' => 'Cree', 'script' => 'Cans', 'native' => 'ᓀᐦᐃᔭᐍᐏᐣ', 'regional' => ''], 286 | 'km' => ['name' => 'Khmer', 'script' => 'Khmr', 'native' => 'ភាសាខ្មែរ', 'regional' => 'km_KH'], 287 | 'mn-Mong' => ['name' => 'Mongolian (Mongolian)', 'script' => 'Mong', 'native' => 'ᠮᠣᠨᠭᠭᠣᠯ ᠬᠡᠯᠡ', 'regional' => 'mn_MN'], 288 | 'shi-Tfng' => ['name' => 'Tachelhit (Tifinagh)', 'script' => 'Tfng', 'native' => 'ⵜⴰⵎⴰⵣⵉⵖⵜ', 'regional' => ''], 289 | 'tzm' => ['name' => 'Central Atlas Tamazight (Tifinagh)','script' => 'Tfng', 'native' => 'ⵜⴰⵎⴰⵣⵉⵖⵜ', 'regional' => ''], 290 | 'yue' => ['name' => 'Yue', 'script' => 'Hant', 'native' => '廣州話', 'regional' => 'yue_HK'], 291 | 'ja' => ['name' => 'Japanese', 'script' => 'Jpan', 'native' => '日本語', 'regional' => 'ja_JP'], 292 | 'zh' => ['name' => 'Chinese (Simplified)', 'script' => 'Hans', 'native' => '简体中文', 'regional' => 'zh_CN'], 293 | 'zh-Hant' => ['name' => 'Chinese (Traditional)', 'script' => 'Hant', 'native' => '繁體中文', 'regional' => 'zh_CN'], 294 | 'ii' => ['name' => 'Sichuan Yi', 'script' => 'Yiii', 'native' => 'ꆈꌠꉙ', 'regional' => ''], 295 | 'vai' => ['name' => 'Vai (Vai)', 'script' => 'Vaii', 'native' => 'ꕙꔤ', 'regional' => ''], 296 | 'jv-Java' => ['name' => 'Javanese (Javanese)', 'script' => 'Java', 'native' => 'ꦧꦱꦗꦮ', 'regional' => ''], 297 | 'ko' => ['name' => 'Korean', 'script' => 'Hang', 'native' => '한국어', 'regional' => 'ko_KR'], 298 | ], 299 | 300 | // Negotiate for the user locale using the Accept-Language header if it's not defined in the URL? 301 | // If false, system will take app.php locale attribute 302 | 'useAcceptLanguageHeader' => true, 303 | 304 | // If LaravelLocalizationRedirectFilter is active and hideDefaultLocaleInURL 305 | // is true, the url would not have the default application language 306 | // 307 | // IMPORTANT - When hideDefaultLocaleInURL is set to true, the unlocalized root is treated as the applications default locale "app.locale". 308 | // Because of this language negotiation using the Accept-Language header will NEVER occur when hideDefaultLocaleInURL is true. 309 | // 310 | 'hideDefaultLocaleInURL' => false, 311 | 312 | // If you want to display the locales in particular order in the language selector you should write the order here. 313 | //CAUTION: Please consider using the appropriate locale code otherwise it will not work 314 | //Example: 'localesOrder' => ['es','en'], 315 | 'localesOrder' => [], 316 | 317 | // If you want to use custom lang url segments like 'at' instead of 'de-AT', you can use the mapping to tallow the LanguageNegotiator to assign the descired locales based on HTTP Accept Language Header. For example you want ot use 'at', so map HTTP Accept Language Header 'de-AT' to 'at' (['de-AT' => 'at']). 318 | 'localesMapping' => [], 319 | 320 | // URLs which should not be processed, e.g. '/nova', '/nova/*', '/nova-api/*' or specific application URLs 321 | // Defaults to [] 322 | 'urlsIgnored' => [], 323 | ]; 324 | -------------------------------------------------------------------------------- /tests/lang/en/routes.php: -------------------------------------------------------------------------------- 1 | 'about', 5 | 'view' => 'view/{id}', 6 | 'view_project' => 'view/{id}/project/{project_id?}', 7 | 'manage' => 'manage/{file_id?}', 8 | 'hello' => 'Hello world', 9 | 'test_text' => 'Test text', 10 | ]; 11 | -------------------------------------------------------------------------------- /tests/lang/es/routes.php: -------------------------------------------------------------------------------- 1 | 'acerca', 5 | 'view' => 'ver/{id}', 6 | 'view_project' => 'ver/{id}/proyecto/{project_id?}', 7 | 'manage' => 'administrar/{file_id?}', 8 | 'hello' => 'Hola mundo', 9 | 'test_text' => 'Texto de prueba', 10 | ]; 11 | --------------------------------------------------------------------------------