├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── labeler.yml ├── pull_request_template.md └── workflows │ ├── labeler.yml │ ├── release.yml │ ├── stale.yml │ └── test.yml ├── .gitignore ├── .php_cs ├── .styleci.yml ├── CODE_OF_CONDUCT.md ├── changelog.md ├── composer.json ├── config └── localizater.php ├── contributing.md ├── license.md ├── phpunit.xml ├── readme.md ├── src ├── Facades │ └── Localizater.php ├── Localizater.php ├── LocalizaterMiddleware.php ├── LocalizaterServiceProvider.php └── helpers.php └── tests ├── CreatesApplication.php ├── Features ├── ExtraHelpersTest.php ├── GroupTest.php ├── LocaleDirTest.php ├── LocaleNameTest.php ├── LocaleRouteTest.php └── LocalizaterMiddlewareTest.php └── TestCase.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | indent_size = 2 14 | 15 | [*.{yml,yaml}] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.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 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.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/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | testing: 2 | - tests/* 3 | 4 | repo: 5 | - any: ['./*', '.github/*'] 6 | 7 | src: 8 | - src/* 9 | 10 | config: 11 | - config/* 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | ## Proposed Changes 4 | 5 | - 6 | - 7 | - 8 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | # This workflow will triage pull requests and apply a label based on the 2 | # paths that are modified in the pull request. 3 | # 4 | # To use this workflow, you will need to set up a .github/labeler.yml 5 | # file with configuration. For more information, see: 6 | # https://github.com/actions/labeler 7 | 8 | name: Labeler 9 | on: pull_request 10 | 11 | jobs: 12 | label: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/labeler@master 18 | with: 19 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | jobs: 8 | build: 9 | name: Create Release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | - name: Create Release 15 | id: create_release 16 | uses: actions/create-release@v1 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | with: 20 | tag_name: ${{ github.ref }} 21 | release_name: ${{ github.ref }} 22 | draft: false 23 | prerelease: false 24 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "30 1 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v1 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days' 17 | stale-pr-message: 'This pull request is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days' 18 | days-before-stale: 30 19 | days-before-close: 5 20 | stale-issue-label: 'no-issue-activity' 21 | stale-pr-label: 'no-pr-activity' 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | test: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | php: [7.4, 7.3] 15 | laravel: [8.*, 7.*, 6.*, 5.8.*] 16 | dependency-version: [prefer-lowest, prefer-stable] 17 | os: [ubuntu-latest] 18 | include: 19 | - laravel: 8.* 20 | testbench: 6.* 21 | - laravel: 7.* 22 | testbench: 5.* 23 | - laravel: 6.* 24 | testbench: 4.* 25 | - laravel: 5.8.* 26 | testbench: 3.8.* 27 | 28 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ${{ matrix.os }} 29 | 30 | services: 31 | mysql: 32 | image: mysql:5.7 33 | env: 34 | MYSQL_ALLOW_EMPTY_PASSWORD: yes 35 | MYSQL_DATABASE: localizater 36 | ports: 37 | - 3306 38 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 39 | 40 | steps: 41 | - name: Checkout code 42 | uses: actions/checkout@v1 43 | 44 | - name: Cache dependencies 45 | uses: actions/cache@v1 46 | with: 47 | path: ~/.composer/cache/files 48 | key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 49 | 50 | - name: Setup PHP 51 | uses: shivammathur/setup-php@v2 52 | with: 53 | php-version: ${{ matrix.php }} 54 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, mysql, mysqli, pdo_mysql, bcmath, soap, intl, gd, exif, iconv, imagick 55 | coverage: none 56 | 57 | - name: Install dependencies 58 | run: | 59 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 60 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest 61 | - name: Execute tests 62 | run: vendor/bin/phpunit 63 | env: 64 | DB_PORT: ${{ job.services.mysql.ports[3306] }} 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | build 3 | composer.phar 4 | composer.lock 5 | .phpunit.result.cache 6 | report 7 | .DS_Store 8 | *.sublime-project 9 | *.sublime-workspace 10 | testbench/ 11 | coverage 12 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | setRules([ 4 | '@PSR2' => true, 5 | 'array_indentation' => true, 6 | 'array_syntax' => ['syntax' => 'short'], 7 | 'combine_consecutive_unsets' => true, 8 | 'method_separation' => true, 9 | 'no_multiline_whitespace_before_semicolons' => true, 10 | 'single_quote' => true, 11 | 'binary_operator_spaces' => [ 12 | 'align_double_arrow' => false, 13 | 'align_equals' => false, 14 | ], 15 | // 'blank_line_after_opening_tag' => true, 16 | 'blank_line_before_return' => true, 17 | 'braces' => [ 18 | 'allow_single_line_closure' => true, 19 | ], 20 | // 'cast_spaces' => true, 21 | // 'class_definition' => array('singleLine' => true), 22 | 'concat_space' => ['spacing' => 'none'], 23 | 'declare_equal_normalize' => true, 24 | 'function_typehint_space' => true, 25 | 'hash_to_slash_comment' => true, 26 | 'include' => true, 27 | 'lowercase_cast' => true, 28 | // 'native_function_casing' => true, 29 | // 'new_with_braces' => true, 30 | // 'no_blank_lines_after_class_opening' => true, 31 | // 'no_blank_lines_after_phpdoc' => true, 32 | // 'no_empty_comment' => true, 33 | // 'no_empty_phpdoc' => true, 34 | // 'no_empty_statement' => true, 35 | 'no_extra_consecutive_blank_lines' => [ 36 | 'curly_brace_block', 37 | 'extra', 38 | 'parenthesis_brace_block', 39 | 'square_brace_block', 40 | 'throw', 41 | 'use', 42 | ], 43 | // 'no_leading_import_slash' => true, 44 | // 'no_leading_namespace_whitespace' => true, 45 | // 'no_mixed_echo_print' => array('use' => 'echo'), 46 | 'no_multiline_whitespace_around_double_arrow' => true, 47 | // 'no_short_bool_cast' => true, 48 | // 'no_singleline_whitespace_before_semicolons' => true, 49 | 'no_spaces_around_offset' => true, 50 | // 'no_trailing_comma_in_list_call' => true, 51 | // 'no_trailing_comma_in_singleline_array' => true, 52 | // 'no_unneeded_control_parentheses' => true, 53 | 'no_unused_imports' => true, 54 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 55 | 'no_whitespace_before_comma_in_array' => true, 56 | 'no_whitespace_in_blank_line' => true, 57 | // 'normalize_index_brace' => true, 58 | 'object_operator_without_whitespace' => true, 59 | // 'php_unit_fqcn_annotation' => true, 60 | // 'phpdoc_align' => true, 61 | // 'phpdoc_annotation_without_dot' => true, 62 | // 'phpdoc_indent' => true, 63 | // 'phpdoc_inline_tag' => true, 64 | // 'phpdoc_no_access' => true, 65 | // 'phpdoc_no_alias_tag' => true, 66 | // 'phpdoc_no_empty_return' => true, 67 | // 'phpdoc_no_package' => true, 68 | // 'phpdoc_no_useless_inheritdoc' => true, 69 | 'phpdoc_order' => false, 70 | // 'phpdoc_return_self_reference' => true, 71 | // 'phpdoc_scalar' => true, 72 | // 'phpdoc_separation' => true, 73 | // 'phpdoc_single_line_var_spacing' => true, 74 | 'phpdoc_summary' => true, 75 | // 'phpdoc_to_comment' => true, 76 | // 'phpdoc_trim' => true, 77 | // 'phpdoc_types' => true, 78 | // 'phpdoc_var_without_name' => true, 79 | // 'pre_increment' => true, 80 | 'return_type_declaration' => ['space_before' => 'none'], 81 | // 'self_accessor' => true, 82 | // 'short_scalar_cast' => true, 83 | 'single_blank_line_before_namespace' => true, 84 | // 'single_class_element_per_statement' => true, 85 | // 'space_after_semicolon' => true, 86 | // 'standardize_not_equals' => true, 87 | 'ternary_operator_spaces' => true, 88 | 'trailing_comma_in_multiline_array' => true, 89 | 'trim_array_spaces' => true, 90 | 'unary_operator_spaces' => true, 91 | 'whitespace_after_comma_in_array' => true, 92 | 'function_declaration' => true, 93 | 'indentation_type' => true, 94 | 'no_spaces_after_function_name' => true, 95 | 'no_spaces_inside_parenthesis' => true, 96 | 'not_operator_with_successor_space' => true, 97 | 'no_alias_functions' => true 98 | ]) 99 | //->setIndent("\t") 100 | ->setLineEnding("\n"); 101 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at elkebir.med@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `Localizater` will be documented in this file. 4 | 5 | ## Version 2.0 6 | 7 | ### Add 8 | 9 | - Support Laravel 8 10 | 11 | ## Version 1.3 12 | 13 | ### Add 14 | 15 | - Feature: Get HTML direction attribute based on a specified locale. 16 | 17 | ## Version 1.2 18 | 19 | ### Add 20 | 21 | - Feature: Get HTML direction attribute based on current locale. 22 | 23 | ## Version 1.1.1 24 | 25 | ### Fixed 26 | 27 | - `readme.md` typos 28 | 29 | ## Version 1.1 30 | 31 | ### Updated 32 | 33 | - Change the order of `locale_route()` parameters 34 | 35 | ## Version 1.0 36 | 37 | ### Added 38 | 39 | - First stable release. Everything is brand new! 40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getsupercode/localizater", 3 | "version": "2.0.0", 4 | "description": "Laravel package for wrapping routes in multiple locale prefixes", 5 | "license": "MIT", 6 | "type": "library", 7 | "authors": [ 8 | { 9 | "name": "Mohamed Elkebir", 10 | "email": "elkebir.med@gmail.com", 11 | "homepage": "http://getsupercode.com" 12 | } 13 | ], 14 | "homepage": "https://github.com/getsupercode/localizater", 15 | "keywords": [ 16 | "Laravel", 17 | "Localizater" 18 | ], 19 | "require": { 20 | "php": "^7.3|^8.0", 21 | "illuminate/support": "~5|~6|~7|~8" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "^8.0|^9.0", 25 | "mockery/mockery": "^1.1", 26 | "orchestra/testbench": "~3|~4|~5|~6", 27 | "sempro/phpunit-pretty-print": "^1.0", 28 | "friendsofphp/php-cs-fixer": "^2.16 || ^3.0" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Getsupercode\\Localizater\\": "src/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Getsupercode\\Localizater\\Tests\\": "tests" 38 | } 39 | }, 40 | "scripts": { 41 | "test": "vendor/bin/phpunit", 42 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 43 | }, 44 | "extra": { 45 | "laravel": { 46 | "providers": [ 47 | "Getsupercode\\Localizater\\LocalizaterServiceProvider" 48 | ], 49 | "aliases": { 50 | "Localizater": "Getsupercode\\Localizater\\Facades\\Localizater" 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /config/localizater.php: -------------------------------------------------------------------------------- 1 | [ 8 | * 'en' => 'English', 9 | * 'fr' => 'Français', 10 | * 'ar' => 'العربية', 11 | * ], 12 | */ 13 | 'locales' => [ 14 | 'en' => 'English', 15 | ], 16 | 17 | /** 18 | * RTL Locales. 19 | * 20 | * Locales listed here will have special layout direction. 21 | */ 22 | 'rtl_locales' => ['ar'], 23 | 24 | /** 25 | * Prefix default locale or not. 26 | * 27 | * true: 28 | * example.com/en 29 | * example.com/fr 30 | * 31 | * false: 32 | * example.com 33 | * example.com/fr 34 | */ 35 | 'prefix_default' => false, 36 | 37 | /** 38 | * Prefix default locale route name or not. 39 | * 40 | * true: ->name('page') 41 | * +----------+----------+---------+ 42 | * | Method | URI | Name | 43 | * +----------+----------+---------+ 44 | * | GET|HEAD | /page | en.page | 45 | * +----------+----------+---------+ 46 | * | GET|HEAD | /fr/page | fr.page | 47 | * +----------+----------+---------+ 48 | * 49 | * false: ->name('page') 50 | * +----------+----------+---------+ 51 | * | Method | URI | Name | 52 | * +----------+----------+---------+ 53 | * | GET|HEAD | /page | page | 54 | * +----------+----------+---------+ 55 | * | GET|HEAD | /fr/page | fr.page | 56 | * +----------+----------+---------+ 57 | */ 58 | 'prefix_default_name' => false, 59 | ]; 60 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome and will be fully credited. 4 | 5 | Contributions are accepted via Pull Requests on [Github](https://github.com/getsupercode/localizater). 6 | 7 | Also try to code in the same style as Laravel (which followes the [PSR standard](http://www.php-fig.org/) guidelines). StyleCI is set up to fix any discrepancies. 8 | 9 | **Happy coding**! 10 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) Mohamed Elkebir 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Localizater 2 | 3 | [![Latest Version on Packagist][ico-version]][link-packagist] 4 | [![Total Downloads][ico-downloads]][link-downloads] 5 | ![Test](https://github.com/Getsupercode/Localizater/workflows/Test/badge.svg) 6 | [![StyleCI](https://github.styleci.io/repos/282613224/shield?branch=master)](https://github.styleci.io/repos/282613224?branch=master) 7 | 8 | Laravel package for wrapping routes in multiple locale prefixes. 9 | 10 | ## Installation 11 | 12 | Via Composer 13 | 14 | ```bash 15 | composer require getsupercode/localizater 16 | ``` 17 | 18 | To detect and change the locale of the application based on the request automatically, you can add this middleware to your `app/Http/Kernel`: 19 | 20 | ```php 21 | protected $middlewareGroups = [ 22 | 'web' => [ 23 | \Getsupercode\Localizater\LocalizaterMiddleware::class, 24 | // ... 25 | ] 26 | ]; 27 | ``` 28 | 29 | ## Configuration 30 | 31 | By default, the application locales are only going to be `en` and the default locale is not prefixed. If you want to prefix the default locale, please run the following command to publish the configuration file: 32 | 33 | ```bash 34 | php artisan vendor:publish --provider="Getsupercode\Localizater\LocalizaterServiceProvider" --tag="config" 35 | ``` 36 | 37 | After installing the package, Adding the middleware and publishing the configuration file. You need to edit the configuration file `config/localizater.php` in order to add more locales. 38 | 39 | ## Note 40 | 41 | The default locale is `app.locale` located at `config/app.php` file. 42 | 43 | ### Config: `localizater.locales` 44 | 45 | Add supported locales. It's recommended to write the locale value with its native language. 46 | 47 | ```php 48 | 'locales' => [ 49 | 'en' => 'English', 50 | 'fr' => 'Français', 51 | 'ar' => 'العربية', 52 | ] 53 | ``` 54 | 55 | ### Config: `localizater.rtl_locales` 56 | 57 | Add RTL direction locales. 58 | 59 | ```php 60 | 'rtl_locales' => ['ar'] 61 | ``` 62 | 63 | ### Config: `localizater.prefix_default` 64 | 65 | If this option is set to true, Default locale URL will be prefixed. 66 | 67 | ```txt 68 | true: 69 | www.example.com/en 70 | www.example.com/fr 71 | 72 | false: 73 | www.example.com 74 | www.example.com/fr 75 | ``` 76 | 77 | ### Config: `localizater.prefix_default_name` 78 | 79 | If this option is set to true, Default locale route name will be prefixed. 80 | 81 | **true:** 82 | 83 | | Method | URI | URI | Name | 84 | | ------ | ---- | -------- | ------- | 85 | | GET | HEAD | /page | en.page | 86 | | GET | HEAD | /fr/page | fr.page | 87 | 88 | **false:** 89 | 90 | | Method | URI | URI | Name | 91 | | ------ | ---- | -------- | ------- | 92 | | GET | HEAD | /page | page | 93 | | GET | HEAD | /fr/page | fr.page | 94 | 95 | ## Usage 96 | 97 | The package will not override the route features you already know. It's just a wrapper function that will create multiple locale routes for you. 98 | 99 | ```php 100 | // routes/web.php 101 | 102 | name('welcome'); 109 | 110 | Route::get('/user', 'UserController@index'); 111 | }); 112 | 113 | // Put other (Non read) route actions outside the `Localizater::group` as you don't need to have multiple locales for those actions. 114 | 115 | Route::post('/user', 'UserController@store'); 116 | ``` 117 | 118 | The above example will give us: 119 | 120 | | Method | URI | Name | 121 | | --------- | -------- | ---------- | 122 | | GET\|HEAD | / | welcome | 123 | | GET\|HEAD | /fr | fr.welcome | 124 | | GET\|HEAD | /user | | 125 | | GET\|HEAD | /fr/user | fr. | 126 | | POST | /user | | 127 | 128 | ### Route naming 129 | 130 | If you add a name to a route it will be prepended by the locale key `locale.name`. _for example: (`fr.welcome`)_ 131 | 132 | All locales without a name will have the same prefix name like `fr.`. And this is normal as you don't need its names. 133 | 134 | ### Localizater attributes 135 | 136 | You can add attributes to the localizer group function as you do with the route group function. 137 | 138 | ```php 139 | Localizater::group(['middleware' => 'auth'], function () { 140 | Route::view('/home', 'home')->name('home'); 141 | }); 142 | ``` 143 | 144 | Or: 145 | 146 | ```php 147 | Localizater::group(function () { 148 | Route::group(['middleware' => 'auth'], function () { 149 | Route::view('/home', 'home')->name('home'); 150 | }); 151 | }); 152 | ``` 153 | 154 | ### Get route URL in a specified locale 155 | 156 | You can get the current route URL in different locale key: 157 | 158 | ```php 159 | // Current route URL: example.com 160 | 161 | locale_route(null, 'fr'); 162 | 163 | // Output: example.com/fr 164 | ``` 165 | 166 | Or a named route: 167 | 168 | ```php 169 | // Route URL: example.com/fr/home 170 | 171 | locale_route('home', 'en'); 172 | 173 | // Output: example.com/home 174 | ``` 175 | 176 | You can pass the same parameters as the [`route()`](https://laravel.com/docs/7.x/helpers#method-route) function after the locale parameter. 177 | 178 | ```php 179 | // locale_route($route, $locale, $parameters, $absolute); 180 | 181 | // Current route 182 | locale_route(null, 'fr', ['status' => 'active'], true); 183 | 184 | // Named route 185 | locale_route('home', 'fr', ['status' => 'active'], true); 186 | ``` 187 | 188 | ### Get locale language name 189 | 190 | You can get the value of the locale key in `localizater.locales` configuration for the current locale or a specified locale: 191 | 192 | ```php 193 | locale_name(); 194 | // Output: English 195 | 196 | locale_name('fr'); 197 | // Output: Français 198 | 199 | locale_name('ar'); 200 | // Output: العربية 201 | ``` 202 | 203 | ### Get HTML `dir` attribute based on current locale 204 | 205 | You can get HTML `dir` attribute based on current locale. The package will search for RTL locales in `localizater.rtl_locales` config. If the current locale is listed there, The output will be `rtl` or `ltr` if it's not listed. 206 | 207 | ```php 208 | // Current locale is: ar 209 | locale_dir(); 210 | // Output: rtl 211 | 212 | // Current locale is: en 213 | locale_dir(); 214 | // Output: ltr 215 | ``` 216 | 217 | ```html 218 | 219 | ``` 220 | 221 | You can also get `dir` attribute for a specified locale: 222 | 223 | ```php 224 | locale_dir('ar'); 225 | // Output: rtl 226 | ``` 227 | 228 | ## Change log 229 | 230 | Please see the [changelog](changelog.md) for more information on what has changed recently. 231 | 232 | ## Testing 233 | 234 | ```bash 235 | composer test 236 | ``` 237 | 238 | ## Contributing 239 | 240 | Please see [contributing.md](contributing.md) for details. 241 | 242 | ## Security 243 | 244 | If you discover any security related issues, please email elkebir.med@gmail.com instead of using the issue tracker. 245 | 246 | ## Credits 247 | 248 | - [Mohamed Elkebir][link-author] 249 | - [All Contributors][link-contributors] 250 | 251 | ## License 252 | 253 | MIT. Please see the [license file](license.md) for more information. 254 | 255 | [ico-version]: https://img.shields.io/packagist/v/getsupercode/localizater.svg?style=flat-square 256 | [ico-downloads]: https://img.shields.io/packagist/dt/getsupercode/localizater.svg?style=flat-square 257 | [ico-styleci]: https://styleci.io/repos/282613224/shield 258 | [link-packagist]: https://packagist.org/packages/getsupercode/localizater 259 | [link-downloads]: https://packagist.org/packages/getsupercode/localizater 260 | [link-styleci]: https://styleci.io/repos/282613224 261 | [link-author]: https://github.com/elkebirmed 262 | [link-contributors]: ../../contributors 263 | -------------------------------------------------------------------------------- /src/Facades/Localizater.php: -------------------------------------------------------------------------------- 1 | locales = Config::get('localizater.locales'); 60 | $this->rtl_locales = Config::get('localizater.rtl_locales'); 61 | $this->locale = Config::get('app.locale'); 62 | $this->prefixDefault = Config::get('localizater.prefix_default'); 63 | $this->prefixDefaultName = Config::get('localizater.prefix_default_name'); 64 | $this->router = App::make('router'); 65 | } 66 | 67 | /** 68 | * Create a route group with shared attributes and multiple locales. 69 | * 70 | * @param \Closure|string|array $attributes 71 | * @param \Closure $handle 72 | * @return \Illuminate\Routing\Router|\Illuminate\Routing\RouteRegistrar 73 | */ 74 | protected function group($attributes, $handle) 75 | { 76 | foreach ($this->locales as $key => $value) { 77 | $this->router->group([ 78 | 'prefix' => $this->prefix($key), 79 | 'as' => $this->as($key), 80 | ], function () use ($attributes, $handle) { 81 | $this->router->group($attributes, $handle); 82 | }); 83 | } 84 | } 85 | 86 | /** 87 | * Get locale route URL/URI. 88 | * 89 | * @param string|null $route 90 | * @param string|null $locale 91 | * @param array $parameters 92 | * @param bool $absolute 93 | * @return string 94 | */ 95 | public function localeRoute($route = null, $locale = null, $parameters = [], $absolute = true) 96 | { 97 | // Get route 98 | $route = $this->route($route, $parameters, $absolute); 99 | 100 | // Get route URL path 101 | $parse_url = parse_url($route, PHP_URL_PATH); 102 | 103 | // Get route URL query strings 104 | $parse_query = parse_url($route, PHP_URL_QUERY); 105 | 106 | // Get root domain 107 | $root = preg_replace('/\/$/', '', Request::root()); 108 | 109 | // Get route URL query strings 110 | $segments = explode('/', $parse_url); 111 | 112 | // Get wanted locale. 113 | $locale = $locale ?: App::getLocale(); 114 | 115 | // If this is welcome page add the default locale as a segment 116 | if (! $parse_url) { 117 | $segments[1] = $this->locale; 118 | } 119 | 120 | // Check if locale exists 121 | if (array_key_exists($segments[1], $this->locales)) { 122 | unset($segments[1]); 123 | } 124 | 125 | // Change the locale prefix 126 | if ($this->locale == $locale && ! $this->prefixDefaultName) { 127 | array_splice($segments, 1, 0); 128 | } else { 129 | array_splice($segments, 1, 0, $locale); 130 | } 131 | 132 | // Make route 133 | $url = '/'.implode('/', array_values(array_filter($segments))); 134 | 135 | // Add path to domain 136 | if ($absolute) { 137 | $url = $root.$url; 138 | } 139 | 140 | // Add query strings if exists 141 | if ($parse_query) { 142 | $url = $url.'?'.$parse_query; 143 | } 144 | 145 | return $url; 146 | } 147 | 148 | /** 149 | * Get locale language name. 150 | * 151 | * @param string|null $locale 152 | * @return string 153 | */ 154 | public function localeName($locale = null) 155 | { 156 | return Config::get('localizater.locales')[$locale ?: App::getLocale()]; 157 | } 158 | 159 | /** 160 | * Get locale direction. 161 | * @param string|null $locale 162 | * @return string 163 | */ 164 | public function localeDir($locale = null) 165 | { 166 | return in_array($locale ?: App::getLocale(), $this->rtl_locales) ? 'rtl' : 'ltr'; 167 | } 168 | 169 | /** 170 | * Get `prefix` attribute. 171 | * 172 | * @param string $key 173 | * @return string 174 | */ 175 | protected function prefix(string $key): string 176 | { 177 | return ! $this->prefixDefault && $key === $this->locale ? '' : $key; 178 | } 179 | 180 | /** 181 | * Get `as` attribute. 182 | * 183 | * @param string $key 184 | * @return string 185 | */ 186 | protected function as(string $key): string 187 | { 188 | return ! $this->prefixDefaultName && $key === $this->locale ? '' : $key.'.'; 189 | } 190 | 191 | /** 192 | * Get route full URL. 193 | * 194 | * @param string|null $route 195 | * @param array $parameters 196 | * @param bool $absolute 197 | * @return string 198 | */ 199 | protected function route($route = null, $parameters = [], $absolute = true) 200 | { 201 | return $route 202 | ? route($route, $parameters, $absolute) 203 | : URL::full(); 204 | } 205 | 206 | /** 207 | * Call methods dynamically. 208 | * 209 | * @param string $name 210 | * @param array $arguments 211 | * @return mixed 212 | */ 213 | public function __call(string $name, array $arguments) 214 | { 215 | if ($name === 'group') { 216 | if ($arguments[0] instanceof \Closure) { 217 | $this->group([], ...$arguments); 218 | } else { 219 | $this->group(...$arguments); 220 | } 221 | } 222 | 223 | if ($name === 'getLocale') { 224 | return $this->locale; 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/LocalizaterMiddleware.php: -------------------------------------------------------------------------------- 1 | locales = Config::get('localizater.locales'); 32 | $this->defaultLocale = Localizater::getLocale(); 33 | } 34 | 35 | /** 36 | * Handle an incoming request. 37 | * 38 | * @param \Illuminate\Http\Request $request 39 | * @param \Closure $next 40 | * @return mixed 41 | */ 42 | public function handle($request, Closure $next) 43 | { 44 | // Get first URL segment 45 | $segment = $request->segment(1); 46 | 47 | // Check if segment is a locale prefix 48 | $isLocale = array_key_exists($segment, $this->locales); 49 | 50 | // Change locale 51 | if ($isLocale) { 52 | App::setLocale($segment); 53 | } else { 54 | App::setLocale($this->defaultLocale); 55 | } 56 | 57 | return $next($request); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/LocalizaterServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 21 | $this->bootForConsole(); 22 | } 23 | } 24 | 25 | /** 26 | * Register any package services. 27 | * 28 | * @return void 29 | */ 30 | public function register() 31 | { 32 | $this->mergeConfigFrom(__DIR__.'/../config/localizater.php', 'localizater'); 33 | 34 | // Register the service the package provides. 35 | $this->app->singleton('localizater', function ($app) { 36 | return new Localizater(); 37 | }); 38 | } 39 | 40 | /** 41 | * Get the services provided by the provider. 42 | * 43 | * @codeCoverageIgnore 44 | * 45 | * @return array 46 | */ 47 | public function provides() 48 | { 49 | return ['localizater']; 50 | } 51 | 52 | /** 53 | * Console-specific booting. 54 | * 55 | * @return void 56 | */ 57 | protected function bootForConsole() 58 | { 59 | // Publishing the configuration file. 60 | $this->publishes([ 61 | __DIR__.'/../config/localizater.php' => config_path('localizater.php'), 62 | ], 'localizater.config'); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 | 'Getsupercode\Localizater\Facades\Localizater', 28 | ]; 29 | } 30 | 31 | /** 32 | * Define environment setup. 33 | * 34 | * @param \Illuminate\Foundation\Application $app 35 | * @return void 36 | */ 37 | protected function getEnvironmentSetUp($app) 38 | { 39 | // ... 40 | } 41 | 42 | /** 43 | * Setup the test environment. 44 | */ 45 | public function setUp() 46 | { 47 | parent::setUp(); 48 | } 49 | 50 | /** 51 | * Creates the application. 52 | * 53 | * Needs to be implemented by subclasses. 54 | * 55 | * @return \Illuminate\Foundation\Application 56 | */ 57 | public function createApplication() 58 | { 59 | $app = parent::createApplication(); 60 | 61 | return $app; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Features/ExtraHelpersTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(Localizater::getLocale(), 'en'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Features/GroupTest.php: -------------------------------------------------------------------------------- 1 | get('/')->assertOk(); 22 | $this->get('/fr')->assertOk(); 23 | $this->get('/ar')->assertOk(); 24 | $this->get('/es')->assertNotFound(); 25 | } 26 | 27 | /** @test */ 28 | public function canNotAffectOutsideRoutes() 29 | { 30 | Route::get('/outside', function () { 31 | return ''; 32 | }); 33 | 34 | $this->get('/outside')->assertOk(); 35 | $this->get('/fr/outside')->assertNotFound(); 36 | $this->get('/ar/outside')->assertNotFound(); 37 | } 38 | 39 | /** @test */ 40 | public function canHaveAttributes() 41 | { 42 | Localizater::group(['prefix' => 'admin'], function () { 43 | Route::get('/', function () { 44 | return ''; 45 | }); 46 | }); 47 | 48 | $this->get('/admin')->assertOk(); 49 | $this->get('/fr/admin')->assertOk(); 50 | $this->get('/ar/admin')->assertOk(); 51 | } 52 | 53 | /** @test */ 54 | public function canHaveNames() 55 | { 56 | Localizater::group(function () { 57 | Route::get('/', function () { 58 | return ''; 59 | })->name('welcome'); 60 | }); 61 | 62 | $this->assertEquals(route('welcome', [], false), '/'); 63 | $this->assertEquals(route('fr.welcome', [], false), '/fr'); 64 | $this->assertEquals(route('ar.welcome', [], false), '/ar'); 65 | } 66 | 67 | /** @test */ 68 | public function canPrefixDefaultLocaleRoute() 69 | { 70 | Config::set('localizater.prefix_default', true); 71 | 72 | Localizater::group(function () { 73 | Route::get('/', function () { 74 | return ''; 75 | }); 76 | }); 77 | 78 | $this->get('/en')->assertOk(); 79 | $this->get('/fr')->assertOk(); 80 | $this->get('/ar')->assertOk(); 81 | $this->get('/')->assertNotFound(); 82 | } 83 | 84 | /** @test */ 85 | public function canPrefixDefaultLocaleName() 86 | { 87 | Config::set('localizater.prefix_default_name', true); 88 | 89 | Localizater::group(function () { 90 | Route::get('/', function () { 91 | return ''; 92 | })->name('welcome'); 93 | }); 94 | 95 | $this->assertEquals(route('en.welcome', [], false), '/'); 96 | $this->assertEquals(route('fr.welcome', [], false), '/fr'); 97 | $this->assertEquals(route('ar.welcome', [], false), '/ar'); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tests/Features/LocaleDirTest.php: -------------------------------------------------------------------------------- 1 | setlocale('ar'); 13 | 14 | $this->assertEquals(locale_dir(), 'rtl'); 15 | } 16 | 17 | /** @test */ 18 | public function canGetSpecifiedLocaleDir() 19 | { 20 | $this->assertEquals(locale_dir('en'), 'ltr'); 21 | $this->assertEquals(locale_dir('fr'), 'ltr'); 22 | $this->assertEquals(locale_dir('ar'), 'rtl'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Features/LocaleNameTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(locale_name(), 'English'); 21 | } 22 | 23 | /** @test */ 24 | public function canGetSpecifiedLocaleName() 25 | { 26 | Localizater::group(function () { 27 | Route::get('/', function () { 28 | return ''; 29 | }); 30 | }); 31 | 32 | $this->assertEquals(locale_name('en'), 'English'); 33 | $this->assertEquals(locale_name('fr'), 'Français'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Features/LocaleRouteTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(locale_route(null, 'en', [], false), '/'); 21 | $this->assertEquals(locale_route(null, 'fr', [], false), '/fr'); 22 | } 23 | 24 | /** @test */ 25 | public function canGetNamedRoute() 26 | { 27 | Localizater::group(function () { 28 | Route::get('/', function () { 29 | return ''; 30 | })->name('welcome'); 31 | }); 32 | 33 | $this->assertEquals(locale_route('welcome', 'en', [], false), '/'); 34 | $this->assertEquals(locale_route('welcome', 'fr', [], false), '/fr'); 35 | $this->assertEquals(locale_route('fr.welcome', 'en', [], false), '/'); 36 | $this->assertEquals(locale_route('fr.welcome', 'fr', [], false), '/fr'); 37 | } 38 | 39 | /** @test */ 40 | public function canGetAbsoluteNamedRoute() 41 | { 42 | Localizater::group(function () { 43 | Route::get('/', function () { 44 | return ''; 45 | })->name('welcome'); 46 | }); 47 | 48 | $this->assertEquals(locale_route('welcome', 'en', [], true), 'http://localhost/'); 49 | $this->assertEquals(locale_route('welcome', 'fr', [], true), 'http://localhost/fr'); 50 | $this->assertEquals(locale_route('fr.welcome', 'en', [], true), 'http://localhost/'); 51 | $this->assertEquals(locale_route('fr.welcome', 'fr', [], true), 'http://localhost/fr'); 52 | } 53 | 54 | /** @test */ 55 | public function canGetNamedRouteWithQueryParameters() 56 | { 57 | Localizater::group(function () { 58 | Route::get('/?a=true&b=false', function () { 59 | return ''; 60 | })->name('welcome'); 61 | }); 62 | 63 | $this->assertEquals(locale_route('welcome', 'en', [], false), '/?a=true&b=false'); 64 | $this->assertEquals(locale_route('welcome', 'fr', [], false), '/fr?a=true&b=false'); 65 | $this->assertEquals(locale_route('fr.welcome', 'en', [], false), '/?a=true&b=false'); 66 | $this->assertEquals(locale_route('fr.welcome', 'fr', [], true), 'http://localhost/fr?a=true&b=false'); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/Features/LocalizaterMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | handle( 15 | Request::create('/ar'), 16 | function () { 17 | $this->assertEquals(app()->getLocale(), 'ar'); 18 | } 19 | ); 20 | } 21 | 22 | /** @test */ 23 | public function canRevertToDefaultAppLocale() 24 | { 25 | (new LocalizaterMiddleware())->handle( 26 | Request::create('/not-a-locale'), 27 | function () { 28 | $this->assertEquals(app()->getLocale(), 'en'); 29 | } 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | 'English', 21 | 'fr' => 'Français', 22 | 'ar' => 'العربية', 23 | ]); 24 | } 25 | } 26 | --------------------------------------------------------------------------------