├── .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 | 
4 |
5 | Translation management for your Laravel application.
6 |
7 | 
8 |
9 | 
10 | 
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 |
2 |
16 |
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 |
2 |
3 |
--------------------------------------------------------------------------------
/resources/views/forms/select.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
17 |
18 |
--------------------------------------------------------------------------------
/resources/views/forms/text.blade.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
12 |
13 |
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 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {{ __('translation::translation.language_name') }} |
30 | {{ __('translation::translation.locale') }} |
31 |
32 |
33 |
34 |
35 | @foreach($languages as $language => $name)
36 |
37 |
38 | {{ $name }}
39 | |
40 |
41 |
42 | {{ $language }}
43 |
44 | |
45 |
46 | @endforeach
47 |
48 |
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 |
12 |
13 |
53 |
54 |
55 |
56 | @endsection
--------------------------------------------------------------------------------
/resources/views/languages/translations/index.blade.php:
--------------------------------------------------------------------------------
1 | @extends('translation::layout')
2 |
3 | @section('body')
4 |
5 |
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 |
3 |
4 |
{{ Session::get('success') }}
5 |
6 |
7 | @endif
8 |
9 | @if(Session::has('error'))
10 |
11 |
12 |
{!! Session::get('error') !!}
13 |
14 |
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 |
--------------------------------------------------------------------------------