├── .github ├── FUNDING.yml ├── SECURITY.md └── workflows │ └── run-tests.yml ├── src ├── Filters │ ├── Filter.php │ ├── CountryFilter.php │ ├── LocaleFilter.php │ ├── WeightFilter.php │ ├── LanguageFilter.php │ ├── PropertyFilter.php │ └── CombinedFilter.php ├── Laravel │ └── BrowserLocaleServiceProvider.php ├── Locale.php └── BrowserLocale.php ├── LICENSE.md ├── composer.json └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: ivanvermeyen 2 | custom: https://paypal.me/ivanvermeyen 3 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you discover any security related issues, please email ivan@codezero.be instead of using the issue tracker. 4 | -------------------------------------------------------------------------------- /src/Filters/Filter.php: -------------------------------------------------------------------------------- 1 | filterByProperty($locales, 'country'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Filters/LocaleFilter.php: -------------------------------------------------------------------------------- 1 | filterByProperty($locales, 'locale'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Filters/WeightFilter.php: -------------------------------------------------------------------------------- 1 | filterByProperty($locales, 'weight'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Filters/LanguageFilter.php: -------------------------------------------------------------------------------- 1 | filterByProperty($locales, 'language'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Filters/PropertyFilter.php: -------------------------------------------------------------------------------- 1 | $property) && ! in_array($locale->$property, $filtered)) { 21 | $filtered[] = $locale->$property; 22 | } 23 | } 24 | 25 | return $filtered; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Laravel/BrowserLocaleServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerBrowserLocale(); 19 | } 20 | 21 | /** 22 | * Register BrowserLocale. 23 | * 24 | * @return void 25 | */ 26 | protected function registerBrowserLocale() 27 | { 28 | $this->app->bind(BrowserLocale::class, function () { 29 | return new BrowserLocale( 30 | Request::server('HTTP_ACCEPT_LANGUAGE') 31 | ); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Locale.php: -------------------------------------------------------------------------------- 1 | locale = $locale; 47 | $this->language = $language; 48 | $this->country = $country; 49 | $this->weight = $weight; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) Ivan Vermeyen (ivan@codezero.be) 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codezero/browser-locale", 3 | "description": "Get the most preferred locales from your visitor's browser.", 4 | "keywords": [ 5 | "website", 6 | "browser", 7 | "locale", 8 | "language", 9 | "country", 10 | "php", 11 | "detect" 12 | ], 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Ivan Vermeyen", 17 | "email": "ivan@codezero.be" 18 | } 19 | ], 20 | "require": { 21 | "php": "^7.0|^8.0" 22 | }, 23 | "require-dev": { 24 | "mockery/mockery": "^1.3.3", 25 | "phpunit/phpunit": "^6.0|^7.0|^8.0|^9.0" 26 | }, 27 | "scripts": { 28 | "test": "phpunit" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "CodeZero\\BrowserLocale\\": "src/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "CodeZero\\BrowserLocale\\Tests\\": "tests" 38 | } 39 | }, 40 | "extra": { 41 | "laravel": { 42 | "providers": [ 43 | "CodeZero\\BrowserLocale\\Laravel\\BrowserLocaleServiceProvider" 44 | ] 45 | } 46 | }, 47 | "config": { 48 | "preferred-install": "dist", 49 | "sort-packages": true, 50 | "optimize-autoloader": true 51 | }, 52 | "minimum-stability": "stable", 53 | "prefer-stable": true 54 | } 55 | -------------------------------------------------------------------------------- /src/Filters/CombinedFilter.php: -------------------------------------------------------------------------------- 1 | filtered = []; 26 | 27 | foreach ($locales as $locale) { 28 | $this->filterLocale($locale); 29 | } 30 | 31 | return $this->filtered; 32 | } 33 | 34 | /** 35 | * Filter the given Locale. 36 | * 37 | * @param \CodeZero\BrowserLocale\Locale $locale 38 | * 39 | * @return void 40 | */ 41 | protected function filterLocale(Locale $locale) 42 | { 43 | $language = substr($locale->locale, 0, 2); 44 | 45 | $this->addLocale($locale->locale); 46 | $this->addLocale($language); 47 | } 48 | 49 | /** 50 | * Add a locale to the results array. 51 | * 52 | * @param string $locale 53 | * 54 | * @return void 55 | */ 56 | protected function addLocale($locale) 57 | { 58 | if ( ! in_array($locale, $this->filtered)) { 59 | $this->filtered[] = $locale; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | os: [ ubuntu-latest ] 12 | php: [ 7.0, 7.1, 7.2, 7.3, 7.4, 8.0, 8.1, 8.2 ] 13 | dependency-version: [ prefer-stable ] 14 | 15 | name: P${{ matrix.php }} - ${{ matrix.dependency-version }} - ${{ matrix.os }} 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v2 20 | 21 | - name: Cache dependencies 22 | uses: actions/cache@v2 23 | with: 24 | path: ~/.composer/cache/files 25 | key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 26 | 27 | - name: Setup PHP 28 | uses: shivammathur/setup-php@v2 29 | with: 30 | php-version: ${{ matrix.php }} 31 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick 32 | coverage: xdebug 33 | 34 | - name: Install dependencies 35 | run: | 36 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest 37 | 38 | - name: Execute tests 39 | run: vendor/bin/phpunit --coverage-clover=coverage.xml 40 | 41 | - if: github.event_name == 'push' 42 | name: Run Codacy Coverage Reporter 43 | uses: codacy/codacy-coverage-reporter-action@master 44 | with: 45 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 46 | coverage-reports: coverage.xml 47 | -------------------------------------------------------------------------------- /src/BrowserLocale.php: -------------------------------------------------------------------------------- 1 | locales = $this->parseHttpAcceptLanguages($httpAcceptLanguages); 24 | } 25 | 26 | /** 27 | * Get the most preferred locale. 28 | * 29 | * @return \CodeZero\BrowserLocale\Locale|null 30 | */ 31 | public function getLocale() 32 | { 33 | return $this->locales[0] ?? null; 34 | } 35 | 36 | /** 37 | * Get an array of Locale objects in descending order of preference. 38 | * 39 | * @return array 40 | */ 41 | public function getLocales() 42 | { 43 | return $this->locales; 44 | } 45 | 46 | /** 47 | * Filter the locales using the given Filter. 48 | * 49 | * @param \CodeZero\BrowserLocale\Filters\Filter $filter 50 | * 51 | * @return array 52 | */ 53 | public function filter(Filter $filter) 54 | { 55 | return $filter->filter($this->locales); 56 | } 57 | 58 | /** 59 | * Parse all HTTP Accept Languages. 60 | * 61 | * @param string $httpAcceptLanguages 62 | * 63 | * @return void 64 | */ 65 | protected function parseHttpAcceptLanguages($httpAcceptLanguages) 66 | { 67 | $acceptLanguages = $this->split($httpAcceptLanguages, ','); 68 | $locales = []; 69 | 70 | foreach ($acceptLanguages as $httpAcceptLanguage) { 71 | $locales[] = $this->makeLocale($httpAcceptLanguage); 72 | } 73 | 74 | $sortedLocales = $this->sortLocales($locales); 75 | 76 | return $sortedLocales; 77 | } 78 | 79 | /** 80 | * Convert the given HTTP Accept Language to a Locale object. 81 | * 82 | * @param string $httpAcceptLanguage 83 | * 84 | * @return \CodeZero\BrowserLocale\Locale 85 | */ 86 | protected function makeLocale($httpAcceptLanguage) 87 | { 88 | $parts = $this->split($httpAcceptLanguage, ';'); 89 | 90 | $locale = $parts[0]; 91 | $weight = $parts[1] ?? null; 92 | 93 | return new Locale( 94 | $locale, 95 | $this->getLanguage($locale), 96 | $this->getCountry($locale), 97 | $this->getWeight($weight) 98 | ); 99 | } 100 | 101 | /** 102 | * Split the given string by the delimiter. 103 | * 104 | * @param string $string 105 | * @param string $delimiter 106 | * 107 | * @return array 108 | */ 109 | protected function split($string, $delimiter) 110 | { 111 | if ( ! $string) { 112 | return []; 113 | } 114 | 115 | return explode($delimiter, trim($string)) ?: []; 116 | } 117 | 118 | /** 119 | * Get the 2-letter language code from the locale. 120 | * 121 | * @param string $locale 122 | * 123 | * @return string 124 | */ 125 | protected function getLanguage($locale) 126 | { 127 | return substr($locale, 0, 2) ?: ''; 128 | } 129 | 130 | /** 131 | * Get the 2-letter country code from the locale. 132 | * 133 | * @param string $locale 134 | * 135 | * @return string 136 | */ 137 | protected function getCountry($locale) 138 | { 139 | return substr($locale, 3, 2) ?: ''; 140 | } 141 | 142 | /** 143 | * Parse the relative quality factor and return its value. 144 | * 145 | * @param string $qualityFactor 146 | * 147 | * @return float 148 | */ 149 | protected function getWeight($qualityFactor) 150 | { 151 | $parts = $this->split($qualityFactor, '='); 152 | 153 | $weight = $parts[1] ?? 1.0; 154 | 155 | return (float) $weight; 156 | } 157 | 158 | /** 159 | * Sort the array of locales in descending order of preference. 160 | * 161 | * @param array $locales 162 | * 163 | * @return array 164 | */ 165 | protected function sortLocales($locales) 166 | { 167 | usort($locales, function ($localeA, $localeB) { 168 | if ($localeA->weight === $localeB->weight) { 169 | return 0; 170 | } 171 | 172 | return ($localeA->weight > $localeB->weight) ? -1 : 1; 173 | }); 174 | 175 | return $locales; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BrowserLocale 2 | 3 | [![GitHub release](https://img.shields.io/github/release/codezero-be/browser-locale.svg?style=flat-square)](https://github.com/codezero-be/browser-locale/releases) 4 | [![License](https://img.shields.io/packagist/l/codezero/browser-locale.svg?style=flat-square)](LICENSE.md) 5 | [![Build Status](https://img.shields.io/github/actions/workflow/status/codezero-be/browser-locale/run-tests.yml?style=flat-square&logo=github&logoColor=white&label=tests)](https://github.com/codezero-be/browser-locale/actions) 6 | [![Code Coverage](https://img.shields.io/codacy/coverage/7948f1eaee514e47b4a07f268afc630f/master?style=flat-square)](https://app.codacy.com/gh/codezero-be/browser-locale) 7 | [![Code Quality](https://img.shields.io/codacy/grade/7948f1eaee514e47b4a07f268afc630f/master?style=flat-square)](https://app.codacy.com/gh/codezero-be/browser-locale) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/codezero/browser-locale.svg?style=flat-square)](https://packagist.org/packages/codezero/browser-locale) 9 | 10 | [![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R3UQ8V) 11 | 12 | Get the most preferred locales from your visitor's browser. 13 | 14 | Every browser has a setting for preferred website locales. 15 | 16 | This can be read by PHP, usually with the `$_SERVER["HTTP_ACCEPT_LANGUAGE"]` variable. 17 | 18 | > `$_SERVER["HTTP_ACCEPT_LANGUAGE"]` will return a comma separated list of language codes. Each language code MAY have a "relative quality factor" attached ("nl;q=0.8") which determines the order of preference. For example: `nl-NL,nl;q=0.8,en-US;q=0.6,en;q=0.4`. If no relative quality factor is present, the value is by default `1.0`. 19 | 20 | **BrowserLocale** parses this string and lets you access the preferred locales quickly and easily. 21 | 22 | ## Requirements 23 | 24 | - PHP >= 7.0 25 | 26 | ## Install 27 | 28 | ```bash 29 | composer require codezero/browser-locale 30 | ``` 31 | 32 | ## Instantiate 33 | 34 | For vanilla PHP: 35 | 36 | ```php 37 | $browser = new \CodeZero\BrowserLocale\BrowserLocale($_SERVER["HTTP_ACCEPT_LANGUAGE"]); 38 | ``` 39 | 40 | For Laravel: 41 | 42 | Laravel >= 5.5 will automatically register the ServiceProvider so you can get `BrowserLocale` from the IOC container. 43 | 44 | ```php 45 | $browser = \App::make(\CodeZero\BrowserLocale\BrowserLocale::class); 46 | ``` 47 | 48 | ## Get Primary Locale 49 | 50 | ```php 51 | $locale = $browser->getLocale(); 52 | ``` 53 | 54 | This will return an instance of `\CodeZero\BrowserLocale\Locale` or `null` if no locale exists. 55 | 56 | ```php 57 | if ($locale !== null) { 58 | $full = $locale->locale; // Example: "en-US" 59 | $language = $locale->language; // Example: "en" 60 | $country = $locale->country; // Example: "US" 61 | $weight = $locale->weight; // Example: 1.0 62 | } 63 | ``` 64 | 65 | ## Get All Locales 66 | 67 | ```php 68 | $locales = $browser->getLocales(); 69 | ``` 70 | 71 | This will return an array of `\CodeZero\BrowserLocale\Locale` instances, sorted by weight in descending order. 72 | So the first array item is the most preferred locale. 73 | 74 | If no locales exist, an empty array will be returned. 75 | 76 | ```php 77 | foreach ($locales as $locale) { 78 | $full = $locale->locale; // Example: "en-US" 79 | $language = $locale->language; // Example: "en" 80 | $country = $locale->country; // Example: "US" 81 | $weight = $locale->weight; // Example: 1.0 82 | } 83 | ``` 84 | 85 | ## Filter Locale Info 86 | 87 | You can get a flattened array with only specific Locale information. 88 | These arrays will always be sorted by weight in descending order. 89 | There will be no duplicate values! (e.g. `en` and `en-US` are both the language `en`) 90 | 91 | ### LocaleFilter 92 | 93 | Returns an array of every locale found in the input string. 94 | 95 | ```php 96 | $browser = new \CodeZero\BrowserLocale\BrowserLocale('en-US,en;q=0.8,nl-NL;q=0.6'); 97 | $filter = new \CodeZero\BrowserLocale\Filters\LocaleFilter; 98 | $locales = $browser->filter($filter); 99 | //=> Result: ['en-US', 'en', 'nl-BE'] 100 | ``` 101 | 102 | ### CombinedFilter 103 | 104 | Returns an array of every locale found in the input string, while making sure the 2-letter language version of the locale is always present. 105 | 106 | ```php 107 | $browser = new \CodeZero\BrowserLocale\BrowserLocale('en-US,nl;q=0.8'); 108 | $filter = new \CodeZero\BrowserLocale\Filters\CombinedFilter; 109 | $locales = $browser->filter($filter); 110 | //=> Result: ['en-US', 'en', 'nl'] 111 | ``` 112 | 113 | ### LanguageFilter 114 | 115 | Returns an array of only the 2-letter language codes found in the input string. Language codes are also extracted from full locales and added to the results array. 116 | 117 | ```php 118 | $browser = new \CodeZero\BrowserLocale\BrowserLocale('en-US,en;q=0.8,nl-NL;q=0.6'); 119 | $filter = new \CodeZero\BrowserLocale\Filters\LanguageFilter; 120 | $languages = $browser->filter($filter); 121 | //=> Result: ['en', 'nl'] 122 | ``` 123 | 124 | ### CountryFilter 125 | 126 | Returns an array of only the 2-letter country codes found in the input string. Locales that only contain a 2-letter language code will be skipped. 127 | 128 | ```php 129 | $browser = new \CodeZero\BrowserLocale\BrowserLocale('en-US,en;q=0.8,nl-NL;q=0.6,nl;q=0.4'); 130 | $filter = new \CodeZero\BrowserLocale\Filters\CountryFilter; 131 | $countries = $browser->filter($filter); 132 | //=> Result: ['US', 'NL'] 133 | ``` 134 | 135 | ### WeightFilter 136 | 137 | Returns an array of all relative quality factors found in the input string. The default of `1.0` is also included. 138 | 139 | ```php 140 | $browser = new \CodeZero\BrowserLocale\BrowserLocale('en-US,en;q=0.8,nl-NL;q=0.6,nl;q=0.4'); 141 | $filter = new \CodeZero\BrowserLocale\Filters\WeightFilter; 142 | $weights = $browser->filter($filter); 143 | //=> Result: [1.0, 0.8, 0.6, 0.4] 144 | ``` 145 | 146 | You can create your own filters by implementing the `\CodeZero\BrowserLocale\Filters\Filter` interface. 147 | 148 | ## Testing 149 | 150 | ```bash 151 | composer test 152 | ``` 153 | 154 | ## Security 155 | 156 | If you discover any security related issues, please [e-mail me](mailto:ivan@codezero.be) instead of using the issue tracker. 157 | 158 | ## Changelog 159 | 160 | A complete list of all notable changes to this package can be found on the 161 | [releases page](https://github.com/codezero-be/browser-locale/releases). 162 | 163 | ## License 164 | 165 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 166 | --------------------------------------------------------------------------------