├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── run-tests.yml ├── .gitignore ├── .styleci.yml ├── LICENSE.md ├── composer.json ├── composer.lock ├── config └── translation.php ├── contributing.md ├── database ├── factories │ ├── LanguageFactory.php │ └── TranslationFactory.php └── migrations │ ├── 2018_08_29_200844_create_languages_table.php │ └── 2018_08_29_205156_create_translations_table.php ├── logo.png ├── mix-manifest.json ├── package-lock.json ├── package.json ├── phpunit.xml ├── public └── assets │ ├── css │ └── main.css │ ├── js │ └── app.js │ └── mix-manifest.json ├── readme.md ├── resources ├── assets │ ├── css │ │ └── main.css │ └── js │ │ ├── app.js │ │ ├── bootstrap.js │ │ └── components │ │ └── TranslationInput.vue ├── helpers.php ├── lang │ ├── de │ │ ├── errors.php │ │ └── translation.php │ ├── en │ │ ├── errors.php │ │ └── translation.php │ ├── fr │ │ ├── errors.php │ │ └── translation.php │ └── nl │ │ ├── errors.php │ │ └── translation.php └── views │ ├── forms │ ├── search.blade.php │ ├── select.blade.php │ └── text.blade.php │ ├── icons │ ├── globe.blade.php │ └── translate.blade.php │ ├── languages │ ├── create.blade.php │ ├── index.blade.php │ └── translations │ │ ├── create.blade.php │ │ └── index.blade.php │ ├── layout.blade.php │ ├── nav.blade.php │ └── notifications.blade.php ├── routes └── web.php ├── src ├── Console │ └── Commands │ │ ├── AddLanguageCommand.php │ │ ├── AddTranslationKeyCommand.php │ │ ├── BaseCommand.php │ │ ├── ListLanguagesCommand.php │ │ ├── ListMissingTranslationKeys.php │ │ ├── SynchroniseMissingTranslationKeys.php │ │ └── SynchroniseTranslationsCommand.php ├── ContractDatabaseLoader.php ├── Drivers │ ├── Database.php │ ├── DriverInterface.php │ ├── File.php │ └── Translation.php ├── Events │ └── TranslationAdded.php ├── Exceptions │ ├── LanguageExistsException.php │ └── LanguageKeyExistsException.php ├── Http │ ├── Controllers │ │ ├── LanguageController.php │ │ └── LanguageTranslationController.php │ └── Requests │ │ ├── LanguageRequest.php │ │ └── TranslationRequest.php ├── InterfaceDatabaseLoader.php ├── Language.php ├── Rules │ └── LanguageNotExists.php ├── Scanner.php ├── Translation.php ├── TranslationBindingsServiceProvider.php ├── TranslationManager.php └── TranslationServiceProvider.php ├── tailwind.js ├── tests ├── DatabaseDriverTest.php ├── FileDriverTest.php ├── PackageIsLoadedTest.php ├── ScannerTest.php └── fixtures │ ├── lang │ ├── en.json │ ├── en │ │ └── test.php │ └── es │ │ └── .gitignore │ └── scan-tests │ ├── __.txt │ ├── at_lang.txt │ ├── lang_get.txt │ ├── trans.txt │ └── trans_choice.txt ├── translation.png └── webpack.mix.js /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | 13 | - name: Setup PHP 14 | uses: shivammathur/setup-php@v2 15 | with: 16 | php-version: 8.0 17 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite 18 | coverage: none 19 | 20 | - name: Install Composer dependencies 21 | run: composer install --prefer-dist --no-interaction 22 | 23 | - name: Execute tests 24 | run: vendor/bin/phpunit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /node_modules 3 | .phpunit.result.cache 4 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | finder: 3 | exclude: 4 | - "tests/fixtures" -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Joe Dixon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joedixon/laravel-translation", 3 | "description": "A tool for managing all of your Laravel translations", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Joe Dixon", 9 | "email": "hello@joedixon.co.uk" 10 | } 11 | ], 12 | "require": { 13 | "php": "^8.0", 14 | "illuminate/support": "^8.0||^9.0||^10.0", 15 | "laravel/legacy-factories": "^1.3" 16 | }, 17 | "require-dev": { 18 | "orchestra/testbench": "^6.0|^8.0", 19 | "phpunit/phpunit": "^9.0|^10.0", 20 | "mockery/mockery": "^1.0.0" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "JoeDixon\\Translation\\": "src" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "JoeDixon\\Translation\\Tests\\": "tests" 30 | } 31 | }, 32 | "scripts": { 33 | "test": "vendor/bin/phpunit", 34 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 35 | }, 36 | "extra": { 37 | "laravel": { 38 | "providers": [ 39 | "JoeDixon\\Translation\\TranslationServiceProvider", 40 | "JoeDixon\\Translation\\TranslationBindingsServiceProvider" 41 | ] 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /config/translation.php: -------------------------------------------------------------------------------- 1 | 'file', 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Route group configuration 19 | |-------------------------------------------------------------------------- 20 | | 21 | | The package ships with routes to handle language management. Update the 22 | | configuration here to configure the routes with your preferred group options. 23 | | 24 | */ 25 | 'route_group_config' => [ 26 | 'middleware' => 'web', 27 | ], 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Translation methods 32 | |-------------------------------------------------------------------------- 33 | | 34 | | Update this array to tell the package which methods it should look for 35 | | when finding missing translations. 36 | | 37 | */ 38 | 'translation_methods' => ['trans', '__'], 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Scan paths 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Update this array to tell the package which directories to scan when 46 | | looking for missing translations. 47 | | 48 | */ 49 | 'scan_paths' => [app_path(), resource_path()], 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | UI URL 54 | |-------------------------------------------------------------------------- 55 | | 56 | | Define the URL used to access the language management too. 57 | | 58 | */ 59 | 'ui_url' => 'languages', 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | Database settings 64 | |-------------------------------------------------------------------------- 65 | | 66 | | Define the settings for the database driver here. 67 | | 68 | */ 69 | 'database' => [ 70 | 71 | 'connection' => '', 72 | 73 | 'languages_table' => 'languages', 74 | 75 | 'translations_table' => 'translations', 76 | ], 77 | ]; 78 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | CONTRIBUTING 2 | ============ 3 | 4 | Contributions are welcome, and are accepted via pull requests. Please review these guidelines before submitting any pull requests. 5 | 6 | ## Guidelines 7 | 8 | * Please follow the [PSR-2 Coding Style Guide](http://www.php-fig.org/psr/psr-2/), enforced by [StyleCI](https://styleci.io/). 9 | * Ensure that the current tests pass, and if you've added something new, add the tests where relevant. 10 | * Send a coherent commit history, making sure each individual commit in your pull request is meaningful. 11 | * You may need to [rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) to avoid merge conflicts. 12 | * If you are changing the behavior, or the public api, you may need to update the docs. 13 | * Please remember that we follow [SemVer](http://semver.org/). 14 | 15 | We have [StyleCI](https://styleci.io/) setup to automatically fix any code style issues. -------------------------------------------------------------------------------- /database/factories/LanguageFactory.php: -------------------------------------------------------------------------------- 1 | define(Language::class, function (Generator $faker) { 7 | return [ 8 | 'language' => $faker->word, 9 | 'name' => $faker->word, 10 | ]; 11 | }); 12 | -------------------------------------------------------------------------------- /database/factories/TranslationFactory.php: -------------------------------------------------------------------------------- 1 | define(Translation::class, function (Generator $faker) { 8 | return [ 9 | 'language_id' => function () { 10 | return factory(Language::class)->create()->id; 11 | }, 12 | 'group' => $faker->word, 13 | 'key' => $faker->word, 14 | 'value' => $faker->sentence, 15 | ]; 16 | }); 17 | 18 | $factory->state(Translation::class, 'group', function (Generator $faker) { 19 | return [ 20 | 'language_id' => function () { 21 | return factory(Language::class)->create()->id; 22 | }, 23 | 'group' => $faker->word, 24 | 'key' => $faker->word, 25 | 'value' => $faker->sentence, 26 | ]; 27 | }); 28 | 29 | $factory->state(Translation::class, 'single', function (Generator $faker) { 30 | return [ 31 | 'language_id' => function () { 32 | return factory(Language::class)->create()->id; 33 | }, 34 | 'group' => 'single', 35 | 'key' => $faker->word, 36 | 'value' => $faker->sentence, 37 | ]; 38 | }); 39 | -------------------------------------------------------------------------------- /database/migrations/2018_08_29_200844_create_languages_table.php: -------------------------------------------------------------------------------- 1 | create(config('translation.database.languages_table'), function (Blueprint $table) { 19 | $table->increments('id'); 20 | $table->string('name')->nullable(); 21 | $table->string('language'); 22 | $table->timestamps(); 23 | }); 24 | 25 | $initialLanguages = array_unique([ 26 | config('app.fallback_locale'), 27 | config('app.locale'), 28 | ]); 29 | 30 | foreach ($initialLanguages as $language) { 31 | Language::firstOrCreate([ 32 | 'language' => $language, 33 | ]); 34 | } 35 | } 36 | 37 | /** 38 | * Reverse the migrations. 39 | * 40 | * @return void 41 | */ 42 | public function down() 43 | { 44 | Schema::connection(config('translation.database.connection')) 45 | ->dropIfExists(config('translation.database.languages_table')); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /database/migrations/2018_08_29_205156_create_translations_table.php: -------------------------------------------------------------------------------- 1 | create(config('translation.database.translations_table'), function (Blueprint $table) { 18 | $table->increments('id'); 19 | $table->unsignedInteger('language_id'); 20 | $table->foreign('language_id')->references('id') 21 | ->on(config('translation.database.languages_table')); 22 | $table->string('group')->nullable(); 23 | $table->text('key'); 24 | $table->text('value')->nullable(); 25 | $table->timestamps(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::connection(config('translation.database.connection')) 37 | ->dropIfExists(config('translation.database.translations_table')); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joedixon/laravel-translation/feba4d1e3d12722ca60c05d9180f39b7de227e4e/logo.png -------------------------------------------------------------------------------- /mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/Users/joe/Code/translation/public/vendor/translation/js/app.js": "/Users/joe/Code/translation/public/vendor/translation/js/app.js", 3 | "/../../../public/vendor/translation/css/main.css": "/../../../public/vendor/translation/css/main.css" 4 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 4 | "watch": "NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 5 | "hot": "NODE_ENV=development webpack-dev-server --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 6 | "production": "NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 7 | }, 8 | "license": "MIT", 9 | "devDependencies": { 10 | "laravel-mix": "^4.1.2", 11 | "postcss": "^7.0.36", 12 | "tailwindcss": "^0.6.6", 13 | "vue-template-compiler": "^2.6.10" 14 | }, 15 | "dependencies": { 16 | "axios": "^0.18.1", 17 | "vue": "^2.5.21" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./src 6 | 7 | 8 | ./src/Console 9 | 10 | 11 | 12 | 13 | ./tests 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/assets/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/js/app.js": "/js/app.js", 3 | "/css/main.css": "/css/main.css" 4 | } 5 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ![Laravel Translation](logo.png) 4 | 5 | Translation management for your Laravel application. 6 | 7 | ![Laravel Translation UI](translation.png) 8 | 9 | ![GitHub](https://img.shields.io/github/checks-status/joedixon/laravel-translation/master?style=for-the-badge) 10 | ![GitHub](https://img.shields.io/github/license/joedixon/laravel-translation.svg?style=for-the-badge) 11 | 12 |
13 | 14 | ------ 15 | 16 | ## About Laravel Translation 17 | 18 | Laravel Translation is a package for Laravel which allows you full control 19 | over your translations when using [Laravel's 20 | localization](https://laravel.com/docs/5.7/localization) functionality. 21 | 22 | The package allows you to manage your translations using either the native file 23 | based translations, but also provides a database driver which is useful in 24 | multi-server setups. 25 | 26 | It exposes a user interface allowing you to update existing and add new 27 | translations to your application. 28 | 29 | Below are a full list of features: 30 | 31 | - File and database drivers 32 | - Database translation loader (automatically load translations from the database 33 | when Laravel's translation retrieval methods and the database driver) 34 | - User interface to add new languages and add and update translations 35 | - Artisan commands to manage your translations 36 | - Scan your application for missing translations 37 | 38 | ## Version Compatibility 39 | 40 | | Laravel | Laravel Translation | 41 | | ------------- | ------------------- | 42 | | 6.x | 1.x | 43 | | 7.x | 1.x | 44 | | 8.x | 2.x | 45 | | 9.x | 2.x | 46 | 47 | ## Installation 48 | 49 | Install the package via Composer 50 | 51 | `composer require joedixon/laravel-translation` 52 | 53 | Publish configuration and assets 54 | 55 | `php artisan vendor:publish --provider="JoeDixon\Translation\TranslationServiceProvider"` 56 | 57 | The service provider is loaded automatically using [package discovery](https://laravel.com/docs/5.7/packages#package-discovery). 58 | 59 | ## Usage 60 | 61 | ### Configuration 62 | 63 | The package ships with a configuration file called `translation.php` which is published to the 64 | config directory during installation. Below is an outline of the settings. 65 | 66 | ``` 67 | driver [file|database] 68 | ``` 69 | Choose either `file` or `database`. File translations utilise Laravel's native 70 | file based translations and includes support for both `array` based and `json` based 71 | language files. 72 | 73 | ``` 74 | route_group_config.middleware [string|array] 75 | ``` 76 | Apply middleware to the routes which ship with the package. For example, you may 77 | which to use the `auth` middleware to ensure package user interface is only 78 | accessible to logged in users. 79 | 80 | ``` 81 | translation_methods [array] 82 | ``` 83 | Choose which of Laravel's translation methods to use when searching for missing 84 | translation keys. 85 | 86 | ``` 87 | scan_paths [array] 88 | ``` 89 | Choose which paths to use when searching for missing translations. Narrowing the 90 | search to specific directories will result in a performance increase when 91 | scanning for missing translations. 92 | 93 | ``` 94 | ui_url [string] 95 | ``` 96 | Choose the root URL where the package user interface can be accessed. All routes 97 | will be prefixed by this value. 98 | 99 | e.g. setting this value to `languages` will result in URLs such as `translations/{language}/translations` 100 | 101 | ``` 102 | database.languages_table 103 | ``` 104 | Choose the name of the languages table when using the database driver. 105 | 106 | ``` 107 | database.translations_table 108 | ``` 109 | Choose the name of the translations table when using the database driver. 110 | 111 | ### Drivers 112 | 113 | #### File 114 | Utitlises Laravel's native php array and JSON based language files and exposes a 115 | user interface to manage the enclosed translations. Add and update languages and translations 116 | using either the user interface or the built-in [Artisan commands](https://laravel.com/docs/5.7/artisan). 117 | 118 | #### Database 119 | The database driver takes all of the functionality of Laravel's file based 120 | language files, but moves the storage to the database, utilising the connection 121 | configured for your Laravel application. 122 | 123 | It also replaces the translation loader in the container so all of Laravel's 124 | translation retrieval methods (`__()`, `trans()`, `@lang()`, etc) will load the 125 | relevant strings from the database rather than the files without the need to 126 | change any code in your application. It's a like for like swap. 127 | 128 | To utilise the database driver, make sure to update the database table names in 129 | the configuration file and run the migrations. 130 | 131 | #### Changing Drivers from File (default) to Database 132 | 133 | 1. Update the driver to use database in `./config/translation.php`. 134 | 135 | ```php 136 | 'driver' => 'database' 137 | ``` 138 | 139 | 2. Run the migration to add translations and languages tables. 140 | 141 | ```shell 142 | php artisan migrate 143 | ``` 144 | 145 | 3. Run the following command and folow the prompts to synchronise the translations between drivers. 146 | 147 | ```shell 148 | php artisan translation:sync-translations 149 | ``` 150 | 151 | 4. A few questions will be prompted which have to be answered. See the screenshot below: 152 | 153 | ### User interface 154 | Navigate to http://your-project.test/languages (update `languages` to match the 155 | `translation.ui_url` configuration setting) and use the interface to manage 156 | your translations. 157 | 158 | First, click on the language you wish to edit. On the subsequent page, find the 159 | translation you want to edit and click on the pencil icon or on the text and 160 | make your edits. As soon as you remove focus from the input, your translation 161 | will be saved, indicated by the green check icon. 162 | 163 | ### Artisan Commands 164 | The package ships with a series of Artisan commands which assist with 165 | translation management. 166 | 167 | ``` 168 | translation:add-language 169 | ``` 170 | Add a new language to the application. 171 | 172 | ``` 173 | translation:add-translation-key 174 | ``` 175 | Add a new language key for the application. 176 | 177 | ``` 178 | translation:list-languages 179 | ``` 180 | List all of the available languages in the application. 181 | 182 | ``` 183 | translation:list-missing-translation-keys 184 | ``` 185 | List all of the translation keys in the app which don't have a corresponding translation. 186 | 187 | ``` 188 | translation:sync-translations 189 | ``` 190 | Synchronise translations between drivers. This is useful if you have an exisitng 191 | application using the native file based language files and wish to move to the 192 | database driver. Running this command will take all of the translations from the 193 | language files and insert them in to the database. 194 | 195 | ``` 196 | translation:sync-missing-translation-keys 197 | ``` 198 | This command will scan your project (using the paths supplied in the 199 | configuration file) and create all of the missing translation keys. This can be 200 | run for all languages or a single language. 201 | 202 | -------------------------------------------------------------------------------- /resources/assets/css/main.css: -------------------------------------------------------------------------------- 1 | /** 2 | * This injects Tailwind's base styles, which is a combination of 3 | * Normalize.css and some additional base styles. 4 | * 5 | * You can see the styles here: 6 | * https://github.com/tailwindcss/tailwindcss/blob/master/css/preflight.css 7 | * 8 | * If using `postcss-import`, use this import instead: 9 | * 10 | * @import "tailwindcss/preflight"; 11 | */ 12 | 13 | @tailwind preflight; 14 | /** 15 | * This injects any component classes registered by plugins. 16 | * 17 | * If using `postcss-import`, use this import instead: 18 | * 19 | * @import "tailwindcss/components"; 20 | */ 21 | 22 | @tailwind components; 23 | /** 24 | * Here you would add any of your custom component classes; stuff that you'd 25 | * want loaded *before* the utilities so that the utilities could still 26 | * override them. 27 | * 28 | * Example: 29 | * 30 | * .btn { ... } 31 | * .form-input { ... } 32 | * 33 | * Or if using a preprocessor or `postcss-import`: 34 | * 35 | * @import "components/buttons"; 36 | * @import "components/forms"; 37 | */ 38 | 39 | /** 40 | * This injects all of Tailwind's utility classes, generated based on your 41 | * config file. 42 | * 43 | * If using `postcss-import`, use this import instead: 44 | * 45 | * @import "tailwindcss/utilities"; 46 | */ 47 | 48 | @tailwind utilities; 49 | /** 50 | * Here you would add any custom utilities you need that don't come out of the 51 | * box with Tailwind. 52 | * 53 | * Example : 54 | * 55 | * .bg-pattern-graph-paper { ... } 56 | * .skew-45 { ... } 57 | * 58 | * Or if using a preprocessor or `postcss-import`: 59 | * 60 | * @import "utilities/background-patterns"; 61 | * @import "utilities/skew-transforms"; 62 | */ 63 | 64 | body { 65 | @apply bg-grey-lighter text-grey-darkest 66 | } 67 | 68 | ul { 69 | list-style-type: none; 70 | @apply flex; 71 | } 72 | 73 | li { 74 | @apply px-4; 75 | } 76 | 77 | a { 78 | @apply text-blue; 79 | } 80 | 81 | nav.header { 82 | background: linear-gradient(90deg, #125b93, #2891c4); 83 | @apply border-b flex items-center h-16 text-white w-full 84 | } 85 | 86 | nav a { 87 | @apply .opacity-75 text-white no-underline flex items-center 88 | } 89 | 90 | nav a.active { 91 | @apply opacity-100 92 | } 93 | 94 | nav a:hover { 95 | @apply opacity-100 underline 96 | } 97 | 98 | .panel { 99 | @apply bg-white rounded m-6 shadow text-grey-dark 100 | } 101 | 102 | .panel-header { 103 | @apply p-4 text-lg border-b flex items-center font-thin 104 | } 105 | 106 | .panel-footer { 107 | @apply border-t bg-grey-lighter p-4 108 | } 109 | 110 | .panel-body table { 111 | @apply w-full table-fixed; 112 | } 113 | 114 | .panel-body th, 115 | .panel-body td { 116 | @apply text-left p-4 overflow-x-auto 117 | } 118 | 119 | .panel-body th { 120 | @apply text-grey-darker 121 | } 122 | 123 | .panel-body td { 124 | @apply font-thin align-top 125 | } 126 | 127 | .panel-body tr { 128 | @apply border-b 129 | } 130 | 131 | .panel-body thead tr { 132 | @apply bg-grey-lighter 133 | } 134 | 135 | .panel-body tbody tr:nth-child(even) { 136 | @apply bg-grey-lighter 137 | } 138 | 139 | .panel-body tbody tr:hover, 140 | .panel-body tbody tr:nth-child(even):hover { 141 | @apply bg-blue-lightest 142 | } 143 | 144 | .panel-body tbody tr:last-child { 145 | @apply border-none 146 | } 147 | 148 | .panel-body td textarea { 149 | overflow-wrap: inherit; 150 | @apply border-none resize-none bg-transparent text-grey-darker w-full font-thin h-auto p-0 151 | } 152 | 153 | .panel-body td textarea.active { 154 | @apply w-full rounded h-32 p-2 border border-solid border-grey 155 | } 156 | 157 | .panel-body td textarea:focus { 158 | @apply outline-none; 159 | } 160 | 161 | .button { 162 | @apply bg-transparent text-grey-darker py-2 px-4 border border-grey rounded text-sm font-bold no-underline 163 | } 164 | 165 | .button:hover { 166 | @apply text-blue 167 | } 168 | 169 | .button-blue { 170 | @apply bg-blue text-white border-blue 171 | } 172 | 173 | .button-blue:hover { 174 | @apply text-white bg-blue-dark 175 | } 176 | 177 | .input-group { 178 | @apply w-full mb-6 179 | } 180 | 181 | .input-group label { 182 | @apply block uppercase tracking-wide text-grey-darker text-xs font-bold mb-2 183 | } 184 | 185 | .input-group input { 186 | @apply appearance-none block w-full bg-grey-lighter text-grey-darker border rounded py-3 px-4 mb-3 leading-tight 187 | } 188 | 189 | .input-group:last-child { 190 | @apply mb-0 191 | } 192 | 193 | .input-group input.error { 194 | @apply border-red 195 | } 196 | 197 | .input-group .error-text { 198 | @apply text-red text-xs italic 199 | } 200 | 201 | .select-group { 202 | @apply relative mr-2 203 | } 204 | 205 | .select-group:last-child { 206 | @apply m-0 207 | } 208 | 209 | .select-group select { 210 | @apply text-base block appearance-none bg-white border text-grey-darker uppercase py-2 px-4 pr-8 rounded leading-tight max-w-xs font-thin 211 | } 212 | 213 | .select-group select:focus { 214 | @apply outline-none border-grey 215 | } 216 | 217 | .select-group .caret { 218 | @apply pointer-events-none absolute pin-y pin-r flex items-center px-2 text-grey-darker 219 | } 220 | 221 | .select-group .caret svg { 222 | @apply fill-current h-4 w-4 223 | } 224 | 225 | .w-1\/10 { 226 | width: 10%; 227 | } 228 | 229 | .search-input { 230 | background: url('data:image/svg+xml;charset=utf8,'); 231 | @apply bg-grey-lighter rounded pl-10 py-2 pr-4 bg-no-repeat bg-contain transition border text-grey-darker font-thin w-full 232 | } 233 | 234 | .search-input:focus { 235 | @apply outline-none bg-white border border-grey-light 236 | } 237 | 238 | .transition { 239 | transition: all .1s ease-in; 240 | } 241 | 242 | .search { 243 | max-width: 500px; 244 | @apply mx-2 relative flex-1 245 | } 246 | 247 | ul.search-results { 248 | max-height: 300px; 249 | @apply font-thin pl-0 block absolute w-full bg-grey-lighter border border-t-0 rounded rounded-t-none overflow-x-hidden overflow-y-scroll 250 | } 251 | 252 | ul.search-results li { 253 | @apply px-4 py-2 border-b pl-10; 254 | } 255 | 256 | ul.search-results li:last-child { 257 | @apply border-b-0; 258 | } 259 | 260 | .search.has-results .search-input { 261 | @apply border-b-0 rounded-b-none bg-white 262 | } -------------------------------------------------------------------------------- /resources/assets/js/app.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * First we will load all of this project's JavaScript dependencies which 4 | * includes Vue and other libraries. It is a great starting point when 5 | * building robust, powerful web applications using Vue and Laravel. 6 | */ 7 | 8 | // require('./bootstrap'); 9 | 10 | /** 11 | * We'll load the axios HTTP library which allows us to easily issue requests 12 | * to our Laravel back-end. This library automatically handles sending the 13 | * CSRF token as a header based on the value of the "XSRF" token cookie. 14 | */ 15 | 16 | window.axios = require('axios'); 17 | 18 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 19 | 20 | /** 21 | * Next we will register the CSRF Token as a common header with Axios so that 22 | * all outgoing HTTP requests automatically have it attached. This is just 23 | * a simple convenience so we don't have to attach every token manually. 24 | */ 25 | 26 | let token = document.head.querySelector('meta[name="csrf-token"]'); 27 | 28 | if (token) { 29 | window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; 30 | } else { 31 | console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token'); 32 | } 33 | 34 | window.Vue = require('vue'); 35 | 36 | /** 37 | * Next, we will create a fresh Vue application instance and attach it to 38 | * the page. Then, you may begin adding components to this application 39 | * or customize the JavaScript scaffolding to fit your unique needs. 40 | */ 41 | 42 | Vue.component('translation-input', require('./components/TranslationInput.vue').default); 43 | 44 | const app = new Vue({ 45 | el: '#app', 46 | 47 | data: function () { 48 | return { 49 | showAdvancedOptions: false, 50 | } 51 | }, 52 | 53 | methods: { 54 | submit: function(event) { 55 | event.target.form.submit(); 56 | }, 57 | 58 | toggleAdvancedOptions(event) { 59 | event.preventDefault(); 60 | this.showAdvancedOptions = !this.showAdvancedOptions; 61 | } 62 | } 63 | }); 64 | -------------------------------------------------------------------------------- /resources/assets/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | 2 | window._ = require('lodash'); 3 | window.Popper = require('popper.js').default; 4 | 5 | /** 6 | * We'll load jQuery and the Bootstrap jQuery plugin which provides support 7 | * for JavaScript based Bootstrap features such as modals and tabs. This 8 | * code may be modified to fit the specific needs of your application. 9 | */ 10 | 11 | try { 12 | window.$ = window.jQuery = require('jquery'); 13 | 14 | require('bootstrap'); 15 | } catch (e) {} 16 | 17 | /** 18 | * We'll load the axios HTTP library which allows us to easily issue requests 19 | * to our Laravel back-end. This library automatically handles sending the 20 | * CSRF token as a header based on the value of the "XSRF" token cookie. 21 | */ 22 | 23 | window.axios = require('axios'); 24 | 25 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 26 | 27 | /** 28 | * Next we will register the CSRF Token as a common header with Axios so that 29 | * all outgoing HTTP requests automatically have it attached. This is just 30 | * a simple convenience so we don't have to attach every token manually. 31 | */ 32 | 33 | let token = document.head.querySelector('meta[name="csrf-token"]'); 34 | 35 | if (token) { 36 | window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; 37 | } else { 38 | console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token'); 39 | } 40 | 41 | /** 42 | * Echo exposes an expressive API for subscribing to channels and listening 43 | * for events that are broadcast by Laravel. Echo and event broadcasting 44 | * allows your team to easily build robust real-time web applications. 45 | */ 46 | 47 | // import Echo from 'laravel-echo' 48 | 49 | // window.Pusher = require('pusher-js'); 50 | 51 | // window.Echo = new Echo({ 52 | // broadcaster: 'pusher', 53 | // key: process.env.MIX_PUSHER_APP_KEY, 54 | // cluster: process.env.MIX_PUSHER_APP_CLUSTER, 55 | // encrypted: true 56 | // }); 57 | -------------------------------------------------------------------------------- /resources/assets/js/components/TranslationInput.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 79 | -------------------------------------------------------------------------------- /resources/helpers.php: -------------------------------------------------------------------------------- 1 | $value) { 56 | if (is_array($value) || $value instanceof Illuminate\Support\Collection) { 57 | if (! isset($arrayTwo[$key])) { 58 | $difference[$key] = $value; 59 | } elseif (! (is_array($arrayTwo[$key]) || $arrayTwo[$key] instanceof Illuminate\Support\Collection)) { 60 | $difference[$key] = $value; 61 | } else { 62 | $new_diff = array_diff_assoc_recursive($value, $arrayTwo[$key]); 63 | if ($new_diff != false) { 64 | $difference[$key] = $new_diff; 65 | } 66 | } 67 | } elseif (! isset($arrayTwo[$key])) { 68 | $difference[$key] = $value; 69 | } 70 | } 71 | 72 | return $difference; 73 | } 74 | } 75 | 76 | if (! function_exists('str_before')) { 77 | /** 78 | * Get the portion of a string before a given value. 79 | * 80 | * @param string $subject 81 | * @param string $search 82 | * @return string 83 | */ 84 | function str_before($subject, $search) 85 | { 86 | return $search === '' ? $subject : explode($search, $subject)[0]; 87 | } 88 | } 89 | 90 | // Array undot 91 | if (! function_exists('array_undot')) { 92 | /** 93 | * Expands a single level array with dot notation into a multi-dimensional array. 94 | * 95 | * @param array $dotNotationArray 96 | * @return array 97 | */ 98 | function array_undot(array $dotNotationArray) 99 | { 100 | $array = []; 101 | foreach ($dotNotationArray as $key => $value) { 102 | // if there is a space after the dot, this could legitimately be 103 | // a single key and not nested. 104 | if (count(explode('. ', $key)) > 1) { 105 | $array[$key] = $value; 106 | } else { 107 | Arr::set($array, $key, $value); 108 | } 109 | } 110 | 111 | return $array; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /resources/lang/de/errors.php: -------------------------------------------------------------------------------- 1 | 'Die Sprache { :language } ist bereits vorhanden', 5 | 'key_exists' => 'Der Übersetzungsschlüssel { :key } ist bereits vorhanden', 6 | ]; 7 | -------------------------------------------------------------------------------- /resources/lang/de/translation.php: -------------------------------------------------------------------------------- 1 | 'Sprachen', 5 | 'language' => 'Sprache', 6 | 'type' => 'Typ', 7 | 'file' => 'Datei', 8 | 'key' => 'Schlüssel', 9 | 'prompt_language' => 'Geben Sie den Sprachcode ein, den Sie hinzufügen möchten (z.B. en).', 10 | 'language_added' => 'Neue Sprache wurde erfolgreich hinzugefügt 🙌', 11 | 'prompt_language_for_key' => 'Geben Sie die Sprache für den Schlüssel ein (z.B. en)', 12 | 'prompt_type' => 'Ist das ein Json- oder Array-Schlüssel?', 13 | 'prompt_file' => 'In welcher Datei wird das gespeichert?', 14 | 'prompt_key' => 'Was ist der Schlüssel für diese Übersetzung?', 15 | 'prompt_value' => 'Was ist der Wert für diese Übersetzung', 16 | 'type_error' => 'Übersetzungstyp muss json oder ein Array sein', 17 | 'language_key_added' => 'Neuer Sprachenschlüssel wurde erfolgreich hinzugefügt 👏', 18 | 'no_missing_keys' => 'Es fehlen keine Übersetzungsschlüssel in der App 🎉', 19 | 'keys_synced' => 'Fehlende Schlüssel erfolgreich synchronisiert 🎊', 20 | 'search' => 'Alle Übersetzungen suchen', 21 | 'translations' => 'Übersetzung', 22 | 'language_name' => 'Name', 23 | 'locale' => 'locale', 24 | 'add' => '+ Hinzufügen', 25 | 'add_language' => 'Neue Sprache hinzufügen', 26 | 'save' => 'save', 27 | 'language_exists' => 'Das :attribute ist bereits vorhanden.', 28 | 'uh_oh' => 'Etwas ist nicht ganz richtig', 29 | 'group_single' => 'Gruppe / Single', 30 | 'Gruppe' => 'Gruppe', 31 | 'single' => 'single', 32 | 'value' => 'Wert', 33 | 'namespace' => 'Namespace', 34 | 'synchronisieren' => 'Übersetzungen synchronisieren ⏳', 35 | 'synced' => 'Übersetzungen wurden synchronisiert 😎', 36 | 'add_translation' => 'Übersetzung hinzufügen', 37 | 'translation_added' => 'Neue Übersetzung erfolgreich hinzugefügt 🙌', 38 | 'namespace_label' => 'Namespace (optional)', 39 | 'group_label' => 'Gruppe (optional)', 40 | 'key_label' => 'Schlüssel', 41 | 'value_label' => 'Wert', 42 | 'namespace_placeholder' => 'z.B. my_package', 43 | 'group_placeholder' => 'z.B. validation', 44 | 'key_placeholder' => 'z.B. invalid_key', 45 | 'value_placeholder' => 'z.B. Schlüssel müssen eine einzige Zeichenfolge sein', 46 | 'advanced_options' => 'Erweiterte Optionen umschalten', 47 | ]; 48 | -------------------------------------------------------------------------------- /resources/lang/en/errors.php: -------------------------------------------------------------------------------- 1 | 'The language { :language } already exists', 5 | 'key_exists' => 'The translation key { :key } already exists', 6 | ]; 7 | -------------------------------------------------------------------------------- /resources/lang/en/translation.php: -------------------------------------------------------------------------------- 1 | 'Languages', 5 | 'language' => 'Language', 6 | 'type' => 'Type', 7 | 'file' => 'File', 8 | 'key' => 'Key', 9 | 'prompt_language' => 'Enter the language code you would like to add (e.g. en)', 10 | 'language_added' => 'New language added successfully 🙌', 11 | 'prompt_language_for_key' => 'Enter the language for the key (e.g. en)', 12 | 'prompt_type' => 'Is this a json or array key?', 13 | 'prompt_file' => 'Which file will this be stored in?', 14 | 'prompt_key' => 'What is the key for this translation?', 15 | 'prompt_value' => 'What is the value for this translation', 16 | 'type_error' => 'Translation type must be json or array', 17 | 'language_key_added' => 'New language key added successfully 👏', 18 | 'no_missing_keys' => 'There are no missing translation keys in the app 🎉', 19 | 'keys_synced' => 'Missing keys synchronised successfully 🎊', 20 | 'search' => 'Search all translations', 21 | 'translations' => 'Translation', 22 | 'language_name' => 'Name', 23 | 'locale' => 'Locale', 24 | 'add' => '+ Add', 25 | 'add_language' => 'Add a new language', 26 | 'save' => 'Save', 27 | 'language_exists' => 'The :attribute already exists.', 28 | 'uh_oh' => 'Something\'s not quite right', 29 | 'group_single' => 'Group / Single', 30 | 'group' => 'Group', 31 | 'single' => 'Single', 32 | 'value' => 'Value', 33 | 'namespace' => 'Namespace', 34 | 'add_translation' => 'Add a translation', 35 | 'translation_added' => 'New translation added successfull 🙌', 36 | 'namespace_label' => 'Namespace (Optional)', 37 | 'group_label' => 'Group (Optional)', 38 | 'key_label' => 'Key', 39 | 'value_label' => 'Value', 40 | 'namespace_placeholder' => 'e.g. my_package', 41 | 'group_placeholder' => 'e.g. validation', 42 | 'key_placeholder' => 'e.g. invalid_key', 43 | 'value_placeholder' => 'e.g. Keys must be a single string', 44 | 'advanced_options' => 'Toggle advanced options', 45 | ]; 46 | -------------------------------------------------------------------------------- /resources/lang/fr/errors.php: -------------------------------------------------------------------------------- 1 | 'La clé de traduction { :key } existe déjà', 5 | 'language_exists' => 'La langue { :language } existe déjà', 6 | ]; 7 | -------------------------------------------------------------------------------- /resources/lang/fr/translation.php: -------------------------------------------------------------------------------- 1 | '+ Ajouter', 5 | 'add_language' => 'Ajouter une nouvelle langue', 6 | 'add_translation' => 'Ajouter une traduction', 7 | 'advanced_options' => 'Afficher les options avancées', 8 | 'file' => 'Fichier', 9 | 'group' => 'Groupe', 10 | 'group_label' => 'Groupe (Optionnel)', 11 | 'group_placeholder' => 'Ex: validation', 12 | 'group_single' => 'Groupe / Unique', 13 | 'key' => 'Clé', 14 | 'key_label' => 'Clé', 15 | 'key_placeholder' => 'Par exemple : invalid_key', 16 | 'keys_synced' => 'Clés manquantes synchronisées avec succès 🎊', 17 | 'language' => 'Langue', 18 | 'language_added' => 'Nouvelle langue ajoutée avec succés 🙌', 19 | 'language_exists' => 'Le :attribute existe déjà.', 20 | 'language_key_added' => 'Nouvelle clé dans la langue ajoutée avec succès 👏', 21 | 'language_name' => 'Nom', 22 | 'languages' => 'Langues', 23 | 'locale' => 'Locale', 24 | 'namespace' => 'Namespace', 25 | 'namespace_label' => 'Namespace (Optionnel)', 26 | 'namespace_placeholder' => 'Par exemple : my_package', 27 | 'no_missing_keys' => 'Il ne manque aucune clé de traduction dans l\'application 🎉', 28 | 'prompt_file' => 'Dans quel fichier sera t\'elle stockée ?', 29 | 'prompt_key' => 'Quelle est la clé de cette traduction ?', 30 | 'prompt_language' => 'Entrez le code langue que vous aimeriez ajouter (Ex: fr)', 31 | 'prompt_language_for_key' => 'Entrez la langue pour la clé (Ex: fr)', 32 | 'prompt_type' => 'Est-ce une clé Json ou Array ?', 33 | 'prompt_value' => 'Quelle est la valeur de la traduction', 34 | 'save' => 'Sauvegarder', 35 | 'search' => 'Rechercher toutes les traductions', 36 | 'single' => 'Unique', 37 | 'translation_added' => 'Nouvelle traduction ajoutée avec succès 🙌', 38 | 'translations' => 'Traduction', 39 | 'type' => 'Type', 40 | 'type_error' => 'Le type de traduction doit être en json ou en array', 41 | 'uh_oh' => 'Quelque chose ne fonctionne pas', 42 | 'value' => 'Valeur', 43 | 'value_label' => 'Valeur', 44 | 'value_placeholder' => 'Par exemple : Les clés doivent être une seule chaîne', 45 | ]; 46 | -------------------------------------------------------------------------------- /resources/lang/nl/errors.php: -------------------------------------------------------------------------------- 1 | 'De taal { :language } bestaat al', 5 | 'key_exists' => 'De vertaalsleutel { :key } bestaat al', 6 | ]; 7 | -------------------------------------------------------------------------------- /resources/lang/nl/translation.php: -------------------------------------------------------------------------------- 1 | 'Talen', 5 | 'language' => 'Taal', 6 | 'type' => 'Type', 7 | 'file' => 'Bestand', 8 | 'key' => 'Sleutel', 9 | 'prompt_language' => 'Voer de taalcode in die u wilt toevoegen (bijvoorbeeld: en)', 10 | 'language_added' => 'Nieuwe taal met succes toegevoegd 🙌', 11 | 'prompt_language_for_key' => 'Voer de taal voor de sleutel in (bijvoorbeeld: en)', 12 | 'prompt_type' => 'Is dit een json- of arraysleutel?', 13 | 'prompt_file' => 'In welk bestand wordt dit opgeslagen?', 14 | 'prompt_key' => 'Wat is de sleutel voor deze vertaling?', 15 | 'prompt_value' => 'Wat is de text voor deze vertaling', 16 | 'type_error' => 'Het vertaaltype moet json of array zijn', 17 | 'language_key_added' => 'Nieuwe taalcode toegevoegd 👏', 18 | 'no_missing_keys' => 'Er zijn geen ontbrekende vertaalsleutels in de app 🎉', 19 | 'keys_synced' => 'Ontbrekende toetsen gesynchroniseerd met succes 🎊', 20 | 'search' => 'Doorzoek alle vertalingen', 21 | 'translations' => 'Vertaling', 22 | 'language_name' => 'Naam', 23 | 'locale' => 'locale', 24 | 'add' => '+ Toevoegen', 25 | 'add_language' => 'Voeg een nieuwe taal toe', 26 | 'save' => 'Opslaan', 27 | 'language_exists' => 'Het kenmerk :attribute bestaat al.', 28 | 'uh_oh' => 'Er klopt iets niet helemaal', 29 | 'group_single' => 'Groep / Enkelvoudig', 30 | 'group' => 'Groep', 31 | 'single' => 'Enkelvoudig', 32 | 'value' => 'Waarde', 33 | 'namespace' => 'Namespace', 34 | 'add_translation' => 'Voeg een vertaling toe', 35 | 'translation_added' => 'Nieuwe vertaling succesvol toegevoegd 🙌', 36 | 'namespace_label' => 'Namespace (optioneel)', 37 | 'group_label' => 'Groep (optioneel)', 38 | 'key_label' => 'Sleutel', 39 | 'value_label' => 'Waarde', 40 | 'namespace_placeholder' => 'bijv. Mijn_pakket', 41 | 'group_placeholder' => 'bijv. bevestiging', 42 | 'key_placeholder' => 'bijv. ongeldige sleutel', 43 | 'value_placeholder' => 'bijv. Sleutels mogen geen spaties bevatten', 44 | 'advanced_options' => 'Schakel geavanceerde opties in', 45 | ]; 46 | -------------------------------------------------------------------------------- /resources/views/forms/search.blade.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/views/forms/select.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 13 | 14 |
15 | 16 |
17 | 18 |
-------------------------------------------------------------------------------- /resources/views/forms/text.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 5 | 13 | @if($errors->has($field)) 14 | @foreach($errors->get($field) as $error) 15 |

{!! $error !!}

16 | @endforeach 17 | @endif 18 |
-------------------------------------------------------------------------------- /resources/views/icons/globe.blade.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/views/icons/translate.blade.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/views/languages/create.blade.php: -------------------------------------------------------------------------------- 1 | @extends('translation::layout') 2 | 3 | @section('body') 4 | 5 |
6 | 7 |
8 | 9 | {{ __('translation::translation.add_language') }} 10 | 11 |
12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 |
20 | 21 | @include('translation::forms.text', ['field' => 'name', 'label' => __('translation::translation.language_name'), ]) 22 | 23 | @include('translation::forms.text', ['field' => 'locale', 'label' => __('translation::translation.locale'), ]) 24 | 25 |
26 | 27 |
28 | 29 | 36 | 37 |
38 | 39 |
40 | 41 | @endsection -------------------------------------------------------------------------------- /resources/views/languages/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('translation::layout') 2 | 3 | @section('body') 4 | 5 | @if(count($languages)) 6 | 7 |
8 | 9 |
10 | 11 | {{ __('translation::translation.languages') }} 12 | 13 | 20 | 21 |
22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | @foreach($languages as $language => $name) 36 | 37 | 40 | 45 | 46 | @endforeach 47 | 48 |
{{ __('translation::translation.language_name') }}{{ __('translation::translation.locale') }}
38 | {{ $name }} 39 | 41 | 42 | {{ $language }} 43 | 44 |
49 | 50 |
51 | 52 |
53 | 54 | @endif 55 | 56 | @endsection -------------------------------------------------------------------------------- /resources/views/languages/translations/create.blade.php: -------------------------------------------------------------------------------- 1 | @extends('translation::layout') 2 | 3 | @section('body') 4 | 5 |
6 | 7 |
8 | 9 | {{ __('translation::translation.add_translation') }} 10 | 11 |
12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 |
20 | 21 | @include('translation::forms.text', ['field' => 'group', 'label' => __('translation::translation.group_label'), 'placeholder' => __('translation::translation.group_placeholder')]) 22 | 23 | @include('translation::forms.text', ['field' => 'key', 'label' => __('translation::translation.key_label'), 'placeholder' => __('translation::translation.key_placeholder')]) 24 | 25 | @include('translation::forms.text', ['field' => 'value', 'label' => __('translation::translation.value_label'), 'placeholder' => __('translation::translation.value_placeholder')]) 26 | 27 |
28 | 29 | 30 | 31 |
32 | 33 |
34 | 35 | @include('translation::forms.text', ['field' => 'namespace', 'label' => __('translation::translation.namespace_label'), 'placeholder' => __('translation::translation.namespace_placeholder')]) 36 | 37 |
38 | 39 | 40 |
41 | 42 |
43 | 44 | 51 | 52 |
53 | 54 |
55 | 56 | @endsection -------------------------------------------------------------------------------- /resources/views/languages/translations/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('translation::layout') 2 | 3 | @section('body') 4 | 5 |
6 | 7 |
8 | 9 |
10 | 11 | {{ __('translation::translation.translations') }} 12 | 13 |
14 | 15 | @include('translation::forms.search', ['name' => 'filter', 'value' => Request::get('filter')]) 16 | 17 | @include('translation::forms.select', ['name' => 'language', 'items' => $languages, 'submit' => true, 'selected' => $language]) 18 | 19 |
20 | 21 | @include('translation::forms.select', ['name' => 'group', 'items' => $groups, 'submit' => true, 'selected' => Request::get('group'), 'optional' => true]) 22 | 23 | 24 | {{ __('translation::translation.add') }} 25 | 26 | 27 |
28 | 29 |
30 | 31 |
32 | 33 |
34 | 35 | @if(count($translations)) 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | @foreach($translations as $type => $items) 50 | 51 | @foreach($items as $group => $translations) 52 | 53 | @foreach($translations as $key => $value) 54 | 55 | @if(!is_array($value[config('app.locale')])) 56 | 57 | 58 | 59 | 60 | 69 | 70 | @endif 71 | 72 | @endforeach 73 | 74 | @endforeach 75 | 76 | @endforeach 77 | 78 | 79 |
{{ __('translation::translation.group_single') }}{{ __('translation::translation.key') }}{{ config('app.locale') }}{{ $language }}
{{ $group }}{{ $key }}{{ $value[config('app.locale')] }} 61 | 67 | 68 |
80 | 81 | @endif 82 | 83 |
84 | 85 |
86 | 87 |
88 | 89 | @endsection -------------------------------------------------------------------------------- /resources/views/layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ config('app.name') }} 9 | 10 | 11 | 12 | 13 |
14 | 15 | @include('translation::nav') 16 | @include('translation::notifications') 17 | 18 | @yield('body') 19 | 20 |
21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /resources/views/nav.blade.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/views/notifications.blade.php: -------------------------------------------------------------------------------- 1 | @if(Session::has('success')) 2 | 7 | @endif 8 | 9 | @if(Session::has('error')) 10 | 15 | @endif -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | 'JoeDixon\\Translation\\Http\\Controllers'], function ($router) { 4 | $router->get(config('translation.ui_url'), 'LanguageController@index') 5 | ->name('languages.index'); 6 | 7 | $router->get(config('translation.ui_url').'/create', 'LanguageController@create') 8 | ->name('languages.create'); 9 | 10 | $router->post(config('translation.ui_url'), 'LanguageController@store') 11 | ->name('languages.store'); 12 | 13 | $router->get(config('translation.ui_url').'/{language}/translations', 'LanguageTranslationController@index') 14 | ->name('languages.translations.index'); 15 | 16 | $router->post(config('translation.ui_url').'/{language}', 'LanguageTranslationController@update') 17 | ->name('languages.translations.update'); 18 | 19 | $router->get(config('translation.ui_url').'/{language}/translations/create', 'LanguageTranslationController@create') 20 | ->name('languages.translations.create'); 21 | 22 | $router->post(config('translation.ui_url').'/{language}/translations', 'LanguageTranslationController@store') 23 | ->name('languages.translations.store'); 24 | }); 25 | -------------------------------------------------------------------------------- /src/Console/Commands/AddLanguageCommand.php: -------------------------------------------------------------------------------- 1 | ask(__('translation::translation.prompt_language')); 30 | $name = $this->ask(__('translation::translation.prompt_name')); 31 | 32 | // attempt to add the key and fail gracefully if exception thrown 33 | try { 34 | $this->translation->addLanguage($language, $name); 35 | $this->info(__('translation::translation.language_added')); 36 | } catch (\Exception $e) { 37 | $this->error($e->getMessage()); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Console/Commands/AddTranslationKeyCommand.php: -------------------------------------------------------------------------------- 1 | ask(__('translation::translation.prompt_language_for_key')); 29 | 30 | // we know this should be single or group so we can use the `anticipate` 31 | // method to give our users a helping hand 32 | $type = $this->anticipate(__('translation::translation.prompt_type'), ['single', 'group']); 33 | 34 | // if the group type is selected, prompt for the group key 35 | if ($type === 'group') { 36 | $file = $this->ask(__('translation::translation.prompt_group')); 37 | } 38 | $key = $this->ask(__('translation::translation.prompt_key')); 39 | $value = $this->ask(__('translation::translation.prompt_value')); 40 | 41 | // attempt to add the key for single or group and fail gracefully if 42 | // exception is thrown 43 | if ($type === 'single') { 44 | try { 45 | $this->translation->addSingleTranslation($language, 'single', $key, $value); 46 | 47 | return $this->info(__('translation::translation.language_key_added')); 48 | } catch (\Exception $e) { 49 | return $this->error($e->getMessage()); 50 | } 51 | } elseif ($type === 'group') { 52 | try { 53 | $file = str_replace('.php', '', $file); 54 | $this->translation->addGroupTranslation($language, $file, $key, $value); 55 | 56 | return $this->info(__('translation::translation.language_key_added')); 57 | } catch (\Exception $e) { 58 | return $this->error($e->getMessage()); 59 | } 60 | } else { 61 | return $this->error(__('translation::translation.type_error')); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Console/Commands/BaseCommand.php: -------------------------------------------------------------------------------- 1 | translation = $translation; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Console/Commands/ListLanguagesCommand.php: -------------------------------------------------------------------------------- 1 | translation->allLanguages()->toArray(); 30 | $mappedLanguages = []; 31 | 32 | foreach ($languages as $language => $name) { 33 | $mappedLanguages[] = [$name, $language]; 34 | } 35 | 36 | // return a table of results 37 | $this->table($headers, $mappedLanguages); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Console/Commands/ListMissingTranslationKeys.php: -------------------------------------------------------------------------------- 1 | translation->allLanguages() as $language => $name) { 32 | $missingTranslations[$language] = $this->translation->findMissingTranslations($language); 33 | } 34 | 35 | // check whether or not there are any missing translations 36 | $empty = true; 37 | foreach ($missingTranslations as $language => $values) { 38 | if (! empty($values)) { 39 | $empty = false; 40 | } 41 | } 42 | 43 | // if no missing translations, inform the user and move on with your day 44 | if ($empty) { 45 | return $this->info(__('translation::translation.no_missing_keys')); 46 | } 47 | 48 | // set some headers for the table of results 49 | $headers = [__('translation::translation.language'), __('translation::translation.type'), __('translation::translation.group'), __('translation::translation.key')]; 50 | 51 | // iterate over each of the missing languages 52 | foreach ($missingTranslations as $language => $types) { 53 | // iterate over each of the file types (json or array) 54 | foreach ($types as $type => $keys) { 55 | // iterate over each of the keys 56 | foreach ($keys as $key => $value) { 57 | // populate the array with the relevant data to fill the table 58 | foreach ($value as $k => $v) { 59 | $rows[] = [$language, $type, $key, $k]; 60 | } 61 | } 62 | } 63 | } 64 | 65 | // render the table of results 66 | $this->table($headers, $rows); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Console/Commands/SynchroniseMissingTranslationKeys.php: -------------------------------------------------------------------------------- 1 | argument('language') ?: false; 29 | 30 | try { 31 | // if we have a language, pass it in, if not the method will 32 | // automagically sync all languages 33 | $this->translation->saveMissingTranslations($language); 34 | 35 | return $this->info(__('translation::translation.keys_synced')); 36 | } catch (\Exception $e) { 37 | return $this->error($e->getMessage()); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Console/Commands/SynchroniseTranslationsCommand.php: -------------------------------------------------------------------------------- 1 | scanner = $scanner; 68 | $this->translation = $translation; 69 | } 70 | 71 | /** 72 | * Execute the console command. 73 | * 74 | * @return mixed 75 | */ 76 | public function handle() 77 | { 78 | $languages = array_keys($this->translation->allLanguages()->toArray()); 79 | 80 | // If a valid from driver has been specified as an argument. 81 | if ($this->argument('from') && in_array($this->argument('from'), $this->drivers)) { 82 | $this->fromDriver = $this->argument('from'); 83 | } 84 | 85 | // When the from driver will be entered manually or if the argument is invalid. 86 | else { 87 | $this->fromDriver = $this->anticipate('Which driver would you like to take translations from?', $this->drivers); 88 | 89 | if (! in_array($this->fromDriver, $this->drivers)) { 90 | return $this->error('Invalid driver'); 91 | } 92 | } 93 | 94 | // Create the driver. 95 | $this->fromDriver = $this->createDriver($this->fromDriver); 96 | 97 | // When the to driver has been specified. 98 | if ($this->argument('to') && in_array($this->argument('to'), $this->drivers)) { 99 | $this->toDriver = $this->argument('to'); 100 | } 101 | 102 | // When the to driver will be entered manually. 103 | else { 104 | $this->toDriver = $this->anticipate('Which driver would you like to add the translations to?', $this->drivers); 105 | 106 | if (! in_array($this->toDriver, $this->drivers)) { 107 | return $this->error('Invalid driver'); 108 | } 109 | } 110 | 111 | // Create the driver. 112 | $this->toDriver = $this->createDriver($this->toDriver); 113 | 114 | // If the language argument is set. 115 | if ($this->argument('language')) { 116 | // If all languages should be synced. 117 | if ($this->argument('language') == 'all') { 118 | $language = false; 119 | } 120 | // When a specific language is set and is valid. 121 | elseif (in_array($this->argument('language'), $languages)) { 122 | $language = $this->argument('language'); 123 | } else { 124 | return $this->error('Invalid language'); 125 | } 126 | } // When the language will be entered manually or if the argument is invalid. 127 | else { 128 | $language = $this->anticipate('Which language? (leave blank for all)', $languages); 129 | 130 | if ($language && ! in_array($language, $languages)) { 131 | return $this->error('Invalid language'); 132 | } 133 | } 134 | 135 | $this->line('Syncing translations'); 136 | 137 | // If a specific language is set. 138 | if ($language) { 139 | $this->mergeTranslations($this->toDriver, $language, $this->fromDriver->allTranslationsFor($language)); 140 | } // Else process all languages. 141 | else { 142 | $translations = $this->mergeLanguages($this->toDriver, $this->fromDriver->allTranslations()); 143 | } 144 | 145 | $this->info('Translations have been synced'); 146 | } 147 | 148 | private function createDriver($driver) 149 | { 150 | if ($driver === 'file') { 151 | return new File(new Filesystem, app('path.lang'), config('app.locale'), $this->scanner); 152 | } 153 | 154 | return new Database(config('app.locale'), $this->scanner); 155 | } 156 | 157 | private function mergeLanguages($driver, $languages) 158 | { 159 | foreach ($languages as $language => $translations) { 160 | $this->mergeTranslations($driver, $language, $translations); 161 | } 162 | } 163 | 164 | private function mergeTranslations($driver, $language, $translations) 165 | { 166 | $this->mergeGroupTranslations($driver, $language, $translations['group']); 167 | $this->mergeSingleTranslations($driver, $language, $translations['single']); 168 | } 169 | 170 | private function mergeGroupTranslations($driver, $language, $groups) 171 | { 172 | foreach ($groups as $group => $translations) { 173 | foreach ($translations as $key => $value) { 174 | if (is_array($value)) { 175 | continue; 176 | } 177 | $driver->addGroupTranslation($language, $group, $key, $value); 178 | } 179 | } 180 | } 181 | 182 | private function mergeSingleTranslations($driver, $language, $vendors) 183 | { 184 | foreach ($vendors as $vendor => $translations) { 185 | foreach ($translations as $key => $value) { 186 | if (is_array($value)) { 187 | continue; 188 | } 189 | $driver->addSingleTranslation($language, $vendor, $key, $value); 190 | } 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/ContractDatabaseLoader.php: -------------------------------------------------------------------------------- 1 | translation = $translation; 15 | } 16 | 17 | /** 18 | * Load the messages for the given locale. 19 | * 20 | * @param string $locale 21 | * @param string $group 22 | * @param string $namespace 23 | * @return array 24 | */ 25 | public function load($locale, $group, $namespace = null) 26 | { 27 | if ($group == '*' && $namespace == '*') { 28 | return $this->translation->getSingleTranslationsFor($locale)->get('single', collect())->toArray(); 29 | } 30 | 31 | if (is_null($namespace) || $namespace == '*') { 32 | return $this->translation->getGroupTranslationsFor($locale)->filter(function ($value, $key) use ($group) { 33 | return $key === $group; 34 | })->first(); 35 | } 36 | 37 | return $this->translation->getGroupTranslationsFor($locale)->filter(function ($value, $key) use ($group, $namespace) { 38 | return $key === "{$namespace}::{$group}"; 39 | })->first(); 40 | } 41 | 42 | /** 43 | * Add a new namespace to the loader. 44 | * 45 | * @param string $namespace 46 | * @param string $hint 47 | * @return void 48 | */ 49 | public function addNamespace($namespace, $hint) 50 | { 51 | // 52 | } 53 | 54 | /** 55 | * Add a new JSON path to the loader. 56 | * 57 | * @param string $path 58 | * @return void 59 | */ 60 | public function addJsonPath($path) 61 | { 62 | // 63 | } 64 | 65 | /** 66 | * Get an array of all the registered namespaces. 67 | * 68 | * @return array 69 | */ 70 | public function namespaces() 71 | { 72 | return []; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Drivers/Database.php: -------------------------------------------------------------------------------- 1 | sourceLanguage = $sourceLanguage; 24 | $this->scanner = $scanner; 25 | } 26 | 27 | /** 28 | * Get all languages from the application. 29 | * 30 | * @return Collection 31 | */ 32 | public function allLanguages() 33 | { 34 | return Language::all()->mapWithKeys(function ($language) { 35 | return [$language->language => $language->name ?: $language->language]; 36 | }); 37 | } 38 | 39 | /** 40 | * Get all group translations from the application. 41 | * 42 | * @return array 43 | */ 44 | public function allGroup($language) 45 | { 46 | $groups = TranslationModel::getGroupsForLanguage($language); 47 | 48 | return $groups->map(function ($translation) { 49 | return $translation->group; 50 | }); 51 | } 52 | 53 | /** 54 | * Get all the translations from the application. 55 | * 56 | * @return Collection 57 | */ 58 | public function allTranslations() 59 | { 60 | return $this->allLanguages()->mapWithKeys(function ($name, $language) { 61 | return [$language => $this->allTranslationsFor($language)]; 62 | }); 63 | } 64 | 65 | /** 66 | * Get all translations for a particular language. 67 | * 68 | * @param string $language 69 | * @return Collection 70 | */ 71 | public function allTranslationsFor($language) 72 | { 73 | return Collection::make([ 74 | 'group' => $this->getGroupTranslationsFor($language), 75 | 'single' => $this->getSingleTranslationsFor($language), 76 | ]); 77 | } 78 | 79 | /** 80 | * Add a new language to the application. 81 | * 82 | * @param string $language 83 | * @return void 84 | */ 85 | public function addLanguage($language, $name = null) 86 | { 87 | if ($this->languageExists($language)) { 88 | throw new LanguageExistsException(__('translation::errors.language_exists', ['language' => $language])); 89 | } 90 | 91 | Language::create([ 92 | 'language' => $language, 93 | 'name' => $name, 94 | ]); 95 | } 96 | 97 | /** 98 | * Add a new group type translation. 99 | * 100 | * @param string $language 101 | * @param string $key 102 | * @param string $value 103 | * @return void 104 | */ 105 | public function addGroupTranslation($language, $group, $key, $value = '') 106 | { 107 | if (! $this->languageExists($language)) { 108 | $this->addLanguage($language); 109 | } 110 | 111 | Language::where('language', $language) 112 | ->first() 113 | ->translations() 114 | ->updateOrCreate([ 115 | 'group' => $group, 116 | 'key' => $key, 117 | ], [ 118 | 'group' => $group, 119 | 'key' => $key, 120 | 'value' => $value, 121 | ]); 122 | } 123 | 124 | /** 125 | * Add a new single type translation. 126 | * 127 | * @param string $language 128 | * @param string $key 129 | * @param string $value 130 | * @return void 131 | */ 132 | public function addSingleTranslation($language, $vendor, $key, $value = '') 133 | { 134 | if (! $this->languageExists($language)) { 135 | $this->addLanguage($language); 136 | } 137 | 138 | Language::where('language', $language) 139 | ->first() 140 | ->translations() 141 | ->updateOrCreate([ 142 | 'group' => $vendor, 143 | 'key' => $key, 144 | ], [ 145 | 'key' => $key, 146 | 'value' => $value, 147 | ]); 148 | } 149 | 150 | /** 151 | * Get all of the single translations for a given language. 152 | * 153 | * @param string $language 154 | * @return Collection 155 | */ 156 | public function getSingleTranslationsFor($language) 157 | { 158 | $translations = $this->getLanguage($language) 159 | ->translations() 160 | ->where('group', 'like', '%single') 161 | ->orWhereNull('group') 162 | ->get() 163 | ->groupBy('group'); 164 | 165 | // if there is no group, this is a legacy translation so we need to 166 | // update to 'single'. We do this here so it only happens once. 167 | if ($this->hasLegacyGroups($translations->keys())) { 168 | TranslationModel::whereNull('group')->update(['group' => 'single']); 169 | // if any legacy groups exist, rerun the method so we get the 170 | // updated keys. 171 | return $this->getSingleTranslationsFor($language); 172 | } 173 | 174 | return $translations->map(function ($translations, $group) { 175 | return $translations->mapWithKeys(function ($translation) { 176 | return [$translation->key => $translation->value]; 177 | }); 178 | }); 179 | } 180 | 181 | /** 182 | * Get all of the group translations for a given language. 183 | * 184 | * @param string $language 185 | * @return Collection 186 | */ 187 | public function getGroupTranslationsFor($language) 188 | { 189 | if (isset($this->groupTranslationCache[$language])) { 190 | return $this->groupTranslationCache[$language]; 191 | } 192 | 193 | $languageModel = $this->getLanguage($language); 194 | 195 | if (is_null($languageModel)) { 196 | return collect(); 197 | } 198 | 199 | $translations = $languageModel 200 | ->translations() 201 | ->whereNotNull('group') 202 | ->where('group', 'not like', '%single') 203 | ->get() 204 | ->groupBy('group'); 205 | 206 | $result = $translations->map(function ($translations) { 207 | return $translations->mapWithKeys(function ($translation) { 208 | return [$translation->key => $translation->value]; 209 | }); 210 | }); 211 | 212 | $this->groupTranslationCache[$language] = $result; 213 | 214 | return $result; 215 | } 216 | 217 | /** 218 | * Determine whether or not a language exists. 219 | * 220 | * @param string $language 221 | * @return bool 222 | */ 223 | public function languageExists($language) 224 | { 225 | return $this->getLanguage($language) ? true : false; 226 | } 227 | 228 | /** 229 | * Get a collection of group names for a given language. 230 | * 231 | * @param string $language 232 | * @return Collection 233 | */ 234 | public function getGroupsFor($language) 235 | { 236 | return $this->allGroup($language); 237 | } 238 | 239 | /** 240 | * Get a language from the database. 241 | * 242 | * @param string $language 243 | * @return Language 244 | */ 245 | private function getLanguage($language) 246 | { 247 | if (isset($this->languageCache[$language])) { 248 | return $this->languageCache[$language]; 249 | } 250 | 251 | // Some constallation of composer packages can lead to our code being executed 252 | // as a dependency of running migrations. That's why we need to be able to 253 | // handle the case where the database is empty / our tables don't exist: 254 | try { 255 | $result = Language::where('language', $language)->first(); 256 | } catch (Throwable) { 257 | $result = null; 258 | } 259 | 260 | $this->languageCache[$language] = $result; 261 | 262 | return $result; 263 | } 264 | 265 | /** 266 | * Determine if a set of single translations contains any legacy groups. 267 | * Previously, this was handled by setting the group value to NULL, now 268 | * we use 'single' to cater for vendor JSON language files. 269 | * 270 | * @param Collection $groups 271 | * @return bool 272 | */ 273 | private function hasLegacyGroups($groups) 274 | { 275 | return $groups->filter(function ($key) { 276 | return $key === ''; 277 | })->count() > 0; 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/Drivers/DriverInterface.php: -------------------------------------------------------------------------------- 1 | disk = $disk; 24 | $this->languageFilesPath = $languageFilesPath; 25 | $this->sourceLanguage = $sourceLanguage; 26 | $this->scanner = $scanner; 27 | } 28 | 29 | /** 30 | * Get all languages from the application. 31 | * 32 | * @return Collection 33 | */ 34 | public function allLanguages() 35 | { 36 | // As per the docs, there should be a subdirectory within the 37 | // languages path so we can return these directory names as a collection 38 | $directories = Collection::make($this->disk->directories($this->languageFilesPath)); 39 | 40 | return $directories->mapWithKeys(function ($directory) { 41 | $language = basename($directory); 42 | 43 | return [$language => $language]; 44 | })->filter(function ($language) { 45 | // at the moemnt, we're not supporting vendor specific translations 46 | return $language != 'vendor'; 47 | }); 48 | } 49 | 50 | /** 51 | * Get all group translations from the application. 52 | * 53 | * @return array 54 | */ 55 | public function allGroup($language) 56 | { 57 | $groupPath = "{$this->languageFilesPath}".DIRECTORY_SEPARATOR."{$language}"; 58 | 59 | if (! $this->disk->exists($groupPath)) { 60 | return []; 61 | } 62 | 63 | $groups = Collection::make($this->disk->allFiles($groupPath)); 64 | 65 | return $groups->map(function ($group) { 66 | return $group->getBasename('.php'); 67 | }); 68 | } 69 | 70 | /** 71 | * Get all the translations from the application. 72 | * 73 | * @return Collection 74 | */ 75 | public function allTranslations() 76 | { 77 | return $this->allLanguages()->mapWithKeys(function ($language) { 78 | return [$language => $this->allTranslationsFor($language)]; 79 | }); 80 | } 81 | 82 | /** 83 | * Get all translations for a particular language. 84 | * 85 | * @param string $language 86 | * @return Collection 87 | */ 88 | public function allTranslationsFor($language) 89 | { 90 | return Collection::make([ 91 | 'group' => $this->getGroupTranslationsFor($language), 92 | 'single' => $this->getSingleTranslationsFor($language), 93 | ]); 94 | } 95 | 96 | /** 97 | * Add a new language to the application. 98 | * 99 | * @param string $language 100 | * @return void 101 | */ 102 | public function addLanguage($language, $name = null) 103 | { 104 | if ($this->languageExists($language)) { 105 | throw new LanguageExistsException(__('translation::errors.language_exists', ['language' => $language])); 106 | } 107 | 108 | $this->disk->makeDirectory("{$this->languageFilesPath}".DIRECTORY_SEPARATOR."$language"); 109 | if (! $this->disk->exists("{$this->languageFilesPath}".DIRECTORY_SEPARATOR."{$language}.json")) { 110 | $this->saveSingleTranslations($language, collect(['single' => collect()])); 111 | } 112 | } 113 | 114 | /** 115 | * Add a new group type translation. 116 | * 117 | * @param string $language 118 | * @param string $key 119 | * @param string $value 120 | * @return void 121 | */ 122 | public function addGroupTranslation($language, $group, $key, $value = '') 123 | { 124 | if (! $this->languageExists($language)) { 125 | $this->addLanguage($language); 126 | } 127 | 128 | $translations = $this->getGroupTranslationsFor($language); 129 | 130 | // does the group exist? If not, create it. 131 | if (! $translations->keys()->contains($group)) { 132 | $translations->put($group, collect()); 133 | } 134 | 135 | $values = $translations->get($group); 136 | $values[$key] = $value; 137 | $translations->put($group, collect($values)); 138 | 139 | $this->saveGroupTranslations($language, $group, $translations->get($group)); 140 | } 141 | 142 | /** 143 | * Add a new single type translation. 144 | * 145 | * @param string $language 146 | * @param string $key 147 | * @param string $value 148 | * @return void 149 | */ 150 | public function addSingleTranslation($language, $vendor, $key, $value = '') 151 | { 152 | if (! $this->languageExists($language)) { 153 | $this->addLanguage($language); 154 | } 155 | 156 | $translations = $this->getSingleTranslationsFor($language); 157 | $translations->get($vendor) ?: $translations->put($vendor, collect()); 158 | $translations->get($vendor)->put($key, $value); 159 | 160 | $this->saveSingleTranslations($language, $translations); 161 | } 162 | 163 | /** 164 | * Get all of the single translations for a given language. 165 | * 166 | * @param string $language 167 | * @return Collection 168 | */ 169 | public function getSingleTranslationsFor($language) 170 | { 171 | $files = new Collection($this->disk->allFiles($this->languageFilesPath)); 172 | 173 | return $files->filter(function ($file) use ($language) { 174 | return strpos($file, "{$language}.json"); 175 | })->flatMap(function ($file) { 176 | if (strpos($file->getPathname(), 'vendor')) { 177 | $vendor = Str::before(Str::after($file->getPathname(), 'vendor'.DIRECTORY_SEPARATOR), DIRECTORY_SEPARATOR); 178 | 179 | return ["{$vendor}::single" => new Collection(json_decode($this->disk->get($file), true))]; 180 | } 181 | 182 | return ['single' => new Collection(json_decode($this->disk->get($file), true))]; 183 | }); 184 | } 185 | 186 | /** 187 | * Get all of the group translations for a given language. 188 | * 189 | * @param string $language 190 | * @return Collection 191 | */ 192 | public function getGroupTranslationsFor($language) 193 | { 194 | return $this->getGroupFilesFor($language)->mapWithKeys(function ($group) { 195 | // here we check if the path contains 'vendor' as these will be the 196 | // files which need namespacing 197 | if (Str::contains($group->getPathname(), 'vendor')) { 198 | $vendor = Str::before(Str::after($group->getPathname(), 'vendor'.DIRECTORY_SEPARATOR), DIRECTORY_SEPARATOR); 199 | 200 | return ["{$vendor}::{$group->getBasename('.php')}" => new Collection(Arr::dot($this->disk->getRequire($group->getPathname())))]; 201 | } 202 | 203 | return [$group->getBasename('.php') => new Collection(Arr::dot($this->disk->getRequire($group->getPathname())))]; 204 | }); 205 | } 206 | 207 | /** 208 | * Get all the translations for a given file. 209 | * 210 | * @param string $language 211 | * @param string $file 212 | * @return array 213 | */ 214 | public function getTranslationsForFile($language, $file) 215 | { 216 | $file = Str::finish($file, '.php'); 217 | $filePath = "{$this->languageFilesPath}".DIRECTORY_SEPARATOR."{$language}".DIRECTORY_SEPARATOR."{$file}"; 218 | $translations = []; 219 | 220 | if ($this->disk->exists($filePath)) { 221 | $translations = Arr::dot($this->disk->getRequire($filePath)); 222 | } 223 | 224 | return $translations; 225 | } 226 | 227 | /** 228 | * Determine whether or not a language exists. 229 | * 230 | * @param string $language 231 | * @return bool 232 | */ 233 | public function languageExists($language) 234 | { 235 | return $this->allLanguages()->contains($language); 236 | } 237 | 238 | /** 239 | * Add a new group of translations. 240 | * 241 | * @param string $language 242 | * @param string $group 243 | * @return void 244 | */ 245 | public function addGroup($language, $group) 246 | { 247 | $this->saveGroupTranslations($language, $group, []); 248 | } 249 | 250 | /** 251 | * Save group type language translations. 252 | * 253 | * @param string $language 254 | * @param string $group 255 | * @param array $translations 256 | * @return void 257 | */ 258 | public function saveGroupTranslations($language, $group, $translations) 259 | { 260 | // here we check if it's a namespaced translation which need saving to a 261 | // different path 262 | $translations = $translations instanceof Collection ? $translations->toArray() : $translations; 263 | ksort($translations); 264 | $translations = array_undot($translations); 265 | if (Str::contains($group, '::')) { 266 | return $this->saveNamespacedGroupTranslations($language, $group, $translations); 267 | } 268 | $this->disk->put("{$this->languageFilesPath}".DIRECTORY_SEPARATOR."{$language}".DIRECTORY_SEPARATOR."{$group}.php", "languageFilesPath}".DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR."{$namespace}".DIRECTORY_SEPARATOR."{$language}"; 283 | 284 | if (! $this->disk->exists($directory)) { 285 | $this->disk->makeDirectory($directory, 0755, true); 286 | } 287 | 288 | $this->disk->put("$directory".DIRECTORY_SEPARATOR."{$group}.php", " $translation) { 301 | $vendor = Str::before($group, '::single'); 302 | $languageFilePath = $vendor !== 'single' ? 'vendor'.DIRECTORY_SEPARATOR."{$vendor}".DIRECTORY_SEPARATOR."{$language}.json" : "{$language}.json"; 303 | $this->disk->put( 304 | "{$this->languageFilesPath}".DIRECTORY_SEPARATOR."{$languageFilePath}", 305 | json_encode((object) $translations->get($group), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) 306 | ); 307 | } 308 | } 309 | 310 | /** 311 | * Get all the group files for a given language. 312 | * 313 | * @param string $language 314 | * @return Collection 315 | */ 316 | public function getGroupFilesFor($language) 317 | { 318 | $groups = new Collection($this->disk->allFiles("{$this->languageFilesPath}".DIRECTORY_SEPARATOR."{$language}")); 319 | // namespaced files reside in the vendor directory so we'll grab these 320 | // the `getVendorGroupFileFor` method 321 | $groups = $groups->merge($this->getVendorGroupFilesFor($language)); 322 | 323 | return $groups; 324 | } 325 | 326 | /** 327 | * Get a collection of group names for a given language. 328 | * 329 | * @param string $language 330 | * @return Collection 331 | */ 332 | public function getGroupsFor($language) 333 | { 334 | return $this->getGroupFilesFor($language)->map(function ($file) { 335 | if (Str::contains($file->getPathname(), 'vendor')) { 336 | $vendor = Str::before(Str::after($file->getPathname(), 'vendor'.DIRECTORY_SEPARATOR), DIRECTORY_SEPARATOR); 337 | 338 | return "{$vendor}::{$file->getBasename('.php')}"; 339 | } 340 | 341 | return $file->getBasename('.php'); 342 | }); 343 | } 344 | 345 | /** 346 | * Get all the vendor group files for a given language. 347 | * 348 | * @param string $language 349 | * @return Collection 350 | */ 351 | public function getVendorGroupFilesFor($language) 352 | { 353 | if (! $this->disk->exists("{$this->languageFilesPath}".DIRECTORY_SEPARATOR.'vendor')) { 354 | return; 355 | } 356 | 357 | $vendorGroups = []; 358 | foreach ($this->disk->directories("{$this->languageFilesPath}".DIRECTORY_SEPARATOR.'vendor') as $vendor) { 359 | $vendor = Arr::last(explode(DIRECTORY_SEPARATOR, $vendor)); 360 | if (! $this->disk->exists("{$this->languageFilesPath}".DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR."{$vendor}".DIRECTORY_SEPARATOR."{$language}")) { 361 | array_push($vendorGroups, []); 362 | } else { 363 | array_push($vendorGroups, $this->disk->allFiles("{$this->languageFilesPath}".DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR."{$vendor}".DIRECTORY_SEPARATOR."{$language}")); 364 | } 365 | } 366 | 367 | return new Collection(Arr::flatten($vendorGroups)); 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /src/Drivers/Translation.php: -------------------------------------------------------------------------------- 1 | scanner->findTranslations(), 23 | $this->allTranslationsFor($language) 24 | ); 25 | } 26 | 27 | /** 28 | * Save all of the translations in the app without translation for a given language. 29 | * 30 | * @param string $language 31 | * @return void 32 | */ 33 | public function saveMissingTranslations($language = false) 34 | { 35 | $languages = $language ? [$language => $language] : $this->allLanguages(); 36 | 37 | foreach ($languages as $language => $name) { 38 | $missingTranslations = $this->findMissingTranslations($language); 39 | 40 | foreach ($missingTranslations as $type => $groups) { 41 | foreach ($groups as $group => $translations) { 42 | foreach ($translations as $key => $value) { 43 | if (Str::contains($group, 'single')) { 44 | $this->addSingleTranslation($language, $group, $key); 45 | } else { 46 | $this->addGroupTranslation($language, $group, $key); 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | 54 | /** 55 | * Get all translations for a given language merged with the source language. 56 | * 57 | * @param string $language 58 | * @return Collection 59 | */ 60 | public function getSourceLanguageTranslationsWith($language) 61 | { 62 | $sourceTranslations = $this->allTranslationsFor($this->sourceLanguage); 63 | $languageTranslations = $this->allTranslationsFor($language); 64 | 65 | return $sourceTranslations->map(function ($groups, $type) use ($language, $languageTranslations) { 66 | return $groups->map(function ($translations, $group) use ($type, $language, $languageTranslations) { 67 | $translations = $translations->toArray(); 68 | array_walk($translations, function (&$value, $key) use ($type, $group, $language, $languageTranslations) { 69 | $value = [ 70 | $this->sourceLanguage => $value, 71 | $language => $languageTranslations->get($type, collect())->get($group, collect())->get($key), 72 | ]; 73 | }); 74 | 75 | return $translations; 76 | }); 77 | }); 78 | } 79 | 80 | /** 81 | * Filter all keys and translations for a given language and string. 82 | * 83 | * @param string $language 84 | * @param string $filter 85 | * @return Collection 86 | */ 87 | public function filterTranslationsFor($language, $filter) 88 | { 89 | $allTranslations = $this->getSourceLanguageTranslationsWith($language); 90 | if (! $filter) { 91 | return $allTranslations; 92 | } 93 | 94 | return $allTranslations->map(function ($groups, $type) use ($language, $filter) { 95 | return $groups->map(function ($keys, $group) use ($language, $filter) { 96 | return collect($keys)->filter(function ($translations, $key) use ($group, $language, $filter) { 97 | return strs_contain([$group, $key, $translations[$language], $translations[$this->sourceLanguage]], $filter); 98 | }); 99 | })->filter(function ($keys) { 100 | return $keys->isNotEmpty(); 101 | }); 102 | }); 103 | } 104 | 105 | public function add(Request $request, $language, $isGroupTranslation) 106 | { 107 | $namespace = $request->has('namespace') && $request->get('namespace') ? "{$request->get('namespace')}::" : ''; 108 | $group = $namespace.$request->get('group'); 109 | $key = $request->get('key'); 110 | $value = $request->get('value') ?: ''; 111 | 112 | if ($isGroupTranslation) { 113 | $this->addGroupTranslation($language, $group, $key, $value); 114 | } else { 115 | $this->addSingleTranslation($language, 'single', $key, $value); 116 | } 117 | 118 | Event::dispatch(new TranslationAdded($language, $group ?: 'single', $key, $value)); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Events/TranslationAdded.php: -------------------------------------------------------------------------------- 1 | language = $language; 24 | $this->group = $group; 25 | $this->key = $key; 26 | $this->value = $value; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Exceptions/LanguageExistsException.php: -------------------------------------------------------------------------------- 1 | translation = $translation; 17 | } 18 | 19 | public function index(Request $request) 20 | { 21 | $languages = $this->translation->allLanguages(); 22 | 23 | return view('translation::languages.index', compact('languages')); 24 | } 25 | 26 | public function create() 27 | { 28 | return view('translation::languages.create'); 29 | } 30 | 31 | public function store(LanguageRequest $request) 32 | { 33 | $this->translation->addLanguage($request->locale, $request->name); 34 | 35 | return redirect() 36 | ->route('languages.index') 37 | ->with('success', __('translation::translation.language_added')); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Http/Controllers/LanguageTranslationController.php: -------------------------------------------------------------------------------- 1 | translation = $translation; 19 | } 20 | 21 | public function index(Request $request, $language) 22 | { 23 | // dd($this->translation->getSingleTranslationsFor('en')); 24 | if ($request->has('language') && $request->get('language') !== $language) { 25 | return redirect() 26 | ->route('languages.translations.index', ['language' => $request->get('language'), 'group' => $request->get('group'), 'filter' => $request->get('filter')]); 27 | } 28 | 29 | $languages = $this->translation->allLanguages(); 30 | $groups = $this->translation->getGroupsFor(config('app.locale'))->merge('single'); 31 | $translations = $this->translation->filterTranslationsFor($language, $request->get('filter')); 32 | 33 | if ($request->has('group') && $request->get('group')) { 34 | if ($request->get('group') === 'single') { 35 | $translations = $translations->get('single'); 36 | $translations = new Collection(['single' => $translations]); 37 | } else { 38 | $translations = $translations->get('group')->filter(function ($values, $group) use ($request) { 39 | return $group === $request->get('group'); 40 | }); 41 | 42 | $translations = new Collection(['group' => $translations]); 43 | } 44 | } 45 | 46 | return view('translation::languages.translations.index', compact('language', 'languages', 'groups', 'translations')); 47 | } 48 | 49 | public function create(Request $request, $language) 50 | { 51 | return view('translation::languages.translations.create', compact('language')); 52 | } 53 | 54 | public function store(TranslationRequest $request, $language) 55 | { 56 | $isGroupTranslation = $request->filled('group'); 57 | 58 | $this->translation->add($request, $language, $isGroupTranslation); 59 | 60 | return redirect() 61 | ->route('languages.translations.index', $language) 62 | ->with('success', __('translation::translation.translation_added')); 63 | } 64 | 65 | public function update(Request $request, $language) 66 | { 67 | $isGroupTranslation = ! Str::contains($request->get('group'), 'single'); 68 | 69 | $this->translation->add($request, $language, $isGroupTranslation); 70 | 71 | return ['success' => true]; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Http/Requests/LanguageRequest.php: -------------------------------------------------------------------------------- 1 | 'nullable|string', 29 | 'locale' => ['required', new LanguageNotExists], 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Http/Requests/TranslationRequest.php: -------------------------------------------------------------------------------- 1 | 'required', 28 | 'value' => 'required', 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/InterfaceDatabaseLoader.php: -------------------------------------------------------------------------------- 1 | translation = $translation; 15 | } 16 | 17 | /** 18 | * Load the messages for the given locale. 19 | * 20 | * @param string $locale 21 | * @param string $group 22 | * @param string $namespace 23 | * @return array 24 | */ 25 | public function load($locale, $group, $namespace = null) 26 | { 27 | if ($group == '*' && $namespace == '*') { 28 | return $this->translation->getSingleTranslationsFor($locale)->get('single', collect())->toArray(); 29 | } 30 | 31 | if (is_null($namespace) || $namespace == '*') { 32 | return $this->translation->getGroupTranslationsFor($locale)->filter(function ($value, $key) use ($group) { 33 | return $key === $group; 34 | })->first(); 35 | } 36 | 37 | return $this->translation->getGroupTranslationsFor($locale)->filter(function ($value, $key) use ($group, $namespace) { 38 | return $key === "{$namespace}::{$group}"; 39 | })->first(); 40 | } 41 | 42 | /** 43 | * Add a new namespace to the loader. 44 | * 45 | * @param string $namespace 46 | * @param string $hint 47 | * @return void 48 | */ 49 | public function addNamespace($namespace, $hint) 50 | { 51 | // 52 | } 53 | 54 | /** 55 | * Add a new JSON path to the loader. 56 | * 57 | * @param string $path 58 | * @return void 59 | */ 60 | public function addJsonPath($path) 61 | { 62 | // 63 | } 64 | 65 | /** 66 | * Get an array of all the registered namespaces. 67 | * 68 | * @return array 69 | */ 70 | public function namespaces() 71 | { 72 | return []; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Language.php: -------------------------------------------------------------------------------- 1 | connection = config('translation.database.connection'); 15 | $this->table = config('translation.database.languages_table'); 16 | } 17 | 18 | public function translations() 19 | { 20 | return $this->hasMany(Translation::class); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Rules/LanguageNotExists.php: -------------------------------------------------------------------------------- 1 | make(Translation::class); 20 | 21 | return ! $translation->languageExists($value); 22 | } 23 | 24 | /** 25 | * Get the validation error message. 26 | * 27 | * @return string 28 | */ 29 | public function message() 30 | { 31 | return __('translation::translation.language_exists'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Scanner.php: -------------------------------------------------------------------------------- 1 | disk = $disk; 18 | $this->scanPaths = $scanPaths; 19 | $this->translationMethods = $translationMethods; 20 | } 21 | 22 | /** 23 | * Scan all the files in the provided $scanPath for translations. 24 | * 25 | * @return array 26 | */ 27 | public function findTranslations() 28 | { 29 | $results = ['single' => [], 'group' => []]; 30 | 31 | // This has been derived from a combination of the following: 32 | // * Laravel Language Manager GUI from Mohamed Said (https://github.com/themsaid/laravel-langman-gui) 33 | // * Laravel 5 Translation Manager from Barry vd. Heuvel (https://github.com/barryvdh/laravel-translation-manager) 34 | $matchingPattern = 35 | '[^\w]'. // Must not start with any alphanum or _ 36 | '(?)'. // Must not start with -> 37 | '('.implode('|', $this->translationMethods).')'. // Must start with one of the functions 38 | "\(". // Match opening parentheses 39 | "\s*". // Whitespace before param 40 | "[\'\"]". // Match " or ' 41 | '('. // Start a new group to match: 42 | '.+'. // Must start with group 43 | ')'. // Close group 44 | "[\'\"]". // Closing quote 45 | "\s*". // Whitespace after param 46 | "[\),]"; // Close parentheses or new parameter 47 | 48 | foreach ($this->disk->allFiles($this->scanPaths) as $file) { 49 | if (preg_match_all("/$matchingPattern/siU", $file->getContents(), $matches)) { 50 | foreach ($matches[2] as $key) { 51 | if (preg_match("/(^[a-zA-Z0-9:_-]+([.][^\1)\ ]+)+$)/siU", $key, $arrayMatches)) { 52 | [$file, $k] = explode('.', $arrayMatches[0], 2); 53 | $results['group'][$file][$k] = ''; 54 | continue; 55 | } else { 56 | $results['single']['single'][$key] = ''; 57 | } 58 | } 59 | } 60 | } 61 | 62 | return $results; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Translation.php: -------------------------------------------------------------------------------- 1 | connection = config('translation.database.connection'); 15 | $this->table = config('translation.database.translations_table'); 16 | } 17 | 18 | public function language() 19 | { 20 | return $this->belongsTo(Language::class); 21 | } 22 | 23 | public static function getGroupsForLanguage($language) 24 | { 25 | return static::whereHas('language', function ($q) use ($language) { 26 | $q->where('language', $language); 27 | })->whereNotNull('group') 28 | ->where('group', 'not like', '%single') 29 | ->select('group') 30 | ->distinct() 31 | ->get(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/TranslationBindingsServiceProvider.php: -------------------------------------------------------------------------------- 1 | app['config']['translation.driver'] === 'database') { 19 | $this->registerDatabaseTranslator(); 20 | } else { 21 | parent::register(); 22 | } 23 | } 24 | 25 | private function registerDatabaseTranslator() 26 | { 27 | $this->registerDatabaseLoader(); 28 | 29 | $this->app->singleton('translator', function ($app) { 30 | $loader = $app['translation.loader']; 31 | // When registering the translator component, we'll need to set the default 32 | // locale as well as the fallback locale. So, we'll grab the application 33 | // configuration so we can easily get both of these values from there. 34 | $locale = $app['config']['app.locale']; 35 | $trans = new Translator($loader, $locale); 36 | $trans->setFallback($app['config']['app.fallback_locale']); 37 | 38 | return $trans; 39 | }); 40 | } 41 | 42 | protected function registerDatabaseLoader() 43 | { 44 | $this->app->singleton('translation.loader', function ($app) { 45 | // Post Laravel 5.4, the interface was moved to the contracts 46 | // directory. Here we perform a check to see whether or not the 47 | // interface exists and instantiate the relevant loader accordingly. 48 | if (interface_exists('Illuminate\Contracts\Translation\Loader')) { 49 | return new ContractDatabaseLoader($this->app->make(Translation::class)); 50 | } 51 | 52 | return new InterfaceDatabaseLoader($this->app->make(Translation::class)); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/TranslationManager.php: -------------------------------------------------------------------------------- 1 | app = $app; 21 | $this->config = $config; 22 | $this->scanner = $scanner; 23 | } 24 | 25 | public function resolve() 26 | { 27 | $driver = $this->config['driver']; 28 | $driverResolver = Str::studly($driver); 29 | $method = "resolve{$driverResolver}Driver"; 30 | 31 | if (! method_exists($this, $method)) { 32 | throw new \InvalidArgumentException("Invalid driver [$driver]"); 33 | } 34 | 35 | return $this->{$method}(); 36 | } 37 | 38 | protected function resolveFileDriver() 39 | { 40 | return new File(new Filesystem, $this->app['path.lang'], $this->app->config['app']['locale'], $this->scanner); 41 | } 42 | 43 | protected function resolveDatabaseDriver() 44 | { 45 | return new Database($this->app->config['app']['locale'], $this->scanner); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/TranslationServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadViews(); 25 | 26 | $this->registerRoutes(); 27 | 28 | $this->publishConfiguration(); 29 | 30 | $this->publishAssets(); 31 | 32 | $this->loadMigrations(); 33 | 34 | $this->loadTranslations(); 35 | 36 | $this->registerHelpers(); 37 | } 38 | 39 | /** 40 | * Register package bindings in the container. 41 | * 42 | * @return void 43 | */ 44 | public function register() 45 | { 46 | $this->mergeConfiguration(); 47 | 48 | $this->registerCommands(); 49 | 50 | $this->registerContainerBindings(); 51 | } 52 | 53 | /** 54 | * Load and publish package views. 55 | * 56 | * @return void 57 | */ 58 | private function loadViews() 59 | { 60 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'translation'); 61 | 62 | $this->publishes([ 63 | __DIR__.'/../resources/views' => resource_path('views/vendor/translation'), 64 | ]); 65 | } 66 | 67 | /** 68 | * Register package routes. 69 | * 70 | * @return void 71 | */ 72 | private function registerRoutes() 73 | { 74 | $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); 75 | } 76 | 77 | /** 78 | * Publish package configuration. 79 | * 80 | * @return void 81 | */ 82 | private function publishConfiguration() 83 | { 84 | $this->publishes([ 85 | __DIR__.'/../config/translation.php' => config_path('translation.php'), 86 | ], 'config'); 87 | } 88 | 89 | /** 90 | * Merge package configuration. 91 | * 92 | * @return void 93 | */ 94 | private function mergeConfiguration() 95 | { 96 | $this->mergeConfigFrom(__DIR__.'/../config/translation.php', 'translation'); 97 | } 98 | 99 | /** 100 | * Publish package assets. 101 | * 102 | * @return void 103 | */ 104 | private function publishAssets() 105 | { 106 | $this->publishes([ 107 | __DIR__.'/../public/assets' => public_path('vendor/translation'), 108 | ], 'assets'); 109 | } 110 | 111 | /** 112 | * Load package migrations. 113 | * 114 | * @return void 115 | */ 116 | private function loadMigrations() 117 | { 118 | if (config('translation.driver') !== 'database') { 119 | return; 120 | } 121 | 122 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 123 | } 124 | 125 | /** 126 | * Load package translations. 127 | * 128 | * @return void 129 | */ 130 | private function loadTranslations() 131 | { 132 | $this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'translation'); 133 | 134 | $this->publishes([ 135 | __DIR__.'/../resources/lang' => resource_path('lang/vendor/translation'), 136 | ]); 137 | } 138 | 139 | /** 140 | * Register package commands. 141 | * 142 | * @return void 143 | */ 144 | private function registerCommands() 145 | { 146 | if ($this->app->runningInConsole()) { 147 | $this->commands([ 148 | AddLanguageCommand::class, 149 | AddTranslationKeyCommand::class, 150 | ListLanguagesCommand::class, 151 | ListMissingTranslationKeys::class, 152 | SynchroniseMissingTranslationKeys::class, 153 | SynchroniseTranslationsCommand::class, 154 | ]); 155 | } 156 | } 157 | 158 | /** 159 | * Register package helper functions. 160 | * 161 | * @return void 162 | */ 163 | private function registerHelpers() 164 | { 165 | require __DIR__.'/../resources/helpers.php'; 166 | } 167 | 168 | /** 169 | * Register package bindings in the container. 170 | * 171 | * @return void 172 | */ 173 | private function registerContainerBindings() 174 | { 175 | $this->app->singleton(Scanner::class, function () { 176 | $config = $this->app['config']['translation']; 177 | 178 | return new Scanner(new Filesystem(), $config['scan_paths'], $config['translation_methods']); 179 | }); 180 | 181 | $this->app->singleton(Translation::class, function ($app) { 182 | return (new TranslationManager($app, $app['config']['translation'], $app->make(Scanner::class)))->resolve(); 183 | }); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /tailwind.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Tailwind - The Utility-First CSS Framework 4 | 5 | A project by Adam Wathan (@adamwathan), Jonathan Reinink (@reinink), 6 | David Hemphill (@davidhemphill) and Steve Schoger (@steveschoger). 7 | 8 | Welcome to the Tailwind config file. This is where you can customize 9 | Tailwind specifically for your project. Don't be intimidated by the 10 | length of this file. It's really just a big JavaScript object and 11 | we've done our very best to explain each section. 12 | 13 | View the full documentation at https://tailwindcss.com. 14 | 15 | 16 | |------------------------------------------------------------------------------- 17 | | The default config 18 | |------------------------------------------------------------------------------- 19 | | 20 | | This variable contains the default Tailwind config. You don't have 21 | | to use it, but it can sometimes be helpful to have available. For 22 | | example, you may choose to merge your custom configuration 23 | | values with some of the Tailwind defaults. 24 | | 25 | */ 26 | 27 | // let defaultConfig = require('tailwindcss/defaultConfig')() 28 | 29 | 30 | /* 31 | |------------------------------------------------------------------------------- 32 | | Colors https://tailwindcss.com/docs/colors 33 | |------------------------------------------------------------------------------- 34 | | 35 | | Here you can specify the colors used in your project. To get you started, 36 | | we've provided a generous palette of great looking colors that are perfect 37 | | for prototyping, but don't hesitate to change them for your project. You 38 | | own these colors, nothing will break if you change everything about them. 39 | | 40 | | We've used literal color names ("red", "blue", etc.) for the default 41 | | palette, but if you'd rather use functional names like "primary" and 42 | | "secondary", or even a numeric scale like "100" and "200", go for it. 43 | | 44 | */ 45 | 46 | let colors = { 47 | 'transparent': 'transparent', 48 | 49 | 'black': '#22292f', 50 | 'grey-darkest': '#3d4852', 51 | 'grey-darker': '#606f7b', 52 | 'grey-dark': '#8795a1', 53 | 'grey': '#b8c2cc', 54 | 'grey-light': '#dae1e7', 55 | 'grey-lighter': '#f1f5f8', 56 | 'grey-lightest': '#f8fafc', 57 | 'white': '#ffffff', 58 | 59 | 'red-darkest': '#3b0d0c', 60 | 'red-darker': '#621b18', 61 | 'red-dark': '#cc1f1a', 62 | 'red': '#e3342f', 63 | 'red-light': '#ef5753', 64 | 'red-lighter': '#f9acaa', 65 | 'red-lightest': '#fcebea', 66 | 67 | 'orange-darkest': '#462a16', 68 | 'orange-darker': '#613b1f', 69 | 'orange-dark': '#de751f', 70 | 'orange': '#f6993f', 71 | 'orange-light': '#faad63', 72 | 'orange-lighter': '#fcd9b6', 73 | 'orange-lightest': '#fff5eb', 74 | 75 | 'yellow-darkest': '#453411', 76 | 'yellow-darker': '#684f1d', 77 | 'yellow-dark': '#f2d024', 78 | 'yellow': '#ffed4a', 79 | 'yellow-light': '#fff382', 80 | 'yellow-lighter': '#fff9c2', 81 | 'yellow-lightest': '#fcfbeb', 82 | 83 | 'green-darkest': '#0f2f21', 84 | 'green-darker': '#1a4731', 85 | 'green-dark': '#1f9d55', 86 | 'green': '#38c172', 87 | 'green-light': '#51d88a', 88 | 'green-lighter': '#a2f5bf', 89 | 'green-lightest': '#e3fcec', 90 | 91 | 'teal-darkest': '#0d3331', 92 | 'teal-darker': '#20504f', 93 | 'teal-dark': '#38a89d', 94 | 'teal': '#4dc0b5', 95 | 'teal-light': '#64d5ca', 96 | 'teal-lighter': '#a0f0ed', 97 | 'teal-lightest': '#e8fffe', 98 | 99 | 'blue-darkest': '#12283a', 100 | 'blue-darker': '#1c3d5a', 101 | 'blue-dark': '#125b93', 102 | 'blue': '#2891c4', 103 | 'blue-light': '#6cb2eb', 104 | 'blue-lighter': '#bcdefa', 105 | 'blue-lightest': '#eff8ff', 106 | 107 | 'indigo-darkest': '#191e38', 108 | 'indigo-darker': '#2f365f', 109 | 'indigo-dark': '#5661b3', 110 | 'indigo': '#6574cd', 111 | 'indigo-light': '#7886d7', 112 | 'indigo-lighter': '#b2b7ff', 113 | 'indigo-lightest': '#e6e8ff', 114 | 115 | 'purple-darkest': '#21183c', 116 | 'purple-darker': '#382b5f', 117 | 'purple-dark': '#794acf', 118 | 'purple': '#9561e2', 119 | 'purple-light': '#a779e9', 120 | 'purple-lighter': '#d6bbfc', 121 | 'purple-lightest': '#f3ebff', 122 | 123 | 'pink-darkest': '#451225', 124 | 'pink-darker': '#6f213f', 125 | 'pink-dark': '#eb5286', 126 | 'pink': '#f66d9b', 127 | 'pink-light': '#fa7ea8', 128 | 'pink-lighter': '#ffbbca', 129 | 'pink-lightest': '#ffebef', 130 | } 131 | 132 | module.exports = { 133 | 134 | /* 135 | |----------------------------------------------------------------------------- 136 | | Colors https://tailwindcss.com/docs/colors 137 | |----------------------------------------------------------------------------- 138 | | 139 | | The color palette defined above is also assigned to the "colors" key of 140 | | your Tailwind config. This makes it easy to access them in your CSS 141 | | using Tailwind's config helper. For example: 142 | | 143 | | .error { color: config('colors.red') } 144 | | 145 | */ 146 | 147 | colors: colors, 148 | 149 | 150 | /* 151 | |----------------------------------------------------------------------------- 152 | | Screens https://tailwindcss.com/docs/responsive-design 153 | |----------------------------------------------------------------------------- 154 | | 155 | | Screens in Tailwind are translated to CSS media queries. They define the 156 | | responsive breakpoints for your project. By default Tailwind takes a 157 | | "mobile first" approach, where each screen size represents a minimum 158 | | viewport width. Feel free to have as few or as many screens as you 159 | | want, naming them in whatever way you'd prefer for your project. 160 | | 161 | | Tailwind also allows for more complex screen definitions, which can be 162 | | useful in certain situations. Be sure to see the full responsive 163 | | documentation for a complete list of options. 164 | | 165 | | Class name: .{screen}:{utility} 166 | | 167 | */ 168 | 169 | screens: { 170 | 'sm': '576px', 171 | 'md': '768px', 172 | 'lg': '992px', 173 | 'xl': '1200px', 174 | }, 175 | 176 | 177 | /* 178 | |----------------------------------------------------------------------------- 179 | | Fonts https://tailwindcss.com/docs/fonts 180 | |----------------------------------------------------------------------------- 181 | | 182 | | Here is where you define your project's font stack, or font families. 183 | | Keep in mind that Tailwind doesn't actually load any fonts for you. 184 | | If you're using custom fonts you'll need to import them prior to 185 | | defining them here. 186 | | 187 | | By default we provide a native font stack that works remarkably well on 188 | | any device or OS you're using, since it just uses the default fonts 189 | | provided by the platform. 190 | | 191 | | Class name: .font-{name} 192 | | 193 | */ 194 | 195 | fonts: { 196 | 'sans': [ 197 | 'system-ui', 198 | 'BlinkMacSystemFont', 199 | '-apple-system', 200 | 'Segoe UI', 201 | 'Roboto', 202 | 'Oxygen', 203 | 'Ubuntu', 204 | 'Cantarell', 205 | 'Fira Sans', 206 | 'Droid Sans', 207 | 'Helvetica Neue', 208 | 'sans-serif', 209 | ], 210 | 'serif': [ 211 | 'Constantia', 212 | 'Lucida Bright', 213 | 'Lucidabright', 214 | 'Lucida Serif', 215 | 'Lucida', 216 | 'DejaVu Serif', 217 | 'Bitstream Vera Serif', 218 | 'Liberation Serif', 219 | 'Georgia', 220 | 'serif', 221 | ], 222 | 'mono': [ 223 | 'Menlo', 224 | 'Monaco', 225 | 'Consolas', 226 | 'Liberation Mono', 227 | 'Courier New', 228 | 'monospace', 229 | ] 230 | }, 231 | 232 | 233 | /* 234 | |----------------------------------------------------------------------------- 235 | | Text sizes https://tailwindcss.com/docs/text-sizing 236 | |----------------------------------------------------------------------------- 237 | | 238 | | Here is where you define your text sizes. Name these in whatever way 239 | | makes the most sense to you. We use size names by default, but 240 | | you're welcome to use a numeric scale or even something else 241 | | entirely. 242 | | 243 | | By default Tailwind uses the "rem" unit type for most measurements. 244 | | This allows you to set a root font size which all other sizes are 245 | | then based on. That said, you are free to use whatever units you 246 | | prefer, be it rems, ems, pixels or other. 247 | | 248 | | Class name: .text-{size} 249 | | 250 | */ 251 | 252 | textSizes: { 253 | 'xs': '.75rem', // 12px 254 | 'sm': '.875rem', // 14px 255 | 'base': '1rem', // 16px 256 | 'lg': '1.125rem', // 18px 257 | 'xl': '1.25rem', // 20px 258 | '2xl': '1.5rem', // 24px 259 | '3xl': '1.875rem', // 30px 260 | '4xl': '2.25rem', // 36px 261 | '5xl': '3rem', // 48px 262 | }, 263 | 264 | 265 | /* 266 | |----------------------------------------------------------------------------- 267 | | Font weights https://tailwindcss.com/docs/font-weight 268 | |----------------------------------------------------------------------------- 269 | | 270 | | Here is where you define your font weights. We've provided a list of 271 | | common font weight names with their respective numeric scale values 272 | | to get you started. It's unlikely that your project will require 273 | | all of these, so we recommend removing those you don't need. 274 | | 275 | | Class name: .font-{weight} 276 | | 277 | */ 278 | 279 | fontWeights: { 280 | 'hairline': 100, 281 | 'thin': 200, 282 | 'light': 300, 283 | 'normal': 400, 284 | 'medium': 500, 285 | 'semibold': 600, 286 | 'bold': 700, 287 | 'extrabold': 800, 288 | 'black': 900, 289 | }, 290 | 291 | 292 | /* 293 | |----------------------------------------------------------------------------- 294 | | Leading (line height) https://tailwindcss.com/docs/line-height 295 | |----------------------------------------------------------------------------- 296 | | 297 | | Here is where you define your line height values, or as we call 298 | | them in Tailwind, leadings. 299 | | 300 | | Class name: .leading-{size} 301 | | 302 | */ 303 | 304 | leading: { 305 | 'none': 1, 306 | 'tight': 1.25, 307 | 'normal': 1.5, 308 | 'loose': 2, 309 | }, 310 | 311 | 312 | /* 313 | |----------------------------------------------------------------------------- 314 | | Tracking (letter spacing) https://tailwindcss.com/docs/letter-spacing 315 | |----------------------------------------------------------------------------- 316 | | 317 | | Here is where you define your letter spacing values, or as we call 318 | | them in Tailwind, tracking. 319 | | 320 | | Class name: .tracking-{size} 321 | | 322 | */ 323 | 324 | tracking: { 325 | 'tight': '-0.05em', 326 | 'normal': '0', 327 | 'wide': '0.05em', 328 | }, 329 | 330 | 331 | /* 332 | |----------------------------------------------------------------------------- 333 | | Text colors https://tailwindcss.com/docs/text-color 334 | |----------------------------------------------------------------------------- 335 | | 336 | | Here is where you define your text colors. By default these use the 337 | | color palette we defined above, however you're welcome to set these 338 | | independently if that makes sense for your project. 339 | | 340 | | Class name: .text-{color} 341 | | 342 | */ 343 | 344 | textColors: colors, 345 | 346 | 347 | /* 348 | |----------------------------------------------------------------------------- 349 | | Background colors https://tailwindcss.com/docs/background-color 350 | |----------------------------------------------------------------------------- 351 | | 352 | | Here is where you define your background colors. By default these use 353 | | the color palette we defined above, however you're welcome to set 354 | | these independently if that makes sense for your project. 355 | | 356 | | Class name: .bg-{color} 357 | | 358 | */ 359 | 360 | backgroundColors: colors, 361 | 362 | 363 | /* 364 | |----------------------------------------------------------------------------- 365 | | Background sizes https://tailwindcss.com/docs/background-size 366 | |----------------------------------------------------------------------------- 367 | | 368 | | Here is where you define your background sizes. We provide some common 369 | | values that are useful in most projects, but feel free to add other sizes 370 | | that are specific to your project here as well. 371 | | 372 | | Class name: .bg-{size} 373 | | 374 | */ 375 | 376 | backgroundSize: { 377 | 'auto': 'auto', 378 | 'cover': 'cover', 379 | 'contain': 'contain', 380 | }, 381 | 382 | 383 | /* 384 | |----------------------------------------------------------------------------- 385 | | Border widths https://tailwindcss.com/docs/border-width 386 | |----------------------------------------------------------------------------- 387 | | 388 | | Here is where you define your border widths. Take note that border 389 | | widths require a special "default" value set as well. This is the 390 | | width that will be used when you do not specify a border width. 391 | | 392 | | Class name: .border{-side?}{-width?} 393 | | 394 | */ 395 | 396 | borderWidths: { 397 | default: '1px', 398 | '0': '0', 399 | '2': '2px', 400 | '4': '4px', 401 | '8': '8px', 402 | }, 403 | 404 | 405 | /* 406 | |----------------------------------------------------------------------------- 407 | | Border colors https://tailwindcss.com/docs/border-color 408 | |----------------------------------------------------------------------------- 409 | | 410 | | Here is where you define your border colors. By default these use the 411 | | color palette we defined above, however you're welcome to set these 412 | | independently if that makes sense for your project. 413 | | 414 | | Take note that border colors require a special "default" value set 415 | | as well. This is the color that will be used when you do not 416 | | specify a border color. 417 | | 418 | | Class name: .border-{color} 419 | | 420 | */ 421 | 422 | borderColors: global.Object.assign({ default: colors['grey-light'] }, colors), 423 | 424 | 425 | /* 426 | |----------------------------------------------------------------------------- 427 | | Border radius https://tailwindcss.com/docs/border-radius 428 | |----------------------------------------------------------------------------- 429 | | 430 | | Here is where you define your border radius values. If a `default` radius 431 | | is provided, it will be made available as the non-suffixed `.rounded` 432 | | utility. 433 | | 434 | | If your scale includes a `0` value to reset already rounded corners, it's 435 | | a good idea to put it first so other values are able to override it. 436 | | 437 | | Class name: .rounded{-side?}{-size?} 438 | | 439 | */ 440 | 441 | borderRadius: { 442 | 'none': '0', 443 | 'sm': '.125rem', 444 | default: '.25rem', 445 | 'lg': '.5rem', 446 | 'full': '9999px', 447 | }, 448 | 449 | 450 | /* 451 | |----------------------------------------------------------------------------- 452 | | Width https://tailwindcss.com/docs/width 453 | |----------------------------------------------------------------------------- 454 | | 455 | | Here is where you define your width utility sizes. These can be 456 | | percentage based, pixels, rems, or any other units. By default 457 | | we provide a sensible rem based numeric scale, a percentage 458 | | based fraction scale, plus some other common use-cases. You 459 | | can, of course, modify these values as needed. 460 | | 461 | | 462 | | It's also worth mentioning that Tailwind automatically escapes 463 | | invalid CSS class name characters, which allows you to have 464 | | awesome classes like .w-2/3. 465 | | 466 | | Class name: .w-{size} 467 | | 468 | */ 469 | 470 | width: { 471 | 'auto': 'auto', 472 | 'px': '1px', 473 | '1': '0.25rem', 474 | '2': '0.5rem', 475 | '3': '0.75rem', 476 | '4': '1rem', 477 | '5': '1.25rem', 478 | '6': '1.5rem', 479 | '8': '2rem', 480 | '10': '2.5rem', 481 | '12': '3rem', 482 | '16': '4rem', 483 | '24': '6rem', 484 | '32': '8rem', 485 | '48': '12rem', 486 | '64': '16rem', 487 | '1/2': '50%', 488 | '1/3': '33.33333%', 489 | '2/3': '66.66667%', 490 | '1/4': '25%', 491 | '3/4': '75%', 492 | '1/5': '20%', 493 | '2/5': '40%', 494 | '3/5': '60%', 495 | '4/5': '80%', 496 | '1/6': '16.66667%', 497 | '5/6': '83.33333%', 498 | 'full': '100%', 499 | 'screen': '100vw' 500 | }, 501 | 502 | 503 | /* 504 | |----------------------------------------------------------------------------- 505 | | Height https://tailwindcss.com/docs/height 506 | |----------------------------------------------------------------------------- 507 | | 508 | | Here is where you define your height utility sizes. These can be 509 | | percentage based, pixels, rems, or any other units. By default 510 | | we provide a sensible rem based numeric scale plus some other 511 | | common use-cases. You can, of course, modify these values as 512 | | needed. 513 | | 514 | | Class name: .h-{size} 515 | | 516 | */ 517 | 518 | height: { 519 | 'auto': 'auto', 520 | 'px': '1px', 521 | '1': '0.25rem', 522 | '2': '0.5rem', 523 | '3': '0.75rem', 524 | '4': '1rem', 525 | '5': '1.25rem', 526 | '6': '1.5rem', 527 | '8': '2rem', 528 | '10': '2.5rem', 529 | '12': '3rem', 530 | '16': '4rem', 531 | '24': '6rem', 532 | '32': '8rem', 533 | '48': '12rem', 534 | '64': '16rem', 535 | 'full': '100%', 536 | 'screen': '100vh' 537 | }, 538 | 539 | 540 | /* 541 | |----------------------------------------------------------------------------- 542 | | Minimum width https://tailwindcss.com/docs/min-width 543 | |----------------------------------------------------------------------------- 544 | | 545 | | Here is where you define your minimum width utility sizes. These can 546 | | be percentage based, pixels, rems, or any other units. We provide a 547 | | couple common use-cases by default. You can, of course, modify 548 | | these values as needed. 549 | | 550 | | Class name: .min-w-{size} 551 | | 552 | */ 553 | 554 | minWidth: { 555 | '0': '0', 556 | 'full': '100%', 557 | }, 558 | 559 | 560 | /* 561 | |----------------------------------------------------------------------------- 562 | | Minimum height https://tailwindcss.com/docs/min-height 563 | |----------------------------------------------------------------------------- 564 | | 565 | | Here is where you define your minimum height utility sizes. These can 566 | | be percentage based, pixels, rems, or any other units. We provide a 567 | | few common use-cases by default. You can, of course, modify these 568 | | values as needed. 569 | | 570 | | Class name: .min-h-{size} 571 | | 572 | */ 573 | 574 | minHeight: { 575 | '0': '0', 576 | 'full': '100%', 577 | 'screen': '100vh' 578 | }, 579 | 580 | 581 | /* 582 | |----------------------------------------------------------------------------- 583 | | Maximum width https://tailwindcss.com/docs/max-width 584 | |----------------------------------------------------------------------------- 585 | | 586 | | Here is where you define your maximum width utility sizes. These can 587 | | be percentage based, pixels, rems, or any other units. By default 588 | | we provide a sensible rem based scale and a "full width" size, 589 | | which is basically a reset utility. You can, of course, 590 | | modify these values as needed. 591 | | 592 | | Class name: .max-w-{size} 593 | | 594 | */ 595 | 596 | maxWidth: { 597 | 'xs': '20rem', 598 | 'sm': '30rem', 599 | 'md': '40rem', 600 | 'lg': '50rem', 601 | 'xl': '60rem', 602 | '2xl': '70rem', 603 | '3xl': '80rem', 604 | '4xl': '90rem', 605 | '5xl': '100rem', 606 | 'full': '100%', 607 | }, 608 | 609 | 610 | /* 611 | |----------------------------------------------------------------------------- 612 | | Maximum height https://tailwindcss.com/docs/max-height 613 | |----------------------------------------------------------------------------- 614 | | 615 | | Here is where you define your maximum height utility sizes. These can 616 | | be percentage based, pixels, rems, or any other units. We provide a 617 | | couple common use-cases by default. You can, of course, modify 618 | | these values as needed. 619 | | 620 | | Class name: .max-h-{size} 621 | | 622 | */ 623 | 624 | maxHeight: { 625 | 'full': '100%', 626 | 'screen': '100vh', 627 | }, 628 | 629 | 630 | /* 631 | |----------------------------------------------------------------------------- 632 | | Padding https://tailwindcss.com/docs/padding 633 | |----------------------------------------------------------------------------- 634 | | 635 | | Here is where you define your padding utility sizes. These can be 636 | | percentage based, pixels, rems, or any other units. By default we 637 | | provide a sensible rem based numeric scale plus a couple other 638 | | common use-cases like "1px". You can, of course, modify these 639 | | values as needed. 640 | | 641 | | Class name: .p{side?}-{size} 642 | | 643 | */ 644 | 645 | padding: { 646 | 'px': '1px', 647 | '0': '0', 648 | '1': '0.25rem', 649 | '2': '0.5rem', 650 | '3': '0.75rem', 651 | '4': '1rem', 652 | '5': '1.25rem', 653 | '6': '1.5rem', 654 | '8': '2rem', 655 | '10': '2.5rem', 656 | '12': '3rem', 657 | '16': '4rem', 658 | '20': '5rem', 659 | '24': '6rem', 660 | '32': '8rem', 661 | }, 662 | 663 | 664 | /* 665 | |----------------------------------------------------------------------------- 666 | | Margin https://tailwindcss.com/docs/margin 667 | |----------------------------------------------------------------------------- 668 | | 669 | | Here is where you define your margin utility sizes. These can be 670 | | percentage based, pixels, rems, or any other units. By default we 671 | | provide a sensible rem based numeric scale plus a couple other 672 | | common use-cases like "1px". You can, of course, modify these 673 | | values as needed. 674 | | 675 | | Class name: .m{side?}-{size} 676 | | 677 | */ 678 | 679 | margin: { 680 | 'auto': 'auto', 681 | 'px': '1px', 682 | '0': '0', 683 | '1': '0.25rem', 684 | '2': '0.5rem', 685 | '3': '0.75rem', 686 | '4': '1rem', 687 | '5': '1.25rem', 688 | '6': '1.5rem', 689 | '8': '2rem', 690 | '10': '2.5rem', 691 | '12': '3rem', 692 | '16': '4rem', 693 | '20': '5rem', 694 | '24': '6rem', 695 | '32': '8rem', 696 | }, 697 | 698 | 699 | /* 700 | |----------------------------------------------------------------------------- 701 | | Negative margin https://tailwindcss.com/docs/negative-margin 702 | |----------------------------------------------------------------------------- 703 | | 704 | | Here is where you define your negative margin utility sizes. These can 705 | | be percentage based, pixels, rems, or any other units. By default we 706 | | provide matching values to the padding scale since these utilities 707 | | generally get used together. You can, of course, modify these 708 | | values as needed. 709 | | 710 | | Class name: .-m{side?}-{size} 711 | | 712 | */ 713 | 714 | negativeMargin: { 715 | 'px': '1px', 716 | '0': '0', 717 | '1': '0.25rem', 718 | '2': '0.5rem', 719 | '3': '0.75rem', 720 | '4': '1rem', 721 | '5': '1.25rem', 722 | '6': '1.5rem', 723 | '8': '2rem', 724 | '10': '2.5rem', 725 | '12': '3rem', 726 | '16': '4rem', 727 | '20': '5rem', 728 | '24': '6rem', 729 | '32': '8rem', 730 | }, 731 | 732 | 733 | /* 734 | |----------------------------------------------------------------------------- 735 | | Shadows https://tailwindcss.com/docs/shadows 736 | |----------------------------------------------------------------------------- 737 | | 738 | | Here is where you define your shadow utilities. As you can see from 739 | | the defaults we provide, it's possible to apply multiple shadows 740 | | per utility using comma separation. 741 | | 742 | | If a `default` shadow is provided, it will be made available as the non- 743 | | suffixed `.shadow` utility. 744 | | 745 | | Class name: .shadow-{size?} 746 | | 747 | */ 748 | 749 | shadows: { 750 | default: '0 2px 4px 0 rgba(0,0,0,0.10)', 751 | 'md': '0 4px 8px 0 rgba(0,0,0,0.12), 0 2px 4px 0 rgba(0,0,0,0.08)', 752 | 'lg': '0 15px 30px 0 rgba(0,0,0,0.11), 0 5px 15px 0 rgba(0,0,0,0.08)', 753 | 'inner': 'inset 0 2px 4px 0 rgba(0,0,0,0.06)', 754 | 'outline': '0 0 0 3px rgba(52,144,220,0.5)', 755 | 'none': 'none', 756 | }, 757 | 758 | 759 | /* 760 | |----------------------------------------------------------------------------- 761 | | Z-index https://tailwindcss.com/docs/z-index 762 | |----------------------------------------------------------------------------- 763 | | 764 | | Here is where you define your z-index utility values. By default we 765 | | provide a sensible numeric scale. You can, of course, modify these 766 | | values as needed. 767 | | 768 | | Class name: .z-{index} 769 | | 770 | */ 771 | 772 | zIndex: { 773 | 'auto': 'auto', 774 | '0': 0, 775 | '10': 10, 776 | '20': 20, 777 | '30': 30, 778 | '40': 40, 779 | '50': 50, 780 | }, 781 | 782 | 783 | /* 784 | |----------------------------------------------------------------------------- 785 | | Opacity https://tailwindcss.com/docs/opacity 786 | |----------------------------------------------------------------------------- 787 | | 788 | | Here is where you define your opacity utility values. By default we 789 | | provide a sensible numeric scale. You can, of course, modify these 790 | | values as needed. 791 | | 792 | | Class name: .opacity-{name} 793 | | 794 | */ 795 | 796 | opacity: { 797 | '0': '0', 798 | '25': '.25', 799 | '50': '.5', 800 | '75': '.75', 801 | '100': '1', 802 | }, 803 | 804 | 805 | /* 806 | |----------------------------------------------------------------------------- 807 | | SVG fill https://tailwindcss.com/docs/svg 808 | |----------------------------------------------------------------------------- 809 | | 810 | | Here is where you define your SVG fill colors. By default we just provide 811 | | `fill-current` which sets the fill to the current text color. This lets you 812 | | specify a fill color using existing text color utilities and helps keep the 813 | | generated CSS file size down. 814 | | 815 | | Class name: .fill-{name} 816 | | 817 | */ 818 | 819 | svgFill: { 820 | 'current': 'currentColor', 821 | }, 822 | 823 | 824 | /* 825 | |----------------------------------------------------------------------------- 826 | | SVG stroke https://tailwindcss.com/docs/svg 827 | |----------------------------------------------------------------------------- 828 | | 829 | | Here is where you define your SVG stroke colors. By default we just provide 830 | | `stroke-current` which sets the stroke to the current text color. This lets 831 | | you specify a stroke color using existing text color utilities and helps 832 | | keep the generated CSS file size down. 833 | | 834 | | Class name: .stroke-{name} 835 | | 836 | */ 837 | 838 | svgStroke: { 839 | 'current': 'currentColor', 840 | }, 841 | 842 | 843 | /* 844 | |----------------------------------------------------------------------------- 845 | | Modules https://tailwindcss.com/docs/configuration#modules 846 | |----------------------------------------------------------------------------- 847 | | 848 | | Here is where you control which modules are generated and what variants are 849 | | generated for each of those modules. 850 | | 851 | | Currently supported variants: 852 | | - responsive 853 | | - hover 854 | | - focus 855 | | - active 856 | | - group-hover 857 | | 858 | | To disable a module completely, use `false` instead of an array. 859 | | 860 | */ 861 | 862 | modules: { 863 | appearance: ['responsive'], 864 | backgroundAttachment: ['responsive'], 865 | backgroundColors: ['responsive', 'hover', 'focus'], 866 | backgroundPosition: ['responsive'], 867 | backgroundRepeat: ['responsive'], 868 | backgroundSize: ['responsive'], 869 | borderCollapse: [], 870 | borderColors: ['responsive', 'hover', 'focus'], 871 | borderRadius: ['responsive'], 872 | borderStyle: ['responsive'], 873 | borderWidths: ['responsive'], 874 | cursor: ['responsive'], 875 | display: ['responsive'], 876 | flexbox: ['responsive'], 877 | float: ['responsive'], 878 | fonts: ['responsive'], 879 | fontWeights: ['responsive', 'hover', 'focus'], 880 | height: ['responsive'], 881 | leading: ['responsive'], 882 | lists: ['responsive'], 883 | margin: ['responsive'], 884 | maxHeight: ['responsive'], 885 | maxWidth: ['responsive'], 886 | minHeight: ['responsive'], 887 | minWidth: ['responsive'], 888 | negativeMargin: ['responsive'], 889 | opacity: ['responsive'], 890 | outline: ['focus'], 891 | overflow: ['responsive'], 892 | padding: ['responsive'], 893 | pointerEvents: ['responsive'], 894 | position: ['responsive'], 895 | resize: ['responsive'], 896 | shadows: ['responsive', 'hover', 'focus'], 897 | svgFill: [], 898 | svgStroke: [], 899 | tableLayout: ['responsive'], 900 | textAlign: ['responsive'], 901 | textColors: ['responsive', 'hover', 'focus'], 902 | textSizes: ['responsive'], 903 | textStyle: ['responsive', 'hover', 'focus'], 904 | tracking: ['responsive'], 905 | userSelect: ['responsive'], 906 | verticalAlign: ['responsive'], 907 | visibility: ['responsive'], 908 | whitespace: ['responsive'], 909 | width: ['responsive'], 910 | zIndex: ['responsive'], 911 | }, 912 | 913 | 914 | /* 915 | |----------------------------------------------------------------------------- 916 | | Plugins https://tailwindcss.com/docs/plugins 917 | |----------------------------------------------------------------------------- 918 | | 919 | | Here is where you can register any plugins you'd like to use in your 920 | | project. Tailwind's built-in `container` plugin is enabled by default to 921 | | give you a Bootstrap-style responsive container component out of the box. 922 | | 923 | | Be sure to view the complete plugin documentation to learn more about how 924 | | the plugin system works. 925 | | 926 | */ 927 | 928 | plugins: [ 929 | require('tailwindcss/plugins/container')({ 930 | // center: true, 931 | // padding: '1rem', 932 | }), 933 | ], 934 | 935 | 936 | /* 937 | |----------------------------------------------------------------------------- 938 | | Advanced Options https://tailwindcss.com/docs/configuration#options 939 | |----------------------------------------------------------------------------- 940 | | 941 | | Here is where you can tweak advanced configuration options. We recommend 942 | | leaving these options alone unless you absolutely need to change them. 943 | | 944 | */ 945 | 946 | options: { 947 | prefix: '', 948 | important: false, 949 | separator: ':', 950 | }, 951 | 952 | } 953 | -------------------------------------------------------------------------------- /tests/DatabaseDriverTest.php: -------------------------------------------------------------------------------- 1 | withFactories(__DIR__.'/../database/factories'); 29 | $this->translation = $this->app[Translation::class]; 30 | } 31 | 32 | protected function getEnvironmentSetUp($app) 33 | { 34 | $app['config']->set('translation.driver', 'database'); 35 | $app['config']->set('database.default', 'testing'); 36 | $app['config']->set('database.connections.testing', [ 37 | 'driver' => 'sqlite', 38 | 'database' => ':memory:', 39 | ]); 40 | } 41 | 42 | protected function getPackageProviders($app) 43 | { 44 | return [ 45 | TranslationServiceProvider::class, 46 | TranslationBindingsServiceProvider::class, 47 | ]; 48 | } 49 | 50 | /** @test */ 51 | public function it_returns_all_languages() 52 | { 53 | $newLanguages = factory(Language::class, 2)->create(); 54 | $newLanguages = $newLanguages->mapWithKeys(function ($language) { 55 | return [$language->language => $language->name]; 56 | })->toArray(); 57 | $languages = $this->translation->allLanguages(); 58 | 59 | $this->assertEquals($languages->count(), 3); 60 | $this->assertEquals($languages->toArray(), ['en' => 'en'] + $newLanguages); 61 | } 62 | 63 | /** @test */ 64 | public function it_returns_all_translations() 65 | { 66 | $default = Language::where('language', config('app.locale'))->first(); 67 | factory(Language::class)->create(['language' => 'es', 'name' => 'Español']); 68 | factory(TranslationModel::class)->states('group')->create(['language_id' => $default->id, 'group' => 'test', 'key' => 'hello', 'value' => 'Hello']); 69 | factory(TranslationModel::class)->states('group')->create(['language_id' => $default->id, 'group' => 'test', 'key' => 'whats_up', 'value' => "What's up!"]); 70 | factory(TranslationModel::class)->states('single')->create(['language_id' => $default->id, 'group' => 'single', 'key' => 'Hello', 'value' => 'Hello']); 71 | factory(TranslationModel::class)->states('single')->create(['language_id' => $default->id, 'group' => 'single', 'key' => "What's up", 'value' => "What's up!"]); 72 | 73 | $translations = $this->translation->allTranslations(); 74 | 75 | $this->assertEquals($translations->count(), 2); 76 | $this->assertEquals(['single' => ['single' => ['Hello' => 'Hello', "What's up" => "What's up!"]], 'group' => ['test' => ['hello' => 'Hello', 'whats_up' => "What's up!"]]], $translations->toArray()['en']); 77 | $this->assertArrayHasKey('en', $translations->toArray()); 78 | $this->assertArrayHasKey('es', $translations->toArray()); 79 | } 80 | 81 | /** @test */ 82 | public function it_returns_all_translations_for_a_given_language() 83 | { 84 | $default = Language::where('language', config('app.locale'))->first(); 85 | factory(TranslationModel::class)->states('group')->create(['language_id' => $default->id, 'group' => 'test', 'key' => 'hello', 'value' => 'Hello']); 86 | factory(TranslationModel::class)->states('group')->create(['language_id' => $default->id, 'group' => 'test', 'key' => 'whats_up', 'value' => "What's up!"]); 87 | factory(TranslationModel::class)->states('single')->create(['language_id' => $default->id, 'group' => 'single', 'key' => 'Hello', 'value' => 'Hello']); 88 | factory(TranslationModel::class)->states('single')->create(['language_id' => $default->id, 'group' => 'single', 'key' => "What's up", 'value' => "What's up!"]); 89 | 90 | $translations = $this->translation->allTranslationsFor('en'); 91 | $this->assertEquals($translations->count(), 2); 92 | $this->assertEquals(['single' => ['single' => ['Hello' => 'Hello', "What's up" => "What's up!"]], 'group' => ['test' => ['hello' => 'Hello', 'whats_up' => "What's up!"]]], $translations->toArray()); 93 | $this->assertArrayHasKey('single', $translations->toArray()); 94 | $this->assertArrayHasKey('group', $translations->toArray()); 95 | } 96 | 97 | /** @test */ 98 | public function it_throws_an_exception_if_a_language_exists() 99 | { 100 | $this->expectException(LanguageExistsException::class); 101 | $this->translation->addLanguage('en'); 102 | } 103 | 104 | /** @test */ 105 | public function it_can_add_a_new_language() 106 | { 107 | $this->assertDatabaseMissing(config('translation.database.languages_table'), [ 108 | 'language' => 'fr', 109 | 'name' => 'Français', 110 | ]); 111 | 112 | $this->translation->addLanguage('fr', 'Français'); 113 | $this->assertDatabaseHas(config('translation.database.languages_table'), [ 114 | 'language' => 'fr', 115 | 'name' => 'Français', 116 | ]); 117 | } 118 | 119 | /** @test */ 120 | public function it_can_add_a_new_translation_to_a_new_group() 121 | { 122 | $this->translation->addGroupTranslation('es', 'test', 'hello', 'Hola!'); 123 | 124 | $translations = $this->translation->allTranslationsFor('es'); 125 | 126 | $this->assertEquals(['test' => ['hello' => 'Hola!']], $translations->toArray()['group']); 127 | } 128 | 129 | /** @test */ 130 | public function it_can_add_a_new_translation_to_an_existing_translation_group() 131 | { 132 | $translation = factory(TranslationModel::class)->create(); 133 | 134 | $this->translation->addGroupTranslation($translation->language->language, "{$translation->group}", 'test', 'Testing'); 135 | 136 | $translations = $this->translation->allTranslationsFor($translation->language->language); 137 | $this->assertSame([$translation->group => [$translation->key => $translation->value, 'test' => 'Testing']], $translations->toArray()['group']); 138 | } 139 | 140 | /** @test */ 141 | public function it_can_add_a_new_single_translation() 142 | { 143 | $this->translation->addSingleTranslation('es', 'single', 'Hello', 'Hola!'); 144 | 145 | $translations = $this->translation->allTranslationsFor('es'); 146 | 147 | $this->assertEquals(['single' => ['Hello' => 'Hola!']], $translations->toArray()['single']); 148 | } 149 | 150 | /** @test */ 151 | public function it_can_add_a_new_single_translation_to_an_existing_language() 152 | { 153 | $translation = factory(TranslationModel::class)->states('single')->create(); 154 | 155 | $this->translation->addSingleTranslation($translation->language->language, 'single', 'Test', 'Testing'); 156 | 157 | $translations = $this->translation->allTranslationsFor($translation->language->language); 158 | 159 | $this->assertEquals(['single' => ['Test' => 'Testing', $translation->key => $translation->value]], $translations->toArray()['single']); 160 | } 161 | 162 | /** @test */ 163 | public function it_can_get_a_collection_of_group_names_for_a_given_language() 164 | { 165 | $language = factory(Language::class)->create(['language' => 'en']); 166 | factory(TranslationModel::class)->create([ 167 | 'language_id' => $language->id, 168 | 'group' => 'test', 169 | ]); 170 | 171 | $groups = $this->translation->getGroupsFor('en'); 172 | 173 | $this->assertEquals($groups->toArray(), ['test']); 174 | } 175 | 176 | /** @test */ 177 | public function it_can_merge_a_language_with_the_base_language() 178 | { 179 | $default = Language::where('language', config('app.locale'))->first(); 180 | factory(TranslationModel::class)->states('group')->create(['language_id' => $default->id, 'group' => 'test', 'key' => 'hello', 'value' => 'Hello']); 181 | factory(TranslationModel::class)->states('group')->create(['language_id' => $default->id, 'group' => 'test', 'key' => 'whats_up', 'value' => "What's up!"]); 182 | factory(TranslationModel::class)->states('single')->create(['language_id' => $default->id, 'group' => 'single', 'key' => 'Hello', 'value' => 'Hello']); 183 | factory(TranslationModel::class)->states('single')->create(['language_id' => $default->id, 'group' => 'single', 'key' => "What's up", 'value' => "What's up!"]); 184 | 185 | $this->translation->addGroupTranslation('es', 'test', 'hello', 'Hola!'); 186 | $translations = $this->translation->getSourceLanguageTranslationsWith('es'); 187 | 188 | $this->assertEquals($translations->toArray(), [ 189 | 'group' => [ 190 | 'test' => [ 191 | 'hello' => ['en' => 'Hello', 'es' => 'Hola!'], 192 | 'whats_up' => ['en' => "What's up!", 'es' => ''], 193 | ], 194 | ], 195 | 'single' => [ 196 | 'single' => [ 197 | 'Hello' => [ 198 | 'en' => 'Hello', 199 | 'es' => '', 200 | ], 201 | "What's up" => [ 202 | 'en' => "What's up!", 203 | 'es' => '', 204 | ], 205 | ], 206 | ], 207 | ]); 208 | } 209 | 210 | /** @test */ 211 | public function it_can_add_a_vendor_namespaced_translations() 212 | { 213 | $this->translation->addGroupTranslation('es', 'translation_test::test', 'hello', 'Hola!'); 214 | 215 | $this->assertEquals($this->translation->allTranslationsFor('es')->toArray(), [ 216 | 'group' => [ 217 | 'translation_test::test' => [ 218 | 'hello' => 'Hola!', 219 | ], 220 | ], 221 | 'single' => [], 222 | ]); 223 | } 224 | 225 | /** @test */ 226 | public function it_can_add_a_nested_translation() 227 | { 228 | $this->translation->addGroupTranslation('en', 'test', 'test.nested', 'Nested!'); 229 | 230 | $this->assertEquals($this->translation->getGroupTranslationsFor('en')->toArray(), [ 231 | 'test' => [ 232 | 'test.nested' => 'Nested!', 233 | ], 234 | ]); 235 | } 236 | 237 | /** @test */ 238 | public function it_can_add_nested_vendor_namespaced_translations() 239 | { 240 | $this->translation->addGroupTranslation('es', 'translation_test::test', 'nested.hello', 'Hola!'); 241 | 242 | $this->assertEquals($this->translation->allTranslationsFor('es')->toArray(), [ 243 | 'group' => [ 244 | 'translation_test::test' => [ 245 | 'nested.hello' => 'Hola!', 246 | ], 247 | ], 248 | 'single' => [], 249 | ]); 250 | } 251 | 252 | /** @test */ 253 | public function it_can_merge_a_namespaced_language_with_the_base_language() 254 | { 255 | $this->translation->addGroupTranslation('en', 'translation_test::test', 'hello', 'Hello'); 256 | $this->translation->addGroupTranslation('es', 'translation_test::test', 'hello', 'Hola!'); 257 | $translations = $this->translation->getSourceLanguageTranslationsWith('es'); 258 | 259 | $this->assertEquals($translations->toArray(), [ 260 | 'group' => [ 261 | 'translation_test::test' => [ 262 | 'hello' => ['en' => 'Hello', 'es' => 'Hola!'], 263 | ], 264 | ], 265 | 'single' => [], 266 | ]); 267 | } 268 | 269 | /** @test */ 270 | public function a_list_of_languages_can_be_viewed() 271 | { 272 | $newLanguages = factory(Language::class, 2)->create(); 273 | $response = $this->get(config('translation.ui_url')); 274 | 275 | $response->assertSee(config('app.locale')); 276 | foreach ($newLanguages as $language) { 277 | $response->assertSee($language->language); 278 | } 279 | } 280 | 281 | /** @test */ 282 | public function the_language_creation_page_can_be_viewed() 283 | { 284 | $this->translation->addGroupTranslation(config('app.locale'), 'translation::translation', 'add_language', 'Add a new language'); 285 | $this->get(config('translation.ui_url').'/create') 286 | ->assertSee('Add a new language'); 287 | } 288 | 289 | /** @test */ 290 | public function a_language_can_be_added() 291 | { 292 | $this->post(config('translation.ui_url'), ['locale' => 'de']) 293 | ->assertRedirect(); 294 | 295 | $this->assertDatabaseHas('languages', ['language' => 'de']); 296 | } 297 | 298 | /** @test */ 299 | public function a_list_of_translations_can_be_viewed() 300 | { 301 | $default = Language::where('language', config('app.locale'))->first(); 302 | factory(TranslationModel::class)->states('group')->create(['language_id' => $default->id, 'group' => 'test', 'key' => 'hello', 'value' => 'Hello']); 303 | factory(TranslationModel::class)->states('group')->create(['language_id' => $default->id, 'group' => 'test', 'key' => 'whats_up', 'value' => "What's up!"]); 304 | factory(TranslationModel::class)->states('single')->create(['language_id' => $default->id, 'key' => 'Hello', 'value' => 'Hello!']); 305 | factory(TranslationModel::class)->states('single')->create(['language_id' => $default->id, 'key' => "What's up", 'value' => 'Sup!']); 306 | 307 | $this->get(config('translation.ui_url').'/en/translations') 308 | ->assertSee('hello') 309 | ->assertSee('whats_up') 310 | ->assertSee('Hello') 311 | ->assertSee('Sup!'); 312 | } 313 | 314 | /** @test */ 315 | public function the_translation_creation_page_can_be_viewed() 316 | { 317 | $this->translation->addGroupTranslation('en', 'translation::translation', 'add_translation', 'Add a translation'); 318 | $this->get(config('translation.ui_url').'/'.config('app.locale').'/translations/create') 319 | ->assertSee('Add a translation'); 320 | } 321 | 322 | /** @test */ 323 | public function a_new_translation_can_be_added() 324 | { 325 | $this->post(config('translation.ui_url').'/'.config('app.locale').'/translations', ['group' => 'single', 'key' => 'joe', 'value' => 'is cool']) 326 | ->assertRedirect(); 327 | 328 | $this->assertDatabaseHas('translations', ['language_id' => 1, 'key' => 'joe', 'value' => 'is cool']); 329 | } 330 | 331 | /** @test */ 332 | public function a_translation_can_be_updated() 333 | { 334 | $default = Language::where('language', config('app.locale'))->first(); 335 | factory(TranslationModel::class)->states('group')->create(['language_id' => $default->id, 'group' => 'test', 'key' => 'hello', 'value' => 'Hello']); 336 | $this->assertDatabaseHas('translations', ['language_id' => 1, 'group' => 'test', 'key' => 'hello', 'value' => 'Hello']); 337 | 338 | $this->post(config('translation.ui_url').'/en', ['group' => 'test', 'key' => 'hello', 'value' => 'Hello there!']) 339 | ->assertStatus(200); 340 | 341 | $this->assertDatabaseHas('translations', ['language_id' => 1, 'group' => 'test', 'key' => 'hello', 'value' => 'Hello there!']); 342 | } 343 | 344 | /** @test */ 345 | public function adding_a_translation_fires_an_event_with_the_expected_data() 346 | { 347 | Event::fake(); 348 | 349 | $data = ['key' => 'joe', 'value' => 'is cool']; 350 | $this->post(config('translation.ui_url').'/en/translations', $data); 351 | 352 | Event::assertDispatched(TranslationAdded::class, function ($event) use ($data) { 353 | return $event->language === 'en' && 354 | $event->group === 'single' && 355 | $event->value === $data['value'] && 356 | $event->key === $data['key']; 357 | }); 358 | } 359 | 360 | /** @test */ 361 | public function updating_a_translation_fires_an_event_with_the_expected_data() 362 | { 363 | Event::fake(); 364 | 365 | $data = ['group' => 'test', 'key' => 'hello', 'value' => 'Hello there!']; 366 | $this->post(config('translation.ui_url').'/en/translations', $data); 367 | 368 | Event::assertDispatched(TranslationAdded::class, function ($event) use ($data) { 369 | return $event->language === 'en' && 370 | $event->group === $data['group'] && 371 | $event->value === $data['value'] && 372 | $event->key === $data['key']; 373 | }); 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /tests/FileDriverTest.php: -------------------------------------------------------------------------------- 1 | translation = app()->make(Translation::class); 25 | } 26 | 27 | protected function getPackageProviders($app) 28 | { 29 | return [ 30 | TranslationServiceProvider::class, 31 | TranslationBindingsServiceProvider::class, 32 | ]; 33 | } 34 | 35 | protected function getEnvironmentSetUp($app) 36 | { 37 | $app['config']->set('translation.driver', 'file'); 38 | } 39 | 40 | /** @test */ 41 | public function it_returns_all_languages() 42 | { 43 | $languages = $this->translation->allLanguages(); 44 | 45 | $this->assertEquals($languages->count(), 2); 46 | $this->assertEquals($languages->toArray(), ['en' => 'en', 'es' => 'es']); 47 | } 48 | 49 | /** @test */ 50 | public function it_returns_all_translations() 51 | { 52 | $translations = $this->translation->allTranslations(); 53 | 54 | $this->assertEquals($translations->count(), 2); 55 | $this->assertEquals(['single' => ['single' => ['Hello' => 'Hello', "What's up" => "What's up!"]], 'group' => ['test' => ['hello' => 'Hello', 'whats_up' => "What's up!"]]], $translations->toArray()['en']); 56 | $this->assertArrayHasKey('en', $translations->toArray()); 57 | $this->assertArrayHasKey('es', $translations->toArray()); 58 | } 59 | 60 | /** @test */ 61 | public function it_returns_all_translations_for_a_given_language() 62 | { 63 | $translations = $this->translation->allTranslationsFor('en'); 64 | $this->assertEquals($translations->count(), 2); 65 | $this->assertEquals(['single' => ['single' => ['Hello' => 'Hello', "What's up" => "What's up!"]], 'group' => ['test' => ['hello' => 'Hello', 'whats_up' => "What's up!"]]], $translations->toArray()); 66 | $this->assertArrayHasKey('single', $translations->toArray()); 67 | $this->assertArrayHasKey('group', $translations->toArray()); 68 | } 69 | 70 | /** @test */ 71 | public function it_throws_an_exception_if_a_language_exists() 72 | { 73 | $this->expectException(LanguageExistsException::class); 74 | $this->translation->addLanguage('en'); 75 | } 76 | 77 | /** @test */ 78 | public function it_can_add_a_new_language() 79 | { 80 | $this->translation->addLanguage('fr'); 81 | 82 | $this->assertTrue(file_exists(__DIR__.'/fixtures/lang/fr.json')); 83 | $this->assertTrue(file_exists(__DIR__.'/fixtures/lang/fr')); 84 | 85 | rmdir(__DIR__.'/fixtures/lang/fr'); 86 | unlink(__DIR__.'/fixtures/lang/fr.json'); 87 | } 88 | 89 | /** @test */ 90 | public function it_can_add_a_new_translation_to_a_new_group() 91 | { 92 | $this->translation->addGroupTranslation('es', 'test', 'hello', 'Hola!'); 93 | 94 | $translations = $this->translation->allTranslationsFor('es'); 95 | 96 | $this->assertEquals(['test' => ['hello' => 'Hola!']], $translations->toArray()['group']); 97 | 98 | unlink(__DIR__.'/fixtures/lang/es/test.php'); 99 | } 100 | 101 | /** @test */ 102 | public function it_can_add_a_new_translation_to_an_existing_translation_group() 103 | { 104 | $this->translation->addGroupTranslation('en', 'test', 'test', 'Testing'); 105 | 106 | $translations = $this->translation->allTranslationsFor('en'); 107 | 108 | $this->assertEquals(['test' => ['hello' => 'Hello', 'whats_up' => 'What\'s up!', 'test' => 'Testing']], $translations->toArray()['group']); 109 | 110 | file_put_contents( 111 | app()['path.lang'].'/en/test.php', 112 | " 'Hello', 'whats_up' => 'What\'s up!'], true).';'.\PHP_EOL 113 | ); 114 | } 115 | 116 | /** @test */ 117 | public function it_can_add_a_new_single_translation() 118 | { 119 | $this->translation->addSingleTranslation('es', 'single', 'Hello', 'Hola!'); 120 | 121 | $translations = $this->translation->allTranslationsFor('es'); 122 | 123 | $this->assertEquals(['single' => ['Hello' => 'Hola!']], $translations->toArray()['single']); 124 | 125 | unlink(__DIR__.'/fixtures/lang/es.json'); 126 | } 127 | 128 | /** @test */ 129 | public function it_can_add_a_new_single_translation_to_an_existing_language() 130 | { 131 | $this->translation->addSingleTranslation('en', 'single', 'Test', 'Testing'); 132 | 133 | $translations = $this->translation->allTranslationsFor('en'); 134 | 135 | $this->assertEquals(['single' => ['Hello' => 'Hello', 'What\'s up' => 'What\'s up!', 'Test' => 'Testing']], $translations->toArray()['single']); 136 | 137 | file_put_contents( 138 | app()['path.lang'].'/en.json', 139 | json_encode((object) ['Hello' => 'Hello', 'What\'s up' => 'What\'s up!'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) 140 | ); 141 | } 142 | 143 | /** @test */ 144 | public function it_can_get_a_collection_of_group_names_for_a_given_language() 145 | { 146 | $groups = $this->translation->getGroupsFor('en'); 147 | 148 | $this->assertEquals($groups->toArray(), ['test']); 149 | } 150 | 151 | /** @test */ 152 | public function it_can_merge_a_language_with_the_base_language() 153 | { 154 | $this->translation->addGroupTranslation('es', 'test', 'hello', 'Hola!'); 155 | $translations = $this->translation->getSourceLanguageTranslationsWith('es'); 156 | 157 | $this->assertEquals($translations->toArray(), [ 158 | 'group' => [ 159 | 'test' => [ 160 | 'hello' => ['en' => 'Hello', 'es' => 'Hola!'], 161 | 'whats_up' => ['en' => "What's up!", 'es' => ''], 162 | ], 163 | ], 164 | 'single' => [ 165 | 'single' => [ 166 | 'Hello' => [ 167 | 'en' => 'Hello', 168 | 'es' => '', 169 | ], 170 | "What's up" => [ 171 | 'en' => "What's up!", 172 | 'es' => '', 173 | ], 174 | ], 175 | ], 176 | ]); 177 | 178 | unlink(__DIR__.'/fixtures/lang/es/test.php'); 179 | } 180 | 181 | /** @test */ 182 | public function it_can_add_a_vendor_namespaced_translations() 183 | { 184 | $this->translation->addGroupTranslation('es', 'translation_test::test', 'hello', 'Hola!'); 185 | 186 | $this->assertEquals($this->translation->allTranslationsFor('es')->toArray(), [ 187 | 'group' => [ 188 | 'translation_test::test' => [ 189 | 'hello' => 'Hola!', 190 | ], 191 | ], 192 | 'single' => [], 193 | ]); 194 | 195 | \File::deleteDirectory(__DIR__.'/fixtures/lang/vendor'); 196 | } 197 | 198 | /** @test */ 199 | public function it_can_add_a_nested_translation() 200 | { 201 | $this->translation->addGroupTranslation('en', 'test', 'test.nested', 'Nested!'); 202 | 203 | $this->assertEquals($this->translation->getGroupTranslationsFor('en')->toArray(), [ 204 | 'test' => [ 205 | 'hello' => 'Hello', 206 | 'test.nested' => 'Nested!', 207 | 'whats_up' => 'What\'s up!', 208 | ], 209 | ]); 210 | 211 | file_put_contents( 212 | app()['path.lang'].'/en/test.php', 213 | " 'Hello', 'whats_up' => 'What\'s up!'], true).';'.\PHP_EOL 214 | ); 215 | } 216 | 217 | /** @test */ 218 | public function it_can_add_nested_vendor_namespaced_translations() 219 | { 220 | $this->translation->addGroupTranslation('es', 'translation_test::test', 'nested.hello', 'Hola!'); 221 | 222 | $this->assertEquals($this->translation->allTranslationsFor('es')->toArray(), [ 223 | 'group' => [ 224 | 'translation_test::test' => [ 225 | 'nested.hello' => 'Hola!', 226 | ], 227 | ], 228 | 'single' => [], 229 | ]); 230 | 231 | \File::deleteDirectory(__DIR__.'/fixtures/lang/vendor'); 232 | } 233 | 234 | /** @test */ 235 | public function it_can_merge_a_namespaced_language_with_the_base_language() 236 | { 237 | $this->translation->addGroupTranslation('en', 'translation_test::test', 'hello', 'Hello'); 238 | $this->translation->addGroupTranslation('es', 'translation_test::test', 'hello', 'Hola!'); 239 | $translations = $this->translation->getSourceLanguageTranslationsWith('es'); 240 | 241 | $this->assertEquals($translations->toArray(), [ 242 | 'group' => [ 243 | 'test' => [ 244 | 'hello' => ['en' => 'Hello', 'es' => ''], 245 | 'whats_up' => ['en' => "What's up!", 'es' => ''], 246 | ], 247 | 'translation_test::test' => [ 248 | 'hello' => ['en' => 'Hello', 'es' => 'Hola!'], 249 | ], 250 | ], 251 | 'single' => [ 252 | 'single' => [ 253 | 'Hello' => [ 254 | 'en' => 'Hello', 255 | 'es' => '', 256 | ], 257 | "What's up" => [ 258 | 'en' => "What's up!", 259 | 'es' => '', 260 | ], 261 | ], 262 | ], 263 | ]); 264 | 265 | \File::deleteDirectory(__DIR__.'/fixtures/lang/vendor'); 266 | } 267 | 268 | /** @test */ 269 | public function a_list_of_languages_can_be_viewed() 270 | { 271 | $this->get(config('translation.ui_url')) 272 | ->assertSee('en'); 273 | } 274 | 275 | /** @test */ 276 | public function the_language_creation_page_can_be_viewed() 277 | { 278 | $this->get(config('translation.ui_url').'/create') 279 | ->assertSee('Add a new language'); 280 | } 281 | 282 | /** @test */ 283 | public function a_language_can_be_added() 284 | { 285 | $this->post(config('translation.ui_url'), ['locale' => 'de']) 286 | ->assertRedirect(); 287 | 288 | $this->assertTrue(file_exists(__DIR__.'/fixtures/lang/de.json')); 289 | $this->assertTrue(file_exists(__DIR__.'/fixtures/lang/de')); 290 | 291 | rmdir(__DIR__.'/fixtures/lang/de'); 292 | unlink(__DIR__.'/fixtures/lang/de.json'); 293 | } 294 | 295 | /** @test */ 296 | public function a_list_of_translations_can_be_viewed() 297 | { 298 | $this->get(config('translation.ui_url').'/en/translations') 299 | ->assertSee('hello') 300 | ->assertSee('whats_up'); 301 | } 302 | 303 | /** @test */ 304 | public function the_translation_creation_page_can_be_viewed() 305 | { 306 | $this->get(config('translation.ui_url').'/'.config('app.locale').'/translations/create') 307 | ->assertSee('Add a translation'); 308 | } 309 | 310 | /** @test */ 311 | public function a_new_translation_can_be_added() 312 | { 313 | $this->post(config('translation.ui_url').'/en/translations', ['key' => 'joe', 'value' => 'is cool']) 314 | ->assertRedirect(); 315 | $translations = $this->translation->getSingleTranslationsFor('en'); 316 | 317 | $this->assertEquals(['Hello' => 'Hello', 'What\'s up' => 'What\'s up!', 'joe' => 'is cool'], $translations->toArray()['single']); 318 | 319 | file_put_contents( 320 | app()['path.lang'].'/en.json', 321 | json_encode((object) ['Hello' => 'Hello', 'What\'s up' => 'What\'s up!'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) 322 | ); 323 | } 324 | 325 | /** @test */ 326 | public function a_translation_can_be_updated() 327 | { 328 | $this->post(config('translation.ui_url').'/en', ['group' => 'test', 'key' => 'hello', 'value' => 'Hello there!']) 329 | ->assertStatus(200); 330 | 331 | $translations = $this->translation->getGroupTranslationsFor('en'); 332 | 333 | $this->assertEquals(['hello' => 'Hello there!', 'whats_up' => 'What\'s up!'], $translations->toArray()['test']); 334 | 335 | file_put_contents( 336 | app()['path.lang'].'/en/test.php', 337 | " 'Hello', 'whats_up' => 'What\'s up!'], true).';'.\PHP_EOL 338 | ); 339 | } 340 | 341 | /** @test */ 342 | public function adding_a_translation_fires_an_event_with_the_expected_data() 343 | { 344 | Event::fake(); 345 | 346 | $data = ['key' => 'joe', 'value' => 'is cool']; 347 | $this->post(config('translation.ui_url').'/en/translations', $data); 348 | 349 | Event::assertDispatched(TranslationAdded::class, function ($event) use ($data) { 350 | return $event->language === 'en' && 351 | $event->group === 'single' && 352 | $event->value === $data['value'] && 353 | $event->key === $data['key']; 354 | }); 355 | file_put_contents( 356 | app()['path.lang'].'/en.json', 357 | json_encode((object) ['Hello' => 'Hello', 'What\'s up' => 'What\'s up!'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) 358 | ); 359 | } 360 | 361 | /** @test */ 362 | public function updating_a_translation_fires_an_event_with_the_expected_data() 363 | { 364 | Event::fake(); 365 | 366 | $data = ['group' => 'test', 'key' => 'hello', 'value' => 'Hello there!']; 367 | $this->post(config('translation.ui_url').'/en/translations', $data); 368 | 369 | Event::assertDispatched(TranslationAdded::class, function ($event) use ($data) { 370 | return $event->language === 'en' && 371 | $event->group === $data['group'] && 372 | $event->value === $data['value'] && 373 | $event->key === $data['key']; 374 | }); 375 | file_put_contents( 376 | app()['path.lang'].'/en/test.php', 377 | " 'Hello', 'whats_up' => 'What\'s up!'], true).';'.\PHP_EOL 378 | ); 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /tests/PackageIsLoadedTest.php: -------------------------------------------------------------------------------- 1 | assertArrayHasKey(TranslationServiceProvider::class, app()->getLoadedProviders()); 23 | $this->assertArrayHasKey(TranslationBindingsServiceProvider::class, app()->getLoadedProviders()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/ScannerTest.php: -------------------------------------------------------------------------------- 1 | set('translation.scan_paths', __DIR__.'/fixtures/scan-tests'); 25 | $app['config']->set('translation.translation_methods', ['__', 'trans', 'trans_choice', '@lang', 'Lang::get']); 26 | } 27 | 28 | /** @test */ 29 | public function it_finds_all_translations() 30 | { 31 | $this->scanner = app()->make(Scanner::class); 32 | $matches = $this->scanner->findTranslations(); 33 | 34 | $this->assertEquals($matches, ['single' => ['single' => ['This will go in the JSON array' => '', 'This will also go in the JSON array' => '', 'trans' => '']], 'group' => ['lang' => ['first_match' => ''], 'lang_get' => ['first' => '', 'second' => ''], 'trans' => ['first_match' => '', 'third_match' => ''], 'trans_choice' => ['with_params' => '']]]); 35 | $this->assertCount(2, $matches); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/fixtures/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Hello": "Hello", 3 | "What's up": "What's up!" 4 | } -------------------------------------------------------------------------------- /tests/fixtures/lang/en/test.php: -------------------------------------------------------------------------------- 1 | 'Hello', 5 | 'whats_up' => 'What\'s up!', 6 | ); 7 | -------------------------------------------------------------------------------- /tests/fixtures/lang/es/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joedixon/laravel-translation/feba4d1e3d12722ca60c05d9180f39b7de227e4e/tests/fixtures/lang/es/.gitignore -------------------------------------------------------------------------------- /tests/fixtures/scan-tests/__.txt: -------------------------------------------------------------------------------- 1 | 2 | __('This will go in the JSON array') 3 | 4 | __( 5 | 'This will also go in the JSON array' 6 | ) -------------------------------------------------------------------------------- /tests/fixtures/scan-tests/at_lang.txt: -------------------------------------------------------------------------------- 1 | 2 | @lang('lang.first_match') -------------------------------------------------------------------------------- /tests/fixtures/scan-tests/lang_get.txt: -------------------------------------------------------------------------------- 1 | 2 | Lang::get('lang_get.first') 3 | Lang::get('lang_get.second'); -------------------------------------------------------------------------------- /tests/fixtures/scan-tests/trans.txt: -------------------------------------------------------------------------------- 1 | 2 | trans('trans.first_match'); 3 | trans('trans'); 4 | trans('trans.third_match'); 5 | -------------------------------------------------------------------------------- /tests/fixtures/scan-tests/trans_choice.txt: -------------------------------------------------------------------------------- 1 | 2 | trans_choice('trans_choice.with_params', ['parameters' => 'Go here']) -------------------------------------------------------------------------------- /translation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joedixon/laravel-translation/feba4d1e3d12722ca60c05d9180f39b7de227e4e/translation.png -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | let mix = require('laravel-mix'); 2 | var tailwindcss = require('tailwindcss'); 3 | 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Mix Asset Management 7 | |-------------------------------------------------------------------------- 8 | | 9 | | Mix provides a clean, fluent API for defining some Webpack build steps 10 | | for your Laravel application. By default, we are compiling the Sass 11 | | file for your application, as well as bundling up your JS files. 12 | | 13 | */ 14 | 15 | mix.setPublicPath('public/assets'); 16 | // mix.setPublicPath('../../../public/vendor/translation'); 17 | 18 | mix.postCss('resources/assets/css/main.css', 'css', [ 19 | tailwindcss('./tailwind.js'), 20 | ]).js('resources/assets/js/app.js', 'js') 21 | 22 | // Full API 23 | // mix.js(src, output); 24 | // mix.react(src, output); <-- Identical to mix.js(), but registers React Babel compilation. 25 | // mix.preact(src, output); <-- Identical to mix.js(), but registers Preact compilation. 26 | // mix.coffee(src, output); <-- Identical to mix.js(), but registers CoffeeScript compilation. 27 | // mix.ts(src, output); <-- TypeScript support. Requires tsconfig.json to exist in the same folder as webpack.mix.js 28 | // mix.extract(vendorLibs); 29 | // mix.sass(src, output); 30 | // mix.standaloneSass('src', output); <-- Faster, but isolated from Webpack. 31 | // mix.fastSass('src', output); <-- Alias for mix.standaloneSass(). 32 | // mix.less(src, output); 33 | // mix.stylus(src, output); 34 | // mix.postCss(src, output, [require('postcss-some-plugin')()]); 35 | // mix.browserSync('my-site.test'); 36 | // mix.combine(files, destination); 37 | // mix.babel(files, destination); <-- Identical to mix.combine(), but also includes Babel compilation. 38 | // mix.copy(from, to); 39 | // mix.copyDirectory(fromDir, toDir); 40 | // mix.minify(file); 41 | // mix.sourceMaps(); // Enable sourcemaps 42 | // mix.version(); // Enable versioning. 43 | // mix.disableNotifications(); 44 | // mix.setPublicPath('path/to/public'); 45 | // mix.setResourceRoot('prefix/for/resource/locators'); 46 | // mix.autoload({}); <-- Will be passed to Webpack's ProvidePlugin. 47 | // mix.webpackConfig({}); <-- Override webpack.config.js, without editing the file directly. 48 | // mix.babelConfig({}); <-- Merge extra Babel configuration (plugins, etc.) with Mix's default. 49 | // mix.then(function () {}) <-- Will be triggered each time Webpack finishes building. 50 | // mix.extend(name, handler) <-- Extend Mix's API with your own components. 51 | // mix.options({ 52 | // extractVueStyles: false, // Extract .vue component styling to file, rather than inline. 53 | // globalVueStyles: file, // Variables file to be imported in every component. 54 | // processCssUrls: true, // Process/optimize relative stylesheet url()'s. Set to false, if you don't want them touched. 55 | // purifyCss: false, // Remove unused CSS selectors. 56 | // uglify: {}, // Uglify-specific options. https://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin 57 | // postCss: [] // Post-CSS options: https://github.com/postcss/postcss/blob/master/docs/plugins.md 58 | // }); 59 | --------------------------------------------------------------------------------