huge Laravel 10 upgrade

This commit is contained in:
HolgerHatGarKeineNode
2023-02-19 20:13:20 +01:00
parent 5c74f77beb
commit 12847f95f6
440 changed files with 46336 additions and 682 deletions

View File

@@ -0,0 +1,35 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,24 @@
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.0
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite
coverage: none
- name: Install Composer dependencies
run: composer install --prefer-dist --no-interaction
- name: Execute tests
run: vendor/bin/phpunit

View File

@@ -0,0 +1,3 @@
/vendor/
/node_modules
.phpunit.result.cache

View File

@@ -0,0 +1,4 @@
preset: laravel
finder:
exclude:
- "tests/fixtures"

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Joe Dixon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,44 @@
{
"name": "joedixon/laravel-translation",
"description": "A tool for managing all of your Laravel translations",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Joe Dixon",
"email": "hello@joedixon.co.uk"
}
],
"require": {
"php": "^8.0",
"illuminate/support": "^8.0||^9.0||^10.0",
"laravel/legacy-factories": "^1.3"
},
"require-dev": {
"orchestra/testbench": "^6.0",
"phpunit/phpunit": "^9.0",
"mockery/mockery": "^1.0.0"
},
"autoload": {
"psr-4": {
"JoeDixon\\Translation\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"JoeDixon\\Translation\\Tests\\": "tests"
}
},
"scripts": {
"test": "vendor/bin/phpunit",
"test-coverage": "vendor/bin/phpunit --coverage-html coverage"
},
"extra": {
"laravel": {
"providers": [
"JoeDixon\\Translation\\TranslationServiceProvider",
"JoeDixon\\Translation\\TranslationBindingsServiceProvider"
]
}
}
}

7808
support/laravel-translation/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,77 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Package driver
|--------------------------------------------------------------------------
|
| The package supports different drivers for translation management.
|
| Supported: "file", "database"
|
*/
'driver' => 'file',
/*
|--------------------------------------------------------------------------
| Route group configuration
|--------------------------------------------------------------------------
|
| The package ships with routes to handle language management. Update the
| configuration here to configure the routes with your preferred group options.
|
*/
'route_group_config' => [
'middleware' => 'web',
],
/*
|--------------------------------------------------------------------------
| Translation methods
|--------------------------------------------------------------------------
|
| Update this array to tell the package which methods it should look for
| when finding missing translations.
|
*/
'translation_methods' => ['trans', '__'],
/*
|--------------------------------------------------------------------------
| Scan paths
|--------------------------------------------------------------------------
|
| Update this array to tell the package which directories to scan when
| looking for missing translations.
|
*/
'scan_paths' => [app_path(), resource_path()],
/*
|--------------------------------------------------------------------------
| UI URL
|--------------------------------------------------------------------------
|
| Define the URL used to access the language management too.
|
*/
'ui_url' => 'languages',
/*
|--------------------------------------------------------------------------
| Database settings
|--------------------------------------------------------------------------
|
| Define the settings for the database driver here.
|
*/
'database' => [
'connection' => '',
'languages_table' => 'languages',
'translations_table' => 'translations',
],
];

View File

@@ -0,0 +1,15 @@
CONTRIBUTING
============
Contributions are welcome, and are accepted via pull requests. Please review these guidelines before submitting any pull requests.
## Guidelines
* Please follow the [PSR-2 Coding Style Guide](http://www.php-fig.org/psr/psr-2/), enforced by [StyleCI](https://styleci.io/).
* Ensure that the current tests pass, and if you've added something new, add the tests where relevant.
* Send a coherent commit history, making sure each individual commit in your pull request is meaningful.
* You may need to [rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) to avoid merge conflicts.
* If you are changing the behavior, or the public api, you may need to update the docs.
* Please remember that we follow [SemVer](http://semver.org/).
We have [StyleCI](https://styleci.io/) setup to automatically fix any code style issues.

View File

@@ -0,0 +1,11 @@
<?php
use Faker\Generator;
use JoeDixon\Translation\Language;
$factory->define(Language::class, function (Generator $faker) {
return [
'language' => $faker->word,
'name' => $faker->word,
];
});

View File

@@ -0,0 +1,38 @@
<?php
use Faker\Generator;
use JoeDixon\Translation\Language;
use JoeDixon\Translation\Translation;
$factory->define(Translation::class, function (Generator $faker) {
return [
'language_id' => function () {
return factory(Language::class)->create()->id;
},
'group' => $faker->word,
'key' => $faker->word,
'value' => $faker->sentence,
];
});
$factory->state(Translation::class, 'group', function (Generator $faker) {
return [
'language_id' => function () {
return factory(Language::class)->create()->id;
},
'group' => $faker->word,
'key' => $faker->word,
'value' => $faker->sentence,
];
});
$factory->state(Translation::class, 'single', function (Generator $faker) {
return [
'language_id' => function () {
return factory(Language::class)->create()->id;
},
'group' => 'single',
'key' => $faker->word,
'value' => $faker->sentence,
];
});

View File

@@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use JoeDixon\Translation\Language;
class CreateLanguagesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::connection(config('translation.database.connection'))
->create(config('translation.database.languages_table'), function (Blueprint $table) {
$table->increments('id');
$table->string('name')->nullable();
$table->string('language');
$table->timestamps();
});
$initialLanguages = array_unique([
config('app.fallback_locale'),
config('app.locale'),
]);
foreach ($initialLanguages as $language) {
Language::firstOrCreate([
'language' => $language,
]);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::connection(config('translation.database.connection'))
->dropIfExists(config('translation.database.languages_table'));
}
}

View File

@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTranslationsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::connection(config('translation.database.connection'))
->create(config('translation.database.translations_table'), function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('language_id');
$table->foreign('language_id')->references('id')
->on(config('translation.database.languages_table'));
$table->string('group')->nullable();
$table->text('key');
$table->text('value')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::connection(config('translation.database.connection'))
->dropIfExists(config('translation.database.translations_table'));
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -0,0 +1,4 @@
{
"/Users/joe/Code/translation/public/vendor/translation/js/app.js": "/Users/joe/Code/translation/public/vendor/translation/js/app.js",
"/../../../public/vendor/translation/css/main.css": "/../../../public/vendor/translation/css/main.css"
}

21940
support/laravel-translation/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
{
"scripts": {
"dev": "NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"hot": "NODE_ENV=development webpack-dev-server --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"production": "NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
},
"license": "MIT",
"devDependencies": {
"laravel-mix": "^4.1.2",
"postcss": "^7.0.36",
"tailwindcss": "^0.6.6",
"vue-template-compiler": "^2.6.10"
},
"dependencies": {
"axios": "^0.18.1",
"vue": "^2.5.21"
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" backupStaticAttributes="false" bootstrap="vendor/autoload.php" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./src</directory>
</include>
<exclude>
<directory>./src/Console</directory>
</exclude>
</coverage>
<testsuites>
<testsuite name="Feature">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<php>
<env name="APP_KEY" value="AckfSECXIvnK5r28GVIWUAxmbBSjTsmF"/>
</php>
</phpunit>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
{
"/js/app.js": "/js/app.js",
"/css/main.css": "/css/main.css"
}

View File

@@ -0,0 +1,201 @@
<div align="center">
![Laravel Translation](logo.png)
Translation management for your Laravel application.
![Laravel Translation UI](translation.png)
![GitHub](https://img.shields.io/github/checks-status/joedixon/laravel-translation/master?style=for-the-badge)
![GitHub](https://img.shields.io/github/license/joedixon/laravel-translation.svg?style=for-the-badge)
</div>
------
## About Laravel Translation
Laravel Translation is a package for Laravel which allows you full control
over your translations when using [Laravel's
localization](https://laravel.com/docs/5.7/localization) functionality.
The package allows you to manage your translations using either the native file
based translations, but also provides a database driver which is useful in
multi-server setups.
It exposes a user interface allowing you to update existing and add new
translations to your application.
Below are a full list of features:
- File and database drivers
- Database translation loader (automatically load translations from the database
when Laravel's translation retrieval methods and the database driver)
- User interface to add new languages and add and update translations
- Artisan commands to manage your translations
- Scan your application for missing translations
## Version Compatibility
| Laravel | Laravel Translation |
| ------------- | ------------------- |
| 6.x | 1.x |
| 7.x | 1.x |
| 8.x | 2.x |
| 9.x | 2.x |
## Installation
Install the package via Composer
`composer require joedixon/laravel-translation`
Publish configuration and assets
`php artisan vendor:publish --provider="JoeDixon\Translation\TranslationServiceProvider"`
The service provider is loaded automatically using [package discovery](https://laravel.com/docs/5.7/packages#package-discovery).
## Usage
### Configuration
The package ships with a configuration file called `translation.php` which is published to the
config directory during installation. Below is an outline of the settings.
```
driver [file|database]
```
Choose either `file` or `database`. File translations utilise Laravel's native
file based translations and includes support for both `array` based and `json` based
language files.
```
route_group_config.middleware [string|array]
```
Apply middleware to the routes which ship with the package. For example, you may
which to use the `auth` middleware to ensure package user interface is only
accessible to logged in users.
```
translation_methods [array]
```
Choose which of Laravel's translation methods to use when searching for missing
translation keys.
```
scan_paths [array]
```
Choose which paths to use when searching for missing translations. Narrowing the
search to specific directories will result in a performance increase when
scanning for missing translations.
```
ui_url [string]
```
Choose the root URL where the package user interface can be accessed. All routes
will be prefixed by this value.
e.g. setting this value to `languages` will result in URLs such as `translations/{language}/translations`
```
database.languages_table
```
Choose the name of the languages table when using the database driver.
```
database.translations_table
```
Choose the name of the translations table when using the database driver.
### Drivers
#### File
Utitlises Laravel's native php array and JSON based language files and exposes a
user interface to manage the enclosed translations. Add and update languages and translations
using either the user interface or the built-in [Artisan commands](https://laravel.com/docs/5.7/artisan).
#### Database
The database driver takes all of the functionality of Laravel's file based
language files, but moves the storage to the database, utilising the connection
configured for your Laravel application.
It also replaces the translation loader in the container so all of Laravel's
translation retrieval methods (`__()`, `trans()`, `@lang()`, etc) will load the
relevant strings from the database rather than the files without the need to
change any code in your application. It's a like for like swap.
To utilise the database driver, make sure to update the database table names in
the configuration file and run the migrations.
#### Changing Drivers from File (default) to Database
1. Update the driver to use database in `./config/translation.php`.
```php
'driver' => 'database'
```
2. Run the migration to add translations and languages tables.
```shell
php artisan migrate
```
3. Run the following command and folow the prompts to synchronise the translations between drivers.
```shell
php artisan translation:sync-translations
```
4. A few questions will be prompted which have to be answered. See the screenshot below:
### User interface
Navigate to http://your-project.test/languages (update `languages` to match the
`translation.ui_url` configuration setting) and use the interface to manage
your translations.
First, click on the language you wish to edit. On the subsequent page, find the
translation you want to edit and click on the pencil icon or on the text and
make your edits. As soon as you remove focus from the input, your translation
will be saved, indicated by the green check icon.
### Artisan Commands
The package ships with a series of Artisan commands which assist with
translation management.
```
translation:add-language
```
Add a new language to the application.
```
translation:add-translation-key
```
Add a new language key for the application.
```
translation:list-languages
```
List all of the available languages in the application.
```
translation:list-missing-translation-keys
```
List all of the translation keys in the app which don't have a corresponding translation.
```
translation:sync-translations
```
Synchronise translations between drivers. This is useful if you have an exisitng
application using the native file based language files and wish to move to the
database driver. Running this command will take all of the translations from the
language files and insert them in to the database.
```
translation:sync-missing-translation-keys
```
This command will scan your project (using the paths supplied in the
configuration file) and create all of the missing translation keys. This can be
run for all languages or a single language.

View File

@@ -0,0 +1,262 @@
/**
* This injects Tailwind's base styles, which is a combination of
* Normalize.css and some additional base styles.
*
* You can see the styles here:
* https://github.com/tailwindcss/tailwindcss/blob/master/css/preflight.css
*
* If using `postcss-import`, use this import instead:
*
* @import "tailwindcss/preflight";
*/
@tailwind preflight;
/**
* This injects any component classes registered by plugins.
*
* If using `postcss-import`, use this import instead:
*
* @import "tailwindcss/components";
*/
@tailwind components;
/**
* Here you would add any of your custom component classes; stuff that you'd
* want loaded *before* the utilities so that the utilities could still
* override them.
*
* Example:
*
* .btn { ... }
* .form-input { ... }
*
* Or if using a preprocessor or `postcss-import`:
*
* @import "components/buttons";
* @import "components/forms";
*/
/**
* This injects all of Tailwind's utility classes, generated based on your
* config file.
*
* If using `postcss-import`, use this import instead:
*
* @import "tailwindcss/utilities";
*/
@tailwind utilities;
/**
* Here you would add any custom utilities you need that don't come out of the
* box with Tailwind.
*
* Example :
*
* .bg-pattern-graph-paper { ... }
* .skew-45 { ... }
*
* Or if using a preprocessor or `postcss-import`:
*
* @import "utilities/background-patterns";
* @import "utilities/skew-transforms";
*/
body {
@apply bg-grey-lighter text-grey-darkest
}
ul {
list-style-type: none;
@apply flex;
}
li {
@apply px-4;
}
a {
@apply text-blue;
}
nav.header {
background: linear-gradient(90deg, #125b93, #2891c4);
@apply border-b flex items-center h-16 text-white w-full
}
nav a {
@apply .opacity-75 text-white no-underline flex items-center
}
nav a.active {
@apply opacity-100
}
nav a:hover {
@apply opacity-100 underline
}
.panel {
@apply bg-white rounded m-6 shadow text-grey-dark
}
.panel-header {
@apply p-4 text-lg border-b flex items-center font-thin
}
.panel-footer {
@apply border-t bg-grey-lighter p-4
}
.panel-body table {
@apply w-full table-fixed;
}
.panel-body th,
.panel-body td {
@apply text-left p-4 overflow-x-auto
}
.panel-body th {
@apply text-grey-darker
}
.panel-body td {
@apply font-thin align-top
}
.panel-body tr {
@apply border-b
}
.panel-body thead tr {
@apply bg-grey-lighter
}
.panel-body tbody tr:nth-child(even) {
@apply bg-grey-lighter
}
.panel-body tbody tr:hover,
.panel-body tbody tr:nth-child(even):hover {
@apply bg-blue-lightest
}
.panel-body tbody tr:last-child {
@apply border-none
}
.panel-body td textarea {
overflow-wrap: inherit;
@apply border-none resize-none bg-transparent text-grey-darker w-full font-thin h-auto p-0
}
.panel-body td textarea.active {
@apply w-full rounded h-32 p-2 border border-solid border-grey
}
.panel-body td textarea:focus {
@apply outline-none;
}
.button {
@apply bg-transparent text-grey-darker py-2 px-4 border border-grey rounded text-sm font-bold no-underline
}
.button:hover {
@apply text-blue
}
.button-blue {
@apply bg-blue text-white border-blue
}
.button-blue:hover {
@apply text-white bg-blue-dark
}
.input-group {
@apply w-full mb-6
}
.input-group label {
@apply block uppercase tracking-wide text-grey-darker text-xs font-bold mb-2
}
.input-group input {
@apply appearance-none block w-full bg-grey-lighter text-grey-darker border rounded py-3 px-4 mb-3 leading-tight
}
.input-group:last-child {
@apply mb-0
}
.input-group input.error {
@apply border-red
}
.input-group .error-text {
@apply text-red text-xs italic
}
.select-group {
@apply relative mr-2
}
.select-group:last-child {
@apply m-0
}
.select-group select {
@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
}
.select-group select:focus {
@apply outline-none border-grey
}
.select-group .caret {
@apply pointer-events-none absolute pin-y pin-r flex items-center px-2 text-grey-darker
}
.select-group .caret svg {
@apply fill-current h-4 w-4
}
.w-1\/10 {
width: 10%;
}
.search-input {
background: url('data:image/svg+xml;charset=utf8,<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com"><path d="M 11.732 12.439 C 9.047 14.525 5.109 12.921 4.645 9.553 C 4.181 6.183 7.538 3.575 10.689 4.858 C 13.399 5.962 14.306 9.349 12.512 11.66 L 15.466 14.603 L 14.682 15.388 L 11.738 12.439 Z M 9.027 12.263 C 11.577 12.263 13.172 9.5 11.897 7.291 C 11.304 6.267 10.21 5.634 9.027 5.634 C 6.476 5.634 4.88 8.397 6.157 10.606 C 6.749 11.63 7.842 12.263 9.027 12.263 Z" style="fill: rgb(96, 111, 123);"/></svg>');
@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
}
.search-input:focus {
@apply outline-none bg-white border border-grey-light
}
.transition {
transition: all .1s ease-in;
}
.search {
max-width: 500px;
@apply mx-2 relative flex-1
}
ul.search-results {
max-height: 300px;
@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
}
ul.search-results li {
@apply px-4 py-2 border-b pl-10;
}
ul.search-results li:last-child {
@apply border-b-0;
}
.search.has-results .search-input {
@apply border-b-0 rounded-b-none bg-white
}

View File

@@ -0,0 +1,63 @@
/**
* First we will load all of this project's JavaScript dependencies which
* includes Vue and other libraries. It is a great starting point when
* building robust, powerful web applications using Vue and Laravel.
*/
// require('./bootstrap');
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Next we will register the CSRF Token as a common header with Axios so that
* all outgoing HTTP requests automatically have it attached. This is just
* a simple convenience so we don't have to attach every token manually.
*/
let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}
window.Vue = require('vue');
/**
* Next, we will create a fresh Vue application instance and attach it to
* the page. Then, you may begin adding components to this application
* or customize the JavaScript scaffolding to fit your unique needs.
*/
Vue.component('translation-input', require('./components/TranslationInput.vue').default);
const app = new Vue({
el: '#app',
data: function () {
return {
showAdvancedOptions: false,
}
},
methods: {
submit: function(event) {
event.target.form.submit();
},
toggleAdvancedOptions(event) {
event.preventDefault();
this.showAdvancedOptions = !this.showAdvancedOptions;
}
}
});

View File

@@ -0,0 +1,56 @@
window._ = require('lodash');
window.Popper = require('popper.js').default;
/**
* We'll load jQuery and the Bootstrap jQuery plugin which provides support
* for JavaScript based Bootstrap features such as modals and tabs. This
* code may be modified to fit the specific needs of your application.
*/
try {
window.$ = window.jQuery = require('jquery');
require('bootstrap');
} catch (e) {}
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Next we will register the CSRF Token as a common header with Axios so that
* all outgoing HTTP requests automatically have it attached. This is just
* a simple convenience so we don't have to attach every token manually.
*/
let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo'
// window.Pusher = require('pusher-js');
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: process.env.MIX_PUSHER_APP_KEY,
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
// encrypted: true
// });

View File

@@ -0,0 +1,78 @@
<template>
<div class="flex">
<svg v-show="!isActive && isLoading" v-on:click="setActive" class="cursor-pointer fill-current w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M0 2C0 .9.9 0 2 0h14l4 4v14a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm5 0v6h10V2H5zm6 1h3v4h-3V3z"/></svg>
<svg v-show="!isActive && hasSaved" v-on:click="setActive" class="cursor-pointer fill-current text-green w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M2.93 17.07A10 10 0 1 1 17.07 2.93 10 10 0 0 1 2.93 17.07zm12.73-1.41A8 8 0 1 0 4.34 4.34a8 8 0 0 0 11.32 11.32zM6.7 9.29L9 11.6l4.3-4.3 1.4 1.42L9 14.4l-3.7-3.7 1.4-1.42z"/></svg>
<svg v-show="!isActive && hasErrored" v-on:click="setActive" class="cursor-pointer fill-current text-red w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M2.93 17.07A10 10 0 1 1 17.07 2.93 10 10 0 0 1 2.93 17.07zm1.41-1.41A8 8 0 1 0 15.66 4.34 8 8 0 0 0 4.34 15.66zm9.9-8.49L11.41 10l2.83 2.83-1.41 1.41L10 11.41l-2.83 2.83-1.41-1.41L8.59 10 5.76 7.17l1.41-1.41L10 8.59l2.83-2.83 1.41 1.41z"/></svg>
<svg v-show="!isActive && !hasSaved && !hasErrored && !isLoading" v-on:click="setActive" class="cursor-pointer fill-current w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M12.3 3.7l4 4L4 20H0v-4L12.3 3.7zm1.4-1.4L16 0l4 4-2.3 2.3-4-4z"/></svg>
<textarea
rows="1"
v-model="translation"
v-bind:class="{ active: isActive }"
v-on:focus="setActive"
v-on:blur="storeTranslation"
ref="trans"
></textarea>
</div>
</template>
<script>
export default {
props: ['initialTranslation', 'language', 'group', 'translationKey', 'route'],
data: function() {
return {
isActive: false,
hasSaved: false,
hasErrored: false,
isLoading: false,
hasChanged: false,
translation: this.initialTranslation
}
},
methods: {
setActive: function() {
this.isActive = true;
this.$refs.trans.focus();
},
storeTranslation: function() {
this.isActive = false;
if(!this.hasChanged) {
return;
}
this.isLoading = true;
window.axios.post(`/${this.route}/${this.language}`, {
language: this.language,
group: this.group,
key: this.translationKey,
value: this.translation
}).then((response) => {
this.hasSaved = true;
this.isLoading = false;
this.hasChanged = false;
})
.catch((error) => {
this.hasErrored = true;
this.isLoading = false;
})
}
},
watch: {
hasSaved: function(hasSaved) {
if(hasSaved) {
setTimeout(() => { this.hasSaved = false }, 3000)
}
},
hasErrored: function(hasErrored) {
if(hasErrored) {
setTimeout(() => { this.hasErrored = false }, 3000)
}
},
translation: function(translation)
{
this.hasChanged = true;
}
}
}
</script>

View File

@@ -0,0 +1,113 @@
<?php
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
if (! function_exists('set_active')) {
/**
* Determine if a route is the currently active route.
*
* @param string $path
* @param string $class
* @return string
*/
function set_active($path, $class = 'active')
{
return Request::is(config('translation.ui_url').$path) ? $class : '';
}
}
if (! function_exists('strs_contain')) {
/**
* Determine whether any of the provided strings in the haystack contain the needle.
*
* @param array $haystacks
* @param string $needle
* @return bool
*/
function strs_contain($haystacks, $needle)
{
$haystacks = (array) $haystacks;
foreach ($haystacks as $haystack) {
if (is_array($haystack)) {
return strs_contain($haystack, $needle);
} elseif (Str::contains(strtolower($haystack), strtolower($needle))) {
return true;
}
}
return false;
}
}
if (! function_exists('array_diff_assoc_recursive')) {
/**
* Recursively diff two arrays.
*
* @param array $arrayOne
* @param array $arrayTwo
* @return array
*/
function array_diff_assoc_recursive($arrayOne, $arrayTwo)
{
$difference = [];
foreach ($arrayOne as $key => $value) {
if (is_array($value) || $value instanceof Illuminate\Support\Collection) {
if (! isset($arrayTwo[$key])) {
$difference[$key] = $value;
} elseif (! (is_array($arrayTwo[$key]) || $arrayTwo[$key] instanceof Illuminate\Support\Collection)) {
$difference[$key] = $value;
} else {
$new_diff = array_diff_assoc_recursive($value, $arrayTwo[$key]);
if ($new_diff != false) {
$difference[$key] = $new_diff;
}
}
} elseif (! isset($arrayTwo[$key])) {
$difference[$key] = $value;
}
}
return $difference;
}
}
if (! function_exists('str_before')) {
/**
* Get the portion of a string before a given value.
*
* @param string $subject
* @param string $search
* @return string
*/
function str_before($subject, $search)
{
return $search === '' ? $subject : explode($search, $subject)[0];
}
}
// Array undot
if (! function_exists('array_undot')) {
/**
* Expands a single level array with dot notation into a multi-dimensional array.
*
* @param array $dotNotationArray
* @return array
*/
function array_undot(array $dotNotationArray)
{
$array = [];
foreach ($dotNotationArray as $key => $value) {
// if there is a space after the dot, this could legitimately be
// a single key and not nested.
if (count(explode('. ', $key)) > 1) {
$array[$key] = $value;
} else {
Arr::set($array, $key, $value);
}
}
return $array;
}
}

View File

@@ -0,0 +1,6 @@
<?php
return [
'language_exists' => 'Die Sprache { :language } ist bereits vorhanden',
'key_exists' => 'Der Übersetzungsschlüssel { :key } ist bereits vorhanden',
];

View File

@@ -0,0 +1,47 @@
<?php
return [
'languages' => 'Sprachen',
'language' => 'Sprache',
'type' => 'Typ',
'file' => 'Datei',
'key' => 'Schlüssel',
'prompt_language' => 'Geben Sie den Sprachcode ein, den Sie hinzufügen möchten (z.B. en).',
'language_added' => 'Neue Sprache wurde erfolgreich hinzugefügt 🙌',
'prompt_language_for_key' => 'Geben Sie die Sprache für den Schlüssel ein (z.B. en)',
'prompt_type' => 'Ist das ein Json- oder Array-Schlüssel?',
'prompt_file' => 'In welcher Datei wird das gespeichert?',
'prompt_key' => 'Was ist der Schlüssel für diese Übersetzung?',
'prompt_value' => 'Was ist der Wert für diese Übersetzung',
'type_error' => 'Übersetzungstyp muss json oder ein Array sein',
'language_key_added' => 'Neuer Sprachenschlüssel wurde erfolgreich hinzugefügt 👏',
'no_missing_keys' => 'Es fehlen keine Übersetzungsschlüssel in der App 🎉',
'keys_synced' => 'Fehlende Schlüssel erfolgreich synchronisiert 🎊',
'search' => 'Alle Übersetzungen suchen',
'translations' => 'Übersetzung',
'language_name' => 'Name',
'locale' => 'locale',
'add' => '+ Hinzufügen',
'add_language' => 'Neue Sprache hinzufügen',
'save' => 'save',
'language_exists' => 'Das :attribute ist bereits vorhanden.',
'uh_oh' => 'Etwas ist nicht ganz richtig',
'group_single' => 'Gruppe / Single',
'Gruppe' => 'Gruppe',
'single' => 'single',
'value' => 'Wert',
'namespace' => 'Namespace',
'synchronisieren' => 'Übersetzungen synchronisieren ⏳',
'synced' => 'Übersetzungen wurden synchronisiert 😎',
'add_translation' => 'Übersetzung hinzufügen',
'translation_added' => 'Neue Übersetzung erfolgreich hinzugefügt 🙌',
'namespace_label' => 'Namespace (optional)',
'group_label' => 'Gruppe (optional)',
'key_label' => 'Schlüssel',
'value_label' => 'Wert',
'namespace_placeholder' => 'z.B. my_package',
'group_placeholder' => 'z.B. validation',
'key_placeholder' => 'z.B. invalid_key',
'value_placeholder' => 'z.B. Schlüssel müssen eine einzige Zeichenfolge sein',
'advanced_options' => 'Erweiterte Optionen umschalten',
];

View File

@@ -0,0 +1,6 @@
<?php
return [
'language_exists' => 'The language { :language } already exists',
'key_exists' => 'The translation key { :key } already exists',
];

View File

@@ -0,0 +1,45 @@
<?php
return [
'languages' => 'Languages',
'language' => 'Language',
'type' => 'Type',
'file' => 'File',
'key' => 'Key',
'prompt_language' => 'Enter the language code you would like to add (e.g. en)',
'language_added' => 'New language added successfully 🙌',
'prompt_language_for_key' => 'Enter the language for the key (e.g. en)',
'prompt_type' => 'Is this a json or array key?',
'prompt_file' => 'Which file will this be stored in?',
'prompt_key' => 'What is the key for this translation?',
'prompt_value' => 'What is the value for this translation',
'type_error' => 'Translation type must be json or array',
'language_key_added' => 'New language key added successfully 👏',
'no_missing_keys' => 'There are no missing translation keys in the app 🎉',
'keys_synced' => 'Missing keys synchronised successfully 🎊',
'search' => 'Search all translations',
'translations' => 'Translation',
'language_name' => 'Name',
'locale' => 'Locale',
'add' => '+ Add',
'add_language' => 'Add a new language',
'save' => 'Save',
'language_exists' => 'The :attribute already exists.',
'uh_oh' => 'Something\'s not quite right',
'group_single' => 'Group / Single',
'group' => 'Group',
'single' => 'Single',
'value' => 'Value',
'namespace' => 'Namespace',
'add_translation' => 'Add a translation',
'translation_added' => 'New translation added successfull 🙌',
'namespace_label' => 'Namespace (Optional)',
'group_label' => 'Group (Optional)',
'key_label' => 'Key',
'value_label' => 'Value',
'namespace_placeholder' => 'e.g. my_package',
'group_placeholder' => 'e.g. validation',
'key_placeholder' => 'e.g. invalid_key',
'value_placeholder' => 'e.g. Keys must be a single string',
'advanced_options' => 'Toggle advanced options',
];

View File

@@ -0,0 +1,6 @@
<?php
return [
'key_exists' => 'La clé de traduction { :key } existe déjà',
'language_exists' => 'La langue { :language } existe déjà',
];

View File

@@ -0,0 +1,45 @@
<?php
return [
'add' => '+ Ajouter',
'add_language' => 'Ajouter une nouvelle langue',
'add_translation' => 'Ajouter une traduction',
'advanced_options' => 'Afficher les options avancées',
'file' => 'Fichier',
'group' => 'Groupe',
'group_label' => 'Groupe (Optionnel)',
'group_placeholder' => 'Ex: validation',
'group_single' => 'Groupe / Unique',
'key' => 'Clé',
'key_label' => 'Clé',
'key_placeholder' => 'Par exemple : invalid_key',
'keys_synced' => 'Clés manquantes synchronisées avec succès 🎊',
'language' => 'Langue',
'language_added' => 'Nouvelle langue ajoutée avec succés 🙌',
'language_exists' => 'Le :attribute existe déjà.',
'language_key_added' => 'Nouvelle clé dans la langue ajoutée avec succès 👏',
'language_name' => 'Nom',
'languages' => 'Langues',
'locale' => 'Locale',
'namespace' => 'Namespace',
'namespace_label' => 'Namespace (Optionnel)',
'namespace_placeholder' => 'Par exemple : my_package',
'no_missing_keys' => 'Il ne manque aucune clé de traduction dans l\'application 🎉',
'prompt_file' => 'Dans quel fichier sera t\'elle stockée ?',
'prompt_key' => 'Quelle est la clé de cette traduction ?',
'prompt_language' => 'Entrez le code langue que vous aimeriez ajouter (Ex: fr)',
'prompt_language_for_key' => 'Entrez la langue pour la clé (Ex: fr)',
'prompt_type' => 'Est-ce une clé Json ou Array ?',
'prompt_value' => 'Quelle est la valeur de la traduction',
'save' => 'Sauvegarder',
'search' => 'Rechercher toutes les traductions',
'single' => 'Unique',
'translation_added' => 'Nouvelle traduction ajoutée avec succès 🙌',
'translations' => 'Traduction',
'type' => 'Type',
'type_error' => 'Le type de traduction doit être en json ou en array',
'uh_oh' => 'Quelque chose ne fonctionne pas',
'value' => 'Valeur',
'value_label' => 'Valeur',
'value_placeholder' => 'Par exemple : Les clés doivent être une seule chaîne',
];

View File

@@ -0,0 +1,6 @@
<?php
return [
'language_exists' => 'De taal { :language } bestaat al',
'key_exists' => 'De vertaalsleutel { :key } bestaat al',
];

View File

@@ -0,0 +1,45 @@
<?php
return [
'languages' => 'Talen',
'language' => 'Taal',
'type' => 'Type',
'file' => 'Bestand',
'key' => 'Sleutel',
'prompt_language' => 'Voer de taalcode in die u wilt toevoegen (bijvoorbeeld: en)',
'language_added' => 'Nieuwe taal met succes toegevoegd 🙌',
'prompt_language_for_key' => 'Voer de taal voor de sleutel in (bijvoorbeeld: en)',
'prompt_type' => 'Is dit een json- of arraysleutel?',
'prompt_file' => 'In welk bestand wordt dit opgeslagen?',
'prompt_key' => 'Wat is de sleutel voor deze vertaling?',
'prompt_value' => 'Wat is de text voor deze vertaling',
'type_error' => 'Het vertaaltype moet json of array zijn',
'language_key_added' => 'Nieuwe taalcode toegevoegd 👏',
'no_missing_keys' => 'Er zijn geen ontbrekende vertaalsleutels in de app 🎉',
'keys_synced' => 'Ontbrekende toetsen gesynchroniseerd met succes 🎊',
'search' => 'Doorzoek alle vertalingen',
'translations' => 'Vertaling',
'language_name' => 'Naam',
'locale' => 'locale',
'add' => '+ Toevoegen',
'add_language' => 'Voeg een nieuwe taal toe',
'save' => 'Opslaan',
'language_exists' => 'Het kenmerk :attribute bestaat al.',
'uh_oh' => 'Er klopt iets niet helemaal',
'group_single' => 'Groep / Enkelvoudig',
'group' => 'Groep',
'single' => 'Enkelvoudig',
'value' => 'Waarde',
'namespace' => 'Namespace',
'add_translation' => 'Voeg een vertaling toe',
'translation_added' => 'Nieuwe vertaling succesvol toegevoegd 🙌',
'namespace_label' => 'Namespace (optioneel)',
'group_label' => 'Groep (optioneel)',
'key_label' => 'Sleutel',
'value_label' => 'Waarde',
'namespace_placeholder' => 'bijv. Mijn_pakket',
'group_placeholder' => 'bijv. bevestiging',
'key_placeholder' => 'bijv. ongeldige sleutel',
'value_placeholder' => 'bijv. Sleutels mogen geen spaties bevatten',
'advanced_options' => 'Schakel geavanceerde opties in',
];

View File

@@ -0,0 +1,3 @@
<div class="search">
<input type="text" class="search-input" placeholder="{{ __('translation::translation.search') }}" name="{{ $name }}" value="{{ $value }}">
</div>

View File

@@ -0,0 +1,18 @@
<div class="select-group">
<select name="{{ $name }}" @if(isset($submit) && $submit) v-on:change="submit" @endif>
@if(isset($optional) && $optional)<option value> ----- </option>@endif
@foreach($items as $key => $value)
@if(is_numeric($key))
<option value="{{ $value }}" @if(isset($selected) && $selected === $value) selected="selected" @endif>{{ $value }}</option>
@else
<option value="{{ $key }}" @if(isset($selected) && $selected === $key) selected="selected" @endif>{{ $value }}</option>
@endif
@endforeach
</select>
<div class="caret">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"/></svg>
</div>
</div>

View File

@@ -0,0 +1,18 @@
<div class="input-group">
<label for="{{ $field }}">
{{ $label }}
</label>
<input
class="@if($errors->has($field)) error @endif"
name="{{ $field }}"
id="{{ $field }}"
type="text"
placeholder="{{ isset($placeholder) ? $placeholder : '' }}"
value="{{ old($field) }}"
{{ isset($required) ? 'required' : '' }}>
@if($errors->has($field))
@foreach($errors->get($field) as $error)
<p class="error-text">{!! $error !!}</p>
@endforeach
@endif
</div>

View File

@@ -0,0 +1 @@
<svg class="{{ isset($class) ? $class : 'fill-current w-4 h-4 mr-2' }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M10 20a10 10 0 1 1 0-20 10 10 0 0 1 0 20zm2-2.25a8 8 0 0 0 4-2.46V9a2 2 0 0 1-2-2V3.07a7.95 7.95 0 0 0-3-1V3a2 2 0 0 1-2 2v1a2 2 0 0 1-2 2v2h3a2 2 0 0 1 2 2v5.75zm-4 0V15a2 2 0 0 1-2-2v-1h-.5A1.5 1.5 0 0 1 4 10.5V8H2.25A8.01 8.01 0 0 0 8 17.75z"/></svg>

After

Width:  |  Height:  |  Size: 389 B

View File

@@ -0,0 +1 @@
<svg class="{{ isset($class) ? $class : 'fill-current w-4 h-4 mr-2' }}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M7.41 9l2.24 2.24-.83 2L6 10.4l-3.3 3.3-1.4-1.42L4.58 9l-.88-.88c-.53-.53-1-1.3-1.3-2.12h2.2c.15.28.33.53.51.7l.89.9.88-.88C7.48 6.1 8 4.84 8 4H0V2h5V0h2v2h5v2h-2c0 1.37-.74 3.15-1.7 4.12L7.4 9zm3.84 8L10 20H8l5-12h2l5 12h-2l-1.25-3h-5.5zm.83-2h3.84L14 10.4 12.08 15z"/></svg>

After

Width:  |  Height:  |  Size: 412 B

View File

@@ -0,0 +1,41 @@
@extends('translation::layout')
@section('body')
<div class="panel w-1/2">
<div class="panel-header">
{{ __('translation::translation.add_language') }}
</div>
<form action="{{ route('languages.store') }}" method="POST">
<fieldset>
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<div class="panel-body p-4">
@include('translation::forms.text', ['field' => 'name', 'label' => __('translation::translation.language_name'), ])
@include('translation::forms.text', ['field' => 'locale', 'label' => __('translation::translation.locale'), ])
</div>
</fieldset>
<div class="panel-footer flex flex-row-reverse">
<button class="button button-blue">
{{ __('translation::translation.save') }}
</button>
</div>
</form>
</div>
@endsection

View File

@@ -0,0 +1,56 @@
@extends('translation::layout')
@section('body')
@if(count($languages))
<div class="panel w-1/2">
<div class="panel-header">
{{ __('translation::translation.languages') }}
<div class="flex flex-grow justify-end items-center">
<a href="{{ route('languages.create') }}" class="button">
{{ __('translation::translation.add') }}
</a>
</div>
</div>
<div class="panel-body">
<table>
<thead>
<tr>
<th>{{ __('translation::translation.language_name') }}</th>
<th>{{ __('translation::translation.locale') }}</th>
</tr>
</thead>
<tbody>
@foreach($languages as $language => $name)
<tr>
<td>
{{ $name }}
</td>
<td>
<a href="{{ route('languages.translations.index', $language) }}">
{{ $language }}
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endif
@endsection

View File

@@ -0,0 +1,56 @@
@extends('translation::layout')
@section('body')
<div class="panel w-1/2">
<div class="panel-header">
{{ __('translation::translation.add_translation') }}
</div>
<form action="{{ route('languages.translations.store', $language) }}" method="POST">
<fieldset>
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<div class="panel-body p-4">
@include('translation::forms.text', ['field' => 'group', 'label' => __('translation::translation.group_label'), 'placeholder' => __('translation::translation.group_placeholder')])
@include('translation::forms.text', ['field' => 'key', 'label' => __('translation::translation.key_label'), 'placeholder' => __('translation::translation.key_placeholder')])
@include('translation::forms.text', ['field' => 'value', 'label' => __('translation::translation.value_label'), 'placeholder' => __('translation::translation.value_placeholder')])
<div class="input-group">
<button v-on:click="toggleAdvancedOptions" class="text-blue">{{ __('translation::translation.advanced_options') }}</button>
</div>
<div v-show="showAdvancedOptions">
@include('translation::forms.text', ['field' => 'namespace', 'label' => __('translation::translation.namespace_label'), 'placeholder' => __('translation::translation.namespace_placeholder')])
</div>
</div>
</fieldset>
<div class="panel-footer flex flex-row-reverse">
<button class="button button-blue">
{{ __('translation::translation.save') }}
</button>
</div>
</form>
</div>
@endsection

View File

@@ -0,0 +1,89 @@
@extends('translation::layout')
@section('body')
<form action="{{ route('languages.translations.index', ['language' => $language]) }}" method="get">
<div class="panel">
<div class="panel-header">
{{ __('translation::translation.translations') }}
<div class="flex flex-grow justify-end items-center">
@include('translation::forms.search', ['name' => 'filter', 'value' => Request::get('filter')])
@include('translation::forms.select', ['name' => 'language', 'items' => $languages, 'submit' => true, 'selected' => $language])
<div class="sm:hidden lg:flex items-center">
@include('translation::forms.select', ['name' => 'group', 'items' => $groups, 'submit' => true, 'selected' => Request::get('group'), 'optional' => true])
<a href="{{ route('languages.translations.create', $language) }}" class="button">
{{ __('translation::translation.add') }}
</a>
</div>
</div>
</div>
<div class="panel-body">
@if(count($translations))
<table>
<thead>
<tr>
<th class="w-1/5 uppercase font-thin">{{ __('translation::translation.group_single') }}</th>
<th class="w-1/5 uppercase font-thin">{{ __('translation::translation.key') }}</th>
<th class="uppercase font-thin">{{ config('app.locale') }}</th>
<th class="uppercase font-thin">{{ $language }}</th>
</tr>
</thead>
<tbody>
@foreach($translations as $type => $items)
@foreach($items as $group => $translations)
@foreach($translations as $key => $value)
@if(!is_array($value[config('app.locale')]))
<tr>
<td>{{ $group }}</td>
<td>{{ $key }}</td>
<td>{{ $value[config('app.locale')] }}</td>
<td>
<translation-input
initial-translation="{{ $value[$language] }}"
language="{{ $language }}"
group="{{ $group }}"
translation-key="{{ $key }}"
route="{{ config('translation.ui_url') }}">
</translation-input>
</td>
</tr>
@endif
@endforeach
@endforeach
@endforeach
</tbody>
</table>
@endif
</div>
</div>
</form>
@endsection

View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name') }}</title>
<link rel="stylesheet" href="{{ asset('/vendor/translation/css/main.css') }}">
</head>
<body>
<div id="app">
@include('translation::nav')
@include('translation::notifications')
@yield('body')
</div>
<script src="{{ asset('/vendor/translation/js/app.js') }}"></script>
</body>
</html>

View File

@@ -0,0 +1,20 @@
<nav class="header">
<h1 class="text-lg px-6">{{ config('app.name') }}</h1>
<ul class="flex-grow justify-end pr-2">
<li>
<a href="{{ route('languages.index') }}" class="{{ set_active('') }}{{ set_active('/create') }}">
@include('translation::icons.globe')
{{ __('translation::translation.languages') }}
</a>
</li>
<li>
<a href="{{ route('languages.translations.index', config('app.locale')) }}" class="{{ set_active('*/translations') }}">
@include('translation::icons.translate')
{{ __('translation::translation.translations') }}
</a>
</li>
</ul>
</nav>

View File

@@ -0,0 +1,15 @@
@if(Session::has('success'))
<div class="bg-green-lightest text-green-darker p-6 shadow-md" role="alert">
<div class="flex justify-center">
<p>{{ Session::get('success') }}</p>
</div>
</div>
@endif
@if(Session::has('error'))
<div class="bg-red-lightest text-red-darker p-6 shadow-md" role="alert">
<div class="flex justify-center">
<p>{!! Session::get('error') !!}</p>
</div>
</div>
@endif

View File

@@ -0,0 +1,24 @@
<?php
Route::group(config('translation.route_group_config') + ['namespace' => 'JoeDixon\\Translation\\Http\\Controllers'], function ($router) {
$router->get(config('translation.ui_url'), 'LanguageController@index')
->name('languages.index');
$router->get(config('translation.ui_url').'/create', 'LanguageController@create')
->name('languages.create');
$router->post(config('translation.ui_url'), 'LanguageController@store')
->name('languages.store');
$router->get(config('translation.ui_url').'/{language}/translations', 'LanguageTranslationController@index')
->name('languages.translations.index');
$router->post(config('translation.ui_url').'/{language}', 'LanguageTranslationController@update')
->name('languages.translations.update');
$router->get(config('translation.ui_url').'/{language}/translations/create', 'LanguageTranslationController@create')
->name('languages.translations.create');
$router->post(config('translation.ui_url').'/{language}/translations', 'LanguageTranslationController@store')
->name('languages.translations.store');
});

View File

@@ -0,0 +1,40 @@
<?php
namespace JoeDixon\Translation\Console\Commands;
class AddLanguageCommand extends BaseCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'translation:add-language';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Add a new language to the application';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// ask the user for the language they wish to add
$language = $this->ask(__('translation::translation.prompt_language'));
$name = $this->ask(__('translation::translation.prompt_name'));
// attempt to add the key and fail gracefully if exception thrown
try {
$this->translation->addLanguage($language, $name);
$this->info(__('translation::translation.language_added'));
} catch (\Exception $e) {
$this->error($e->getMessage());
}
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace JoeDixon\Translation\Console\Commands;
class AddTranslationKeyCommand extends BaseCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'translation:add-translation-key';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Add a new language key for the application';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$language = $this->ask(__('translation::translation.prompt_language_for_key'));
// we know this should be single or group so we can use the `anticipate`
// method to give our users a helping hand
$type = $this->anticipate(__('translation::translation.prompt_type'), ['single', 'group']);
// if the group type is selected, prompt for the group key
if ($type === 'group') {
$file = $this->ask(__('translation::translation.prompt_group'));
}
$key = $this->ask(__('translation::translation.prompt_key'));
$value = $this->ask(__('translation::translation.prompt_value'));
// attempt to add the key for single or group and fail gracefully if
// exception is thrown
if ($type === 'single') {
try {
$this->translation->addSingleTranslation($language, 'single', $key, $value);
return $this->info(__('translation::translation.language_key_added'));
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
} elseif ($type === 'group') {
try {
$file = str_replace('.php', '', $file);
$this->translation->addGroupTranslation($language, $file, $key, $value);
return $this->info(__('translation::translation.language_key_added'));
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
} else {
return $this->error(__('translation::translation.type_error'));
}
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace JoeDixon\Translation\Console\Commands;
use Illuminate\Console\Command;
use JoeDixon\Translation\Drivers\Translation;
class BaseCommand extends Command
{
protected $translation;
public function __construct(Translation $translation)
{
parent::__construct();
$this->translation = $translation;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace JoeDixon\Translation\Console\Commands;
class ListLanguagesCommand extends BaseCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'translation:list-languages';
/**
* The console command description.
*
* @var string
*/
protected $description = 'List all of the available languages in the application';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$headers = [__('translation::translation.language_name'), __('translation::translation.language')];
$languages = $this->translation->allLanguages()->toArray();
$mappedLanguages = [];
foreach ($languages as $language => $name) {
$mappedLanguages[] = [$name, $language];
}
// return a table of results
$this->table($headers, $mappedLanguages);
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace JoeDixon\Translation\Console\Commands;
class ListMissingTranslationKeys extends BaseCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'translation:list-missing-translation-keys';
/**
* The console command description.
*
* @var string
*/
protected $description = 'List all of the translation keys in the app which don\'t have a corresponding translation';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$missingTranslations = [];
$rows = [];
foreach ($this->translation->allLanguages() as $language => $name) {
$missingTranslations[$language] = $this->translation->findMissingTranslations($language);
}
// check whether or not there are any missing translations
$empty = true;
foreach ($missingTranslations as $language => $values) {
if (! empty($values)) {
$empty = false;
}
}
// if no missing translations, inform the user and move on with your day
if ($empty) {
return $this->info(__('translation::translation.no_missing_keys'));
}
// set some headers for the table of results
$headers = [__('translation::translation.language'), __('translation::translation.type'), __('translation::translation.group'), __('translation::translation.key')];
// iterate over each of the missing languages
foreach ($missingTranslations as $language => $types) {
// iterate over each of the file types (json or array)
foreach ($types as $type => $keys) {
// iterate over each of the keys
foreach ($keys as $key => $value) {
// populate the array with the relevant data to fill the table
foreach ($value as $k => $v) {
$rows[] = [$language, $type, $key, $k];
}
}
}
}
// render the table of results
$this->table($headers, $rows);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace JoeDixon\Translation\Console\Commands;
class SynchroniseMissingTranslationKeys extends BaseCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'translation:sync-missing-translation-keys {language?}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Add all of the missing translation keys for all languages or a single language';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$language = $this->argument('language') ?: false;
try {
// if we have a language, pass it in, if not the method will
// automagically sync all languages
$this->translation->saveMissingTranslations($language);
return $this->info(__('translation::translation.keys_synced'));
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
}

View File

@@ -0,0 +1,194 @@
<?php
namespace JoeDixon\Translation\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use JoeDixon\Translation\Drivers\Database;
use JoeDixon\Translation\Drivers\File;
use JoeDixon\Translation\Drivers\Translation;
use JoeDixon\Translation\Scanner;
class SynchroniseTranslationsCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'translation:sync-translations {from?} {to?} {language?}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Synchronise translations between drivers';
/**
* File scanner.
*
* @var Scanner
*/
private $scanner;
/**
* Translation.
*
* @var Translation
*/
private $translation;
/**
* From driver.
*/
private $fromDriver;
/**
* To driver.
*/
private $toDriver;
/**
* Translation drivers.
*
* @var array
*/
private $drivers = ['file', 'database'];
/**
* Create a new command instance.
*
* @return void
*/
public function __construct(Scanner $scanner, Translation $translation)
{
parent::__construct();
$this->scanner = $scanner;
$this->translation = $translation;
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$languages = array_keys($this->translation->allLanguages()->toArray());
// If a valid from driver has been specified as an argument.
if ($this->argument('from') && in_array($this->argument('from'), $this->drivers)) {
$this->fromDriver = $this->argument('from');
}
// When the from driver will be entered manually or if the argument is invalid.
else {
$this->fromDriver = $this->anticipate('Which driver would you like to take translations from?', $this->drivers);
if (! in_array($this->fromDriver, $this->drivers)) {
return $this->error('Invalid driver');
}
}
// Create the driver.
$this->fromDriver = $this->createDriver($this->fromDriver);
// When the to driver has been specified.
if ($this->argument('to') && in_array($this->argument('to'), $this->drivers)) {
$this->toDriver = $this->argument('to');
}
// When the to driver will be entered manually.
else {
$this->toDriver = $this->anticipate('Which driver would you like to add the translations to?', $this->drivers);
if (! in_array($this->toDriver, $this->drivers)) {
return $this->error('Invalid driver');
}
}
// Create the driver.
$this->toDriver = $this->createDriver($this->toDriver);
// If the language argument is set.
if ($this->argument('language')) {
// If all languages should be synced.
if ($this->argument('language') == 'all') {
$language = false;
}
// When a specific language is set and is valid.
elseif (in_array($this->argument('language'), $languages)) {
$language = $this->argument('language');
} else {
return $this->error('Invalid language');
}
} // When the language will be entered manually or if the argument is invalid.
else {
$language = $this->anticipate('Which language? (leave blank for all)', $languages);
if ($language && ! in_array($language, $languages)) {
return $this->error('Invalid language');
}
}
$this->line('Syncing translations');
// If a specific language is set.
if ($language) {
$this->mergeTranslations($this->toDriver, $language, $this->fromDriver->allTranslationsFor($language));
} // Else process all languages.
else {
$translations = $this->mergeLanguages($this->toDriver, $this->fromDriver->allTranslations());
}
$this->info('Translations have been synced');
}
private function createDriver($driver)
{
if ($driver === 'file') {
return new File(new Filesystem, app('path.lang'), config('app.locale'), $this->scanner);
}
return new Database(config('app.locale'), $this->scanner);
}
private function mergeLanguages($driver, $languages)
{
foreach ($languages as $language => $translations) {
$this->mergeTranslations($driver, $language, $translations);
}
}
private function mergeTranslations($driver, $language, $translations)
{
$this->mergeGroupTranslations($driver, $language, $translations['group']);
$this->mergeSingleTranslations($driver, $language, $translations['single']);
}
private function mergeGroupTranslations($driver, $language, $groups)
{
foreach ($groups as $group => $translations) {
foreach ($translations as $key => $value) {
if (is_array($value)) {
continue;
}
$driver->addGroupTranslation($language, $group, $key, $value);
}
}
}
private function mergeSingleTranslations($driver, $language, $vendors)
{
foreach ($vendors as $vendor => $translations) {
foreach ($translations as $key => $value) {
if (is_array($value)) {
continue;
}
$driver->addSingleTranslation($language, $vendor, $key, $value);
}
}
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace JoeDixon\Translation;
use Illuminate\Contracts\Translation\Loader;
use JoeDixon\Translation\Drivers\Translation;
class ContractDatabaseLoader implements Loader
{
private $translation;
public function __construct(Translation $translation)
{
$this->translation = $translation;
}
/**
* Load the messages for the given locale.
*
* @param string $locale
* @param string $group
* @param string $namespace
* @return array
*/
public function load($locale, $group, $namespace = null)
{
if ($group == '*' && $namespace == '*') {
return $this->translation->getSingleTranslationsFor($locale)->get('single', collect())->toArray();
}
if (is_null($namespace) || $namespace == '*') {
return $this->translation->getGroupTranslationsFor($locale)->filter(function ($value, $key) use ($group) {
return $key === $group;
})->first();
}
return $this->translation->getGroupTranslationsFor($locale)->filter(function ($value, $key) use ($group, $namespace) {
return $key === "{$namespace}::{$group}";
})->first();
}
/**
* Add a new namespace to the loader.
*
* @param string $namespace
* @param string $hint
* @return void
*/
public function addNamespace($namespace, $hint)
{
//
}
/**
* Add a new JSON path to the loader.
*
* @param string $path
* @return void
*/
public function addJsonPath($path)
{
//
}
/**
* Get an array of all the registered namespaces.
*
* @return array
*/
public function namespaces()
{
return [];
}
}

View File

@@ -0,0 +1,279 @@
<?php
namespace JoeDixon\Translation\Drivers;
use Illuminate\Support\Collection;
use JoeDixon\Translation\Exceptions\LanguageExistsException;
use JoeDixon\Translation\Language;
use JoeDixon\Translation\Translation as TranslationModel;
use Throwable;
class Database extends Translation implements DriverInterface
{
protected $sourceLanguage;
protected $scanner;
protected array $groupTranslationCache = [];
protected array $languageCache = [];
public function __construct($sourceLanguage, $scanner)
{
$this->sourceLanguage = $sourceLanguage;
$this->scanner = $scanner;
}
/**
* Get all languages from the application.
*
* @return Collection
*/
public function allLanguages()
{
return Language::all()->mapWithKeys(function ($language) {
return [$language->language => $language->name ?: $language->language];
});
}
/**
* Get all group translations from the application.
*
* @return array
*/
public function allGroup($language)
{
$groups = TranslationModel::getGroupsForLanguage($language);
return $groups->map(function ($translation) {
return $translation->group;
});
}
/**
* Get all the translations from the application.
*
* @return Collection
*/
public function allTranslations()
{
return $this->allLanguages()->mapWithKeys(function ($name, $language) {
return [$language => $this->allTranslationsFor($language)];
});
}
/**
* Get all translations for a particular language.
*
* @param string $language
* @return Collection
*/
public function allTranslationsFor($language)
{
return Collection::make([
'group' => $this->getGroupTranslationsFor($language),
'single' => $this->getSingleTranslationsFor($language),
]);
}
/**
* Add a new language to the application.
*
* @param string $language
* @return void
*/
public function addLanguage($language, $name = null)
{
if ($this->languageExists($language)) {
throw new LanguageExistsException(__('translation::errors.language_exists', ['language' => $language]));
}
Language::create([
'language' => $language,
'name' => $name,
]);
}
/**
* Add a new group type translation.
*
* @param string $language
* @param string $key
* @param string $value
* @return void
*/
public function addGroupTranslation($language, $group, $key, $value = '')
{
if (! $this->languageExists($language)) {
$this->addLanguage($language);
}
Language::where('language', $language)
->first()
->translations()
->updateOrCreate([
'group' => $group,
'key' => $key,
], [
'group' => $group,
'key' => $key,
'value' => $value,
]);
}
/**
* Add a new single type translation.
*
* @param string $language
* @param string $key
* @param string $value
* @return void
*/
public function addSingleTranslation($language, $vendor, $key, $value = '')
{
if (! $this->languageExists($language)) {
$this->addLanguage($language);
}
Language::where('language', $language)
->first()
->translations()
->updateOrCreate([
'group' => $vendor,
'key' => $key,
], [
'key' => $key,
'value' => $value,
]);
}
/**
* Get all of the single translations for a given language.
*
* @param string $language
* @return Collection
*/
public function getSingleTranslationsFor($language)
{
$translations = $this->getLanguage($language)
->translations()
->where('group', 'like', '%single')
->orWhereNull('group')
->get()
->groupBy('group');
// if there is no group, this is a legacy translation so we need to
// update to 'single'. We do this here so it only happens once.
if ($this->hasLegacyGroups($translations->keys())) {
TranslationModel::whereNull('group')->update(['group' => 'single']);
// if any legacy groups exist, rerun the method so we get the
// updated keys.
return $this->getSingleTranslationsFor($language);
}
return $translations->map(function ($translations, $group) {
return $translations->mapWithKeys(function ($translation) {
return [$translation->key => $translation->value];
});
});
}
/**
* Get all of the group translations for a given language.
*
* @param string $language
* @return Collection
*/
public function getGroupTranslationsFor($language)
{
if (isset($this->groupTranslationCache[$language])) {
return $this->groupTranslationCache[$language];
}
$languageModel = $this->getLanguage($language);
if (is_null($languageModel)) {
return collect();
}
$translations = $languageModel
->translations()
->whereNotNull('group')
->where('group', 'not like', '%single')
->get()
->groupBy('group');
$result = $translations->map(function ($translations) {
return $translations->mapWithKeys(function ($translation) {
return [$translation->key => $translation->value];
});
});
$this->groupTranslationCache[$language] = $result;
return $result;
}
/**
* Determine whether or not a language exists.
*
* @param string $language
* @return bool
*/
public function languageExists($language)
{
return $this->getLanguage($language) ? true : false;
}
/**
* Get a collection of group names for a given language.
*
* @param string $language
* @return Collection
*/
public function getGroupsFor($language)
{
return $this->allGroup($language);
}
/**
* Get a language from the database.
*
* @param string $language
* @return Language
*/
private function getLanguage($language)
{
if (isset($this->languageCache[$language])) {
return $this->languageCache[$language];
}
// Some constallation of composer packages can lead to our code being executed
// as a dependency of running migrations. That's why we need to be able to
// handle the case where the database is empty / our tables don't exist:
try {
$result = Language::where('language', $language)->first();
} catch (Throwable) {
$result = null;
}
$this->languageCache[$language] = $result;
return $result;
}
/**
* Determine if a set of single translations contains any legacy groups.
* Previously, this was handled by setting the group value to NULL, now
* we use 'single' to cater for vendor JSON language files.
*
* @param Collection $groups
* @return bool
*/
private function hasLegacyGroups($groups)
{
return $groups->filter(function ($key) {
return $key === '';
})->count() > 0;
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace JoeDixon\Translation\Drivers;
interface DriverInterface
{
/**
* Get all languages from the application.
*
* @return Collection
*/
public function allLanguages();
/**
* Get all group translations from the application.
*
* @return array
*/
public function allGroup($language);
/**
* Get all the translations from the application.
*
* @return Collection
*/
public function allTranslations();
/**
* Get all translations for a particular language.
*
* @param string $language
* @return Collection
*/
public function allTranslationsFor($language);
/**
* Add a new language to the application.
*
* @param string $language
* @return void
*/
public function addLanguage($language, $name = null);
/**
* Add a new group type translation.
*
* @param string $language
* @param string $key
* @param string $value
* @return void
*/
public function addGroupTranslation($language, $group, $key, $value = '');
/**
* Add a new single type translation.
*
* @param string $language
* @param string $key
* @param string $value
* @return void
*/
public function addSingleTranslation($language, $vendor, $key, $value = '');
/**
* Get all of the single translations for a given language.
*
* @param string $language
* @return Collection
*/
public function getSingleTranslationsFor($language);
/**
* Get all of the group translations for a given language.
*
* @param string $language
* @return Collection
*/
public function getGroupTranslationsFor($language);
/**
* Determine whether or not a language exists.
*
* @param string $language
* @return bool
*/
public function languageExists($language);
/**
* Find all of the translations in the app without translation for a given language.
*
* @param string $language
* @return array
*/
public function findMissingTranslations($language);
/**
* Save all of the translations in the app without translation for a given language.
*
* @param string $language
* @return void
*/
public function saveMissingTranslations($language = false);
/**
* Get a collection of group names for a given language.
*
* @param string $language
* @return Collection
*/
public function getGroupsFor($language);
/**
* Get all translations for a given language merged with the source language.
*
* @param string $language
* @return Collection
*/
public function getSourceLanguageTranslationsWith($language);
/**
* Filter all keys and translations for a given language and string.
*
* @param string $language
* @param string $filter
* @return Collection
*/
public function filterTranslationsFor($language, $filter);
}

View File

@@ -0,0 +1,369 @@
<?php
namespace JoeDixon\Translation\Drivers;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use JoeDixon\Translation\Exceptions\LanguageExistsException;
class File extends Translation implements DriverInterface
{
private $disk;
private $languageFilesPath;
protected $sourceLanguage;
protected $scanner;
public function __construct(Filesystem $disk, $languageFilesPath, $sourceLanguage, $scanner)
{
$this->disk = $disk;
$this->languageFilesPath = $languageFilesPath;
$this->sourceLanguage = $sourceLanguage;
$this->scanner = $scanner;
}
/**
* Get all languages from the application.
*
* @return Collection
*/
public function allLanguages()
{
// As per the docs, there should be a subdirectory within the
// languages path so we can return these directory names as a collection
$directories = Collection::make($this->disk->directories($this->languageFilesPath));
return $directories->mapWithKeys(function ($directory) {
$language = basename($directory);
return [$language => $language];
})->filter(function ($language) {
// at the moemnt, we're not supporting vendor specific translations
return $language != 'vendor';
});
}
/**
* Get all group translations from the application.
*
* @return array
*/
public function allGroup($language)
{
$groupPath = "{$this->languageFilesPath}".DIRECTORY_SEPARATOR."{$language}";
if (! $this->disk->exists($groupPath)) {
return [];
}
$groups = Collection::make($this->disk->allFiles($groupPath));
return $groups->map(function ($group) {
return $group->getBasename('.php');
});
}
/**
* Get all the translations from the application.
*
* @return Collection
*/
public function allTranslations()
{
return $this->allLanguages()->mapWithKeys(function ($language) {
return [$language => $this->allTranslationsFor($language)];
});
}
/**
* Get all translations for a particular language.
*
* @param string $language
* @return Collection
*/
public function allTranslationsFor($language)
{
return Collection::make([
'group' => $this->getGroupTranslationsFor($language),
'single' => $this->getSingleTranslationsFor($language),
]);
}
/**
* Add a new language to the application.
*
* @param string $language
* @return void
*/
public function addLanguage($language, $name = null)
{
if ($this->languageExists($language)) {
throw new LanguageExistsException(__('translation::errors.language_exists', ['language' => $language]));
}
$this->disk->makeDirectory("{$this->languageFilesPath}".DIRECTORY_SEPARATOR."$language");
if (! $this->disk->exists("{$this->languageFilesPath}".DIRECTORY_SEPARATOR."{$language}.json")) {
$this->saveSingleTranslations($language, collect(['single' => collect()]));
}
}
/**
* Add a new group type translation.
*
* @param string $language
* @param string $key
* @param string $value
* @return void
*/
public function addGroupTranslation($language, $group, $key, $value = '')
{
if (! $this->languageExists($language)) {
$this->addLanguage($language);
}
$translations = $this->getGroupTranslationsFor($language);
// does the group exist? If not, create it.
if (! $translations->keys()->contains($group)) {
$translations->put($group, collect());
}
$values = $translations->get($group);
$values[$key] = $value;
$translations->put($group, collect($values));
$this->saveGroupTranslations($language, $group, $translations->get($group));
}
/**
* Add a new single type translation.
*
* @param string $language
* @param string $key
* @param string $value
* @return void
*/
public function addSingleTranslation($language, $vendor, $key, $value = '')
{
if (! $this->languageExists($language)) {
$this->addLanguage($language);
}
$translations = $this->getSingleTranslationsFor($language);
$translations->get($vendor) ?: $translations->put($vendor, collect());
$translations->get($vendor)->put($key, $value);
$this->saveSingleTranslations($language, $translations);
}
/**
* Get all of the single translations for a given language.
*
* @param string $language
* @return Collection
*/
public function getSingleTranslationsFor($language)
{
$files = new Collection($this->disk->allFiles($this->languageFilesPath));
return $files->filter(function ($file) use ($language) {
return strpos($file, "{$language}.json");
})->flatMap(function ($file) {
if (strpos($file->getPathname(), 'vendor')) {
$vendor = Str::before(Str::after($file->getPathname(), 'vendor'.DIRECTORY_SEPARATOR), DIRECTORY_SEPARATOR);
return ["{$vendor}::single" => new Collection(json_decode($this->disk->get($file), true))];
}
return ['single' => new Collection(json_decode($this->disk->get($file), true))];
});
}
/**
* Get all of the group translations for a given language.
*
* @param string $language
* @return Collection
*/
public function getGroupTranslationsFor($language)
{
return $this->getGroupFilesFor($language)->mapWithKeys(function ($group) {
// here we check if the path contains 'vendor' as these will be the
// files which need namespacing
if (Str::contains($group->getPathname(), 'vendor')) {
$vendor = Str::before(Str::after($group->getPathname(), 'vendor'.DIRECTORY_SEPARATOR), DIRECTORY_SEPARATOR);
return ["{$vendor}::{$group->getBasename('.php')}" => new Collection(Arr::dot($this->disk->getRequire($group->getPathname())))];
}
return [$group->getBasename('.php') => new Collection(Arr::dot($this->disk->getRequire($group->getPathname())))];
});
}
/**
* Get all the translations for a given file.
*
* @param string $language
* @param string $file
* @return array
*/
public function getTranslationsForFile($language, $file)
{
$file = Str::finish($file, '.php');
$filePath = "{$this->languageFilesPath}".DIRECTORY_SEPARATOR."{$language}".DIRECTORY_SEPARATOR."{$file}";
$translations = [];
if ($this->disk->exists($filePath)) {
$translations = Arr::dot($this->disk->getRequire($filePath));
}
return $translations;
}
/**
* Determine whether or not a language exists.
*
* @param string $language
* @return bool
*/
public function languageExists($language)
{
return $this->allLanguages()->contains($language);
}
/**
* Add a new group of translations.
*
* @param string $language
* @param string $group
* @return void
*/
public function addGroup($language, $group)
{
$this->saveGroupTranslations($language, $group, []);
}
/**
* Save group type language translations.
*
* @param string $language
* @param string $group
* @param array $translations
* @return void
*/
public function saveGroupTranslations($language, $group, $translations)
{
// here we check if it's a namespaced translation which need saving to a
// different path
$translations = $translations instanceof Collection ? $translations->toArray() : $translations;
ksort($translations);
$translations = array_undot($translations);
if (Str::contains($group, '::')) {
return $this->saveNamespacedGroupTranslations($language, $group, $translations);
}
$this->disk->put("{$this->languageFilesPath}".DIRECTORY_SEPARATOR."{$language}".DIRECTORY_SEPARATOR."{$group}.php", "<?php\n\nreturn ".var_export($translations, true).';'.\PHP_EOL);
}
/**
* Save namespaced group type language translations.
*
* @param string $language
* @param string $group
* @param array $translations
* @return void
*/
private function saveNamespacedGroupTranslations($language, $group, $translations)
{
[$namespace, $group] = explode('::', $group);
$directory = "{$this->languageFilesPath}".DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR."{$namespace}".DIRECTORY_SEPARATOR."{$language}";
if (! $this->disk->exists($directory)) {
$this->disk->makeDirectory($directory, 0755, true);
}
$this->disk->put("$directory".DIRECTORY_SEPARATOR."{$group}.php", "<?php\n\nreturn ".var_export($translations, true).';'.\PHP_EOL);
}
/**
* Save single type language translations.
*
* @param string $language
* @param array $translations
* @return void
*/
private function saveSingleTranslations($language, $translations)
{
foreach ($translations as $group => $translation) {
$vendor = Str::before($group, '::single');
$languageFilePath = $vendor !== 'single' ? 'vendor'.DIRECTORY_SEPARATOR."{$vendor}".DIRECTORY_SEPARATOR."{$language}.json" : "{$language}.json";
$this->disk->put(
"{$this->languageFilesPath}".DIRECTORY_SEPARATOR."{$languageFilePath}",
json_encode((object) $translations->get($group), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
);
}
}
/**
* Get all the group files for a given language.
*
* @param string $language
* @return Collection
*/
public function getGroupFilesFor($language)
{
$groups = new Collection($this->disk->allFiles("{$this->languageFilesPath}".DIRECTORY_SEPARATOR."{$language}"));
// namespaced files reside in the vendor directory so we'll grab these
// the `getVendorGroupFileFor` method
$groups = $groups->merge($this->getVendorGroupFilesFor($language));
return $groups;
}
/**
* Get a collection of group names for a given language.
*
* @param string $language
* @return Collection
*/
public function getGroupsFor($language)
{
return $this->getGroupFilesFor($language)->map(function ($file) {
if (Str::contains($file->getPathname(), 'vendor')) {
$vendor = Str::before(Str::after($file->getPathname(), 'vendor'.DIRECTORY_SEPARATOR), DIRECTORY_SEPARATOR);
return "{$vendor}::{$file->getBasename('.php')}";
}
return $file->getBasename('.php');
});
}
/**
* Get all the vendor group files for a given language.
*
* @param string $language
* @return Collection
*/
public function getVendorGroupFilesFor($language)
{
if (! $this->disk->exists("{$this->languageFilesPath}".DIRECTORY_SEPARATOR.'vendor')) {
return;
}
$vendorGroups = [];
foreach ($this->disk->directories("{$this->languageFilesPath}".DIRECTORY_SEPARATOR.'vendor') as $vendor) {
$vendor = Arr::last(explode(DIRECTORY_SEPARATOR, $vendor));
if (! $this->disk->exists("{$this->languageFilesPath}".DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR."{$vendor}".DIRECTORY_SEPARATOR."{$language}")) {
array_push($vendorGroups, []);
} else {
array_push($vendorGroups, $this->disk->allFiles("{$this->languageFilesPath}".DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR."{$vendor}".DIRECTORY_SEPARATOR."{$language}"));
}
}
return new Collection(Arr::flatten($vendorGroups));
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace JoeDixon\Translation\Drivers;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str;
use JoeDixon\Translation\Events\TranslationAdded;
abstract class Translation
{
/**
* Find all of the translations in the app without translation for a given language.
*
* @param string $language
* @return array
*/
public function findMissingTranslations($language)
{
return array_diff_assoc_recursive(
$this->scanner->findTranslations(),
$this->allTranslationsFor($language)
);
}
/**
* Save all of the translations in the app without translation for a given language.
*
* @param string $language
* @return void
*/
public function saveMissingTranslations($language = false)
{
$languages = $language ? [$language => $language] : $this->allLanguages();
foreach ($languages as $language => $name) {
$missingTranslations = $this->findMissingTranslations($language);
foreach ($missingTranslations as $type => $groups) {
foreach ($groups as $group => $translations) {
foreach ($translations as $key => $value) {
if (Str::contains($group, 'single')) {
$this->addSingleTranslation($language, $group, $key);
} else {
$this->addGroupTranslation($language, $group, $key);
}
}
}
}
}
}
/**
* Get all translations for a given language merged with the source language.
*
* @param string $language
* @return Collection
*/
public function getSourceLanguageTranslationsWith($language)
{
$sourceTranslations = $this->allTranslationsFor($this->sourceLanguage);
$languageTranslations = $this->allTranslationsFor($language);
return $sourceTranslations->map(function ($groups, $type) use ($language, $languageTranslations) {
return $groups->map(function ($translations, $group) use ($type, $language, $languageTranslations) {
$translations = $translations->toArray();
array_walk($translations, function (&$value, $key) use ($type, $group, $language, $languageTranslations) {
$value = [
$this->sourceLanguage => $value,
$language => $languageTranslations->get($type, collect())->get($group, collect())->get($key),
];
});
return $translations;
});
});
}
/**
* Filter all keys and translations for a given language and string.
*
* @param string $language
* @param string $filter
* @return Collection
*/
public function filterTranslationsFor($language, $filter)
{
$allTranslations = $this->getSourceLanguageTranslationsWith(($language));
if (! $filter) {
return $allTranslations;
}
return $allTranslations->map(function ($groups, $type) use ($language, $filter) {
return $groups->map(function ($keys, $group) use ($language, $filter) {
return collect($keys)->filter(function ($translations, $key) use ($group, $language, $filter) {
return strs_contain([$group, $key, $translations[$language], $translations[$this->sourceLanguage]], $filter);
});
})->filter(function ($keys) {
return $keys->isNotEmpty();
});
});
}
public function add(Request $request, $language, $isGroupTranslation)
{
$namespace = $request->has('namespace') && $request->get('namespace') ? "{$request->get('namespace')}::" : '';
$group = $namespace.$request->get('group');
$key = $request->get('key');
$value = $request->get('value') ?: '';
if ($isGroupTranslation) {
$this->addGroupTranslation($language, $group, $key, $value);
} else {
$this->addSingleTranslation($language, 'single', $key, $value);
}
Event::dispatch(new TranslationAdded($language, $group ?: 'single', $key, $value));
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace JoeDixon\Translation\Events;
use Illuminate\Foundation\Events\Dispatchable;
class TranslationAdded
{
use Dispatchable;
public $key;
public $group;
public $value;
public $language;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(string $language, string $group, string $key, string $value)
{
$this->language = $language;
$this->group = $group;
$this->key = $key;
$this->value = $value;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace JoeDixon\Translation\Exceptions;
class LanguageExistsException extends \Exception
{
}

View File

@@ -0,0 +1,7 @@
<?php
namespace JoeDixon\Translation\Exceptions;
class LanguageKeyExistsException extends \Exception
{
}

View File

@@ -0,0 +1,39 @@
<?php
namespace JoeDixon\Translation\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use JoeDixon\Translation\Drivers\Translation;
use JoeDixon\Translation\Http\Requests\LanguageRequest;
class LanguageController extends Controller
{
private $translation;
public function __construct(Translation $translation)
{
$this->translation = $translation;
}
public function index(Request $request)
{
$languages = $this->translation->allLanguages();
return view('translation::languages.index', compact('languages'));
}
public function create()
{
return view('translation::languages.create');
}
public function store(LanguageRequest $request)
{
$this->translation->addLanguage($request->locale, $request->name);
return redirect()
->route('languages.index')
->with('success', __('translation::translation.language_added'));
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace JoeDixon\Translation\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use JoeDixon\Translation\Drivers\Translation;
use JoeDixon\Translation\Http\Requests\TranslationRequest;
class LanguageTranslationController extends Controller
{
private $translation;
public function __construct(Translation $translation)
{
$this->translation = $translation;
}
public function index(Request $request, $language)
{
// dd($this->translation->getSingleTranslationsFor('en'));
if ($request->has('language') && $request->get('language') !== $language) {
return redirect()
->route('languages.translations.index', ['language' => $request->get('language'), 'group' => $request->get('group'), 'filter' => $request->get('filter')]);
}
$languages = $this->translation->allLanguages();
$groups = $this->translation->getGroupsFor(config('app.locale'))->merge('single');
$translations = $this->translation->filterTranslationsFor($language, $request->get('filter'));
if ($request->has('group') && $request->get('group')) {
if ($request->get('group') === 'single') {
$translations = $translations->get('single');
$translations = new Collection(['single' => $translations]);
} else {
$translations = $translations->get('group')->filter(function ($values, $group) use ($request) {
return $group === $request->get('group');
});
$translations = new Collection(['group' => $translations]);
}
}
return view('translation::languages.translations.index', compact('language', 'languages', 'groups', 'translations'));
}
public function create(Request $request, $language)
{
return view('translation::languages.translations.create', compact('language'));
}
public function store(TranslationRequest $request, $language)
{
$isGroupTranslation = $request->filled('group');
$this->translation->add($request, $language, $isGroupTranslation);
return redirect()
->route('languages.translations.index', $language)
->with('success', __('translation::translation.translation_added'));
}
public function update(Request $request, $language)
{
$isGroupTranslation = ! Str::contains($request->get('group'), 'single');
$this->translation->add($request, $language, $isGroupTranslation);
return ['success' => true];
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace JoeDixon\Translation\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use JoeDixon\Translation\Rules\LanguageNotExists;
class LanguageRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'name' => 'nullable|string',
'locale' => ['required', new LanguageNotExists],
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace JoeDixon\Translation\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class TranslationRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'key' => 'required',
'value' => 'required',
];
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace JoeDixon\Translation;
use Illuminate\Translation\LoaderInterface;
use JoeDixon\Translation\Drivers\Translation;
class InterfaceDatabaseLoader implements LoaderInterface
{
private $translation;
public function __construct(Translation $translation)
{
$this->translation = $translation;
}
/**
* Load the messages for the given locale.
*
* @param string $locale
* @param string $group
* @param string $namespace
* @return array
*/
public function load($locale, $group, $namespace = null)
{
if ($group == '*' && $namespace == '*') {
return $this->translation->getSingleTranslationsFor($locale)->get('single', collect())->toArray();
}
if (is_null($namespace) || $namespace == '*') {
return $this->translation->getGroupTranslationsFor($locale)->filter(function ($value, $key) use ($group) {
return $key === $group;
})->first();
}
return $this->translation->getGroupTranslationsFor($locale)->filter(function ($value, $key) use ($group, $namespace) {
return $key === "{$namespace}::{$group}";
})->first();
}
/**
* Add a new namespace to the loader.
*
* @param string $namespace
* @param string $hint
* @return void
*/
public function addNamespace($namespace, $hint)
{
//
}
/**
* Add a new JSON path to the loader.
*
* @param string $path
* @return void
*/
public function addJsonPath($path)
{
//
}
/**
* Get an array of all the registered namespaces.
*
* @return array
*/
public function namespaces()
{
return [];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace JoeDixon\Translation;
use Illuminate\Database\Eloquent\Model;
class Language extends Model
{
protected $guarded = [];
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->connection = config('translation.database.connection');
$this->table = config('translation.database.languages_table');
}
public function translations()
{
return $this->hasMany(Translation::class);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace JoeDixon\Translation\Rules;
use Illuminate\Contracts\Validation\Rule;
use JoeDixon\Translation\Drivers\Translation;
class LanguageNotExists implements Rule
{
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
$translation = app()->make(Translation::class);
return ! $translation->languageExists($value);
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return __('translation::translation.language_exists');
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace JoeDixon\Translation;
use Illuminate\Filesystem\Filesystem;
class Scanner
{
private $disk;
private $scanPaths;
private $translationMethods;
public function __construct(Filesystem $disk, $scanPaths, $translationMethods)
{
$this->disk = $disk;
$this->scanPaths = $scanPaths;
$this->translationMethods = $translationMethods;
}
/**
* Scan all the files in the provided $scanPath for translations.
*
* @return array
*/
public function findTranslations()
{
$results = ['single' => [], 'group' => []];
// This has been derived from a combination of the following:
// * Laravel Language Manager GUI from Mohamed Said (https://github.com/themsaid/laravel-langman-gui)
// * Laravel 5 Translation Manager from Barry vd. Heuvel (https://github.com/barryvdh/laravel-translation-manager)
$matchingPattern =
'[^\w]'. // Must not start with any alphanum or _
'(?<!->)'. // Must not start with ->
'('.implode('|', $this->translationMethods).')'. // Must start with one of the functions
"\(". // Match opening parentheses
"\s*". // Whitespace before param
"[\'\"]". // Match " or '
'('. // Start a new group to match:
'.+'. // Must start with group
')'. // Close group
"[\'\"]". // Closing quote
"\s*". // Whitespace after param
"[\),]"; // Close parentheses or new parameter
foreach ($this->disk->allFiles($this->scanPaths) as $file) {
if (preg_match_all("/$matchingPattern/siU", $file->getContents(), $matches)) {
foreach ($matches[2] as $key) {
if (preg_match("/(^[a-zA-Z0-9:_-]+([.][^\1)\ ]+)+$)/siU", $key, $arrayMatches)) {
[$file, $k] = explode('.', $arrayMatches[0], 2);
$results['group'][$file][$k] = '';
continue;
} else {
$results['single']['single'][$key] = '';
}
}
}
}
return $results;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace JoeDixon\Translation;
use Illuminate\Database\Eloquent\Model;
class Translation extends Model
{
protected $guarded = [];
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->connection = config('translation.database.connection');
$this->table = config('translation.database.translations_table');
}
public function language()
{
return $this->belongsTo(Language::class);
}
public static function getGroupsForLanguage($language)
{
return static::whereHas('language', function ($q) use ($language) {
$q->where('language', $language);
})->whereNotNull('group')
->where('group', 'not like', '%single')
->select('group')
->distinct()
->get();
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace JoeDixon\Translation;
use Illuminate\Translation\TranslationServiceProvider as ServiceProvider;
use Illuminate\Translation\Translator;
use JoeDixon\Translation\Drivers\Translation;
class TranslationBindingsServiceProvider extends ServiceProvider
{
/**
* Register package bindings in the container.
*
* @return void
*/
public function register()
{
if ($this->app['config']['translation.driver'] === 'database') {
$this->registerDatabaseTranslator();
} else {
parent::register();
}
}
private function registerDatabaseTranslator()
{
$this->registerDatabaseLoader();
$this->app->singleton('translator', function ($app) {
$loader = $app['translation.loader'];
// When registering the translator component, we'll need to set the default
// locale as well as the fallback locale. So, we'll grab the application
// configuration so we can easily get both of these values from there.
$locale = $app['config']['app.locale'];
$trans = new Translator($loader, $locale);
$trans->setFallback($app['config']['app.fallback_locale']);
return $trans;
});
}
protected function registerDatabaseLoader()
{
$this->app->singleton('translation.loader', function ($app) {
// Post Laravel 5.4, the interface was moved to the contracts
// directory. Here we perform a check to see whether or not the
// interface exists and instantiate the relevant loader accordingly.
if (interface_exists('Illuminate\Contracts\Translation\Loader')) {
return new ContractDatabaseLoader($this->app->make(Translation::class));
}
return new InterfaceDatabaseLoader($this->app->make(Translation::class));
});
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace JoeDixon\Translation;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
use JoeDixon\Translation\Drivers\Database;
use JoeDixon\Translation\Drivers\File;
class TranslationManager
{
private $app;
private $config;
private $scanner;
public function __construct($app, $config, $scanner)
{
$this->app = $app;
$this->config = $config;
$this->scanner = $scanner;
}
public function resolve()
{
$driver = $this->config['driver'];
$driverResolver = Str::studly($driver);
$method = "resolve{$driverResolver}Driver";
if (! method_exists($this, $method)) {
throw new \InvalidArgumentException("Invalid driver [$driver]");
}
return $this->{$method}();
}
protected function resolveFileDriver()
{
return new File(new Filesystem, $this->app['path.lang'], $this->app->config['app']['locale'], $this->scanner);
}
protected function resolveDatabaseDriver()
{
return new Database($this->app->config['app']['locale'], $this->scanner);
}
}

View File

@@ -0,0 +1,185 @@
<?php
namespace JoeDixon\Translation;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\ServiceProvider;
use JoeDixon\Translation\Console\Commands\AddLanguageCommand;
use JoeDixon\Translation\Console\Commands\AddTranslationKeyCommand;
use JoeDixon\Translation\Console\Commands\ListLanguagesCommand;
use JoeDixon\Translation\Console\Commands\ListMissingTranslationKeys;
use JoeDixon\Translation\Console\Commands\SynchroniseMissingTranslationKeys;
use JoeDixon\Translation\Console\Commands\SynchroniseTranslationsCommand;
use JoeDixon\Translation\Drivers\Translation;
class TranslationServiceProvider extends ServiceProvider
{
/**
* Bootstrap the package services.
*
* @return void
*/
public function boot()
{
$this->loadViews();
$this->registerRoutes();
$this->publishConfiguration();
$this->publishAssets();
$this->loadMigrations();
$this->loadTranslations();
$this->registerHelpers();
}
/**
* Register package bindings in the container.
*
* @return void
*/
public function register()
{
$this->mergeConfiguration();
$this->registerCommands();
$this->registerContainerBindings();
}
/**
* Load and publish package views.
*
* @return void
*/
private function loadViews()
{
$this->loadViewsFrom(__DIR__.'/../resources/views', 'translation');
$this->publishes([
__DIR__.'/../resources/views' => resource_path('views/vendor/translation'),
]);
}
/**
* Register package routes.
*
* @return void
*/
private function registerRoutes()
{
$this->loadRoutesFrom(__DIR__.'/../routes/web.php');
}
/**
* Publish package configuration.
*
* @return void
*/
private function publishConfiguration()
{
$this->publishes([
__DIR__.'/../config/translation.php' => config_path('translation.php'),
], 'config');
}
/**
* Merge package configuration.
*
* @return void
*/
private function mergeConfiguration()
{
$this->mergeConfigFrom(__DIR__.'/../config/translation.php', 'translation');
}
/**
* Publish package assets.
*
* @return void
*/
private function publishAssets()
{
$this->publishes([
__DIR__.'/../public/assets' => public_path('vendor/translation'),
], 'assets');
}
/**
* Load package migrations.
*
* @return void
*/
private function loadMigrations()
{
if (config('translation.driver') !== 'database') {
return;
}
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
}
/**
* Load package translations.
*
* @return void
*/
private function loadTranslations()
{
$this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'translation');
$this->publishes([
__DIR__.'/../resources/lang' => resource_path('lang/vendor/translation'),
]);
}
/**
* Register package commands.
*
* @return void
*/
private function registerCommands()
{
if ($this->app->runningInConsole()) {
$this->commands([
AddLanguageCommand::class,
AddTranslationKeyCommand::class,
ListLanguagesCommand::class,
ListMissingTranslationKeys::class,
SynchroniseMissingTranslationKeys::class,
SynchroniseTranslationsCommand::class,
]);
}
}
/**
* Register package helper functions.
*
* @return void
*/
private function registerHelpers()
{
require __DIR__.'/../resources/helpers.php';
}
/**
* Register package bindings in the container.
*
* @return void
*/
private function registerContainerBindings()
{
$this->app->singleton(Scanner::class, function () {
$config = $this->app['config']['translation'];
return new Scanner(new Filesystem(), $config['scan_paths'], $config['translation_methods']);
});
$this->app->singleton(Translation::class, function ($app) {
return (new TranslationManager($app, $app['config']['translation'], $app->make(Scanner::class)))->resolve();
});
}
}

View File

@@ -0,0 +1,952 @@
/*
Tailwind - The Utility-First CSS Framework
A project by Adam Wathan (@adamwathan), Jonathan Reinink (@reinink),
David Hemphill (@davidhemphill) and Steve Schoger (@steveschoger).
Welcome to the Tailwind config file. This is where you can customize
Tailwind specifically for your project. Don't be intimidated by the
length of this file. It's really just a big JavaScript object and
we've done our very best to explain each section.
View the full documentation at https://tailwindcss.com.
|-------------------------------------------------------------------------------
| The default config
|-------------------------------------------------------------------------------
|
| This variable contains the default Tailwind config. You don't have
| to use it, but it can sometimes be helpful to have available. For
| example, you may choose to merge your custom configuration
| values with some of the Tailwind defaults.
|
*/
// let defaultConfig = require('tailwindcss/defaultConfig')()
/*
|-------------------------------------------------------------------------------
| Colors https://tailwindcss.com/docs/colors
|-------------------------------------------------------------------------------
|
| Here you can specify the colors used in your project. To get you started,
| we've provided a generous palette of great looking colors that are perfect
| for prototyping, but don't hesitate to change them for your project. You
| own these colors, nothing will break if you change everything about them.
|
| We've used literal color names ("red", "blue", etc.) for the default
| palette, but if you'd rather use functional names like "primary" and
| "secondary", or even a numeric scale like "100" and "200", go for it.
|
*/
let colors = {
'transparent': 'transparent',
'black': '#22292f',
'grey-darkest': '#3d4852',
'grey-darker': '#606f7b',
'grey-dark': '#8795a1',
'grey': '#b8c2cc',
'grey-light': '#dae1e7',
'grey-lighter': '#f1f5f8',
'grey-lightest': '#f8fafc',
'white': '#ffffff',
'red-darkest': '#3b0d0c',
'red-darker': '#621b18',
'red-dark': '#cc1f1a',
'red': '#e3342f',
'red-light': '#ef5753',
'red-lighter': '#f9acaa',
'red-lightest': '#fcebea',
'orange-darkest': '#462a16',
'orange-darker': '#613b1f',
'orange-dark': '#de751f',
'orange': '#f6993f',
'orange-light': '#faad63',
'orange-lighter': '#fcd9b6',
'orange-lightest': '#fff5eb',
'yellow-darkest': '#453411',
'yellow-darker': '#684f1d',
'yellow-dark': '#f2d024',
'yellow': '#ffed4a',
'yellow-light': '#fff382',
'yellow-lighter': '#fff9c2',
'yellow-lightest': '#fcfbeb',
'green-darkest': '#0f2f21',
'green-darker': '#1a4731',
'green-dark': '#1f9d55',
'green': '#38c172',
'green-light': '#51d88a',
'green-lighter': '#a2f5bf',
'green-lightest': '#e3fcec',
'teal-darkest': '#0d3331',
'teal-darker': '#20504f',
'teal-dark': '#38a89d',
'teal': '#4dc0b5',
'teal-light': '#64d5ca',
'teal-lighter': '#a0f0ed',
'teal-lightest': '#e8fffe',
'blue-darkest': '#12283a',
'blue-darker': '#1c3d5a',
'blue-dark': '#125b93',
'blue': '#2891c4',
'blue-light': '#6cb2eb',
'blue-lighter': '#bcdefa',
'blue-lightest': '#eff8ff',
'indigo-darkest': '#191e38',
'indigo-darker': '#2f365f',
'indigo-dark': '#5661b3',
'indigo': '#6574cd',
'indigo-light': '#7886d7',
'indigo-lighter': '#b2b7ff',
'indigo-lightest': '#e6e8ff',
'purple-darkest': '#21183c',
'purple-darker': '#382b5f',
'purple-dark': '#794acf',
'purple': '#9561e2',
'purple-light': '#a779e9',
'purple-lighter': '#d6bbfc',
'purple-lightest': '#f3ebff',
'pink-darkest': '#451225',
'pink-darker': '#6f213f',
'pink-dark': '#eb5286',
'pink': '#f66d9b',
'pink-light': '#fa7ea8',
'pink-lighter': '#ffbbca',
'pink-lightest': '#ffebef',
}
module.exports = {
/*
|-----------------------------------------------------------------------------
| Colors https://tailwindcss.com/docs/colors
|-----------------------------------------------------------------------------
|
| The color palette defined above is also assigned to the "colors" key of
| your Tailwind config. This makes it easy to access them in your CSS
| using Tailwind's config helper. For example:
|
| .error { color: config('colors.red') }
|
*/
colors: colors,
/*
|-----------------------------------------------------------------------------
| Screens https://tailwindcss.com/docs/responsive-design
|-----------------------------------------------------------------------------
|
| Screens in Tailwind are translated to CSS media queries. They define the
| responsive breakpoints for your project. By default Tailwind takes a
| "mobile first" approach, where each screen size represents a minimum
| viewport width. Feel free to have as few or as many screens as you
| want, naming them in whatever way you'd prefer for your project.
|
| Tailwind also allows for more complex screen definitions, which can be
| useful in certain situations. Be sure to see the full responsive
| documentation for a complete list of options.
|
| Class name: .{screen}:{utility}
|
*/
screens: {
'sm': '576px',
'md': '768px',
'lg': '992px',
'xl': '1200px',
},
/*
|-----------------------------------------------------------------------------
| Fonts https://tailwindcss.com/docs/fonts
|-----------------------------------------------------------------------------
|
| Here is where you define your project's font stack, or font families.
| Keep in mind that Tailwind doesn't actually load any fonts for you.
| If you're using custom fonts you'll need to import them prior to
| defining them here.
|
| By default we provide a native font stack that works remarkably well on
| any device or OS you're using, since it just uses the default fonts
| provided by the platform.
|
| Class name: .font-{name}
|
*/
fonts: {
'sans': [
'system-ui',
'BlinkMacSystemFont',
'-apple-system',
'Segoe UI',
'Roboto',
'Oxygen',
'Ubuntu',
'Cantarell',
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
'sans-serif',
],
'serif': [
'Constantia',
'Lucida Bright',
'Lucidabright',
'Lucida Serif',
'Lucida',
'DejaVu Serif',
'Bitstream Vera Serif',
'Liberation Serif',
'Georgia',
'serif',
],
'mono': [
'Menlo',
'Monaco',
'Consolas',
'Liberation Mono',
'Courier New',
'monospace',
]
},
/*
|-----------------------------------------------------------------------------
| Text sizes https://tailwindcss.com/docs/text-sizing
|-----------------------------------------------------------------------------
|
| Here is where you define your text sizes. Name these in whatever way
| makes the most sense to you. We use size names by default, but
| you're welcome to use a numeric scale or even something else
| entirely.
|
| By default Tailwind uses the "rem" unit type for most measurements.
| This allows you to set a root font size which all other sizes are
| then based on. That said, you are free to use whatever units you
| prefer, be it rems, ems, pixels or other.
|
| Class name: .text-{size}
|
*/
textSizes: {
'xs': '.75rem', // 12px
'sm': '.875rem', // 14px
'base': '1rem', // 16px
'lg': '1.125rem', // 18px
'xl': '1.25rem', // 20px
'2xl': '1.5rem', // 24px
'3xl': '1.875rem', // 30px
'4xl': '2.25rem', // 36px
'5xl': '3rem', // 48px
},
/*
|-----------------------------------------------------------------------------
| Font weights https://tailwindcss.com/docs/font-weight
|-----------------------------------------------------------------------------
|
| Here is where you define your font weights. We've provided a list of
| common font weight names with their respective numeric scale values
| to get you started. It's unlikely that your project will require
| all of these, so we recommend removing those you don't need.
|
| Class name: .font-{weight}
|
*/
fontWeights: {
'hairline': 100,
'thin': 200,
'light': 300,
'normal': 400,
'medium': 500,
'semibold': 600,
'bold': 700,
'extrabold': 800,
'black': 900,
},
/*
|-----------------------------------------------------------------------------
| Leading (line height) https://tailwindcss.com/docs/line-height
|-----------------------------------------------------------------------------
|
| Here is where you define your line height values, or as we call
| them in Tailwind, leadings.
|
| Class name: .leading-{size}
|
*/
leading: {
'none': 1,
'tight': 1.25,
'normal': 1.5,
'loose': 2,
},
/*
|-----------------------------------------------------------------------------
| Tracking (letter spacing) https://tailwindcss.com/docs/letter-spacing
|-----------------------------------------------------------------------------
|
| Here is where you define your letter spacing values, or as we call
| them in Tailwind, tracking.
|
| Class name: .tracking-{size}
|
*/
tracking: {
'tight': '-0.05em',
'normal': '0',
'wide': '0.05em',
},
/*
|-----------------------------------------------------------------------------
| Text colors https://tailwindcss.com/docs/text-color
|-----------------------------------------------------------------------------
|
| Here is where you define your text colors. By default these use the
| color palette we defined above, however you're welcome to set these
| independently if that makes sense for your project.
|
| Class name: .text-{color}
|
*/
textColors: colors,
/*
|-----------------------------------------------------------------------------
| Background colors https://tailwindcss.com/docs/background-color
|-----------------------------------------------------------------------------
|
| Here is where you define your background colors. By default these use
| the color palette we defined above, however you're welcome to set
| these independently if that makes sense for your project.
|
| Class name: .bg-{color}
|
*/
backgroundColors: colors,
/*
|-----------------------------------------------------------------------------
| Background sizes https://tailwindcss.com/docs/background-size
|-----------------------------------------------------------------------------
|
| Here is where you define your background sizes. We provide some common
| values that are useful in most projects, but feel free to add other sizes
| that are specific to your project here as well.
|
| Class name: .bg-{size}
|
*/
backgroundSize: {
'auto': 'auto',
'cover': 'cover',
'contain': 'contain',
},
/*
|-----------------------------------------------------------------------------
| Border widths https://tailwindcss.com/docs/border-width
|-----------------------------------------------------------------------------
|
| Here is where you define your border widths. Take note that border
| widths require a special "default" value set as well. This is the
| width that will be used when you do not specify a border width.
|
| Class name: .border{-side?}{-width?}
|
*/
borderWidths: {
default: '1px',
'0': '0',
'2': '2px',
'4': '4px',
'8': '8px',
},
/*
|-----------------------------------------------------------------------------
| Border colors https://tailwindcss.com/docs/border-color
|-----------------------------------------------------------------------------
|
| Here is where you define your border colors. By default these use the
| color palette we defined above, however you're welcome to set these
| independently if that makes sense for your project.
|
| Take note that border colors require a special "default" value set
| as well. This is the color that will be used when you do not
| specify a border color.
|
| Class name: .border-{color}
|
*/
borderColors: global.Object.assign({ default: colors['grey-light'] }, colors),
/*
|-----------------------------------------------------------------------------
| Border radius https://tailwindcss.com/docs/border-radius
|-----------------------------------------------------------------------------
|
| Here is where you define your border radius values. If a `default` radius
| is provided, it will be made available as the non-suffixed `.rounded`
| utility.
|
| If your scale includes a `0` value to reset already rounded corners, it's
| a good idea to put it first so other values are able to override it.
|
| Class name: .rounded{-side?}{-size?}
|
*/
borderRadius: {
'none': '0',
'sm': '.125rem',
default: '.25rem',
'lg': '.5rem',
'full': '9999px',
},
/*
|-----------------------------------------------------------------------------
| Width https://tailwindcss.com/docs/width
|-----------------------------------------------------------------------------
|
| Here is where you define your width utility sizes. These can be
| percentage based, pixels, rems, or any other units. By default
| we provide a sensible rem based numeric scale, a percentage
| based fraction scale, plus some other common use-cases. You
| can, of course, modify these values as needed.
|
|
| It's also worth mentioning that Tailwind automatically escapes
| invalid CSS class name characters, which allows you to have
| awesome classes like .w-2/3.
|
| Class name: .w-{size}
|
*/
width: {
'auto': 'auto',
'px': '1px',
'1': '0.25rem',
'2': '0.5rem',
'3': '0.75rem',
'4': '1rem',
'5': '1.25rem',
'6': '1.5rem',
'8': '2rem',
'10': '2.5rem',
'12': '3rem',
'16': '4rem',
'24': '6rem',
'32': '8rem',
'48': '12rem',
'64': '16rem',
'1/2': '50%',
'1/3': '33.33333%',
'2/3': '66.66667%',
'1/4': '25%',
'3/4': '75%',
'1/5': '20%',
'2/5': '40%',
'3/5': '60%',
'4/5': '80%',
'1/6': '16.66667%',
'5/6': '83.33333%',
'full': '100%',
'screen': '100vw'
},
/*
|-----------------------------------------------------------------------------
| Height https://tailwindcss.com/docs/height
|-----------------------------------------------------------------------------
|
| Here is where you define your height utility sizes. These can be
| percentage based, pixels, rems, or any other units. By default
| we provide a sensible rem based numeric scale plus some other
| common use-cases. You can, of course, modify these values as
| needed.
|
| Class name: .h-{size}
|
*/
height: {
'auto': 'auto',
'px': '1px',
'1': '0.25rem',
'2': '0.5rem',
'3': '0.75rem',
'4': '1rem',
'5': '1.25rem',
'6': '1.5rem',
'8': '2rem',
'10': '2.5rem',
'12': '3rem',
'16': '4rem',
'24': '6rem',
'32': '8rem',
'48': '12rem',
'64': '16rem',
'full': '100%',
'screen': '100vh'
},
/*
|-----------------------------------------------------------------------------
| Minimum width https://tailwindcss.com/docs/min-width
|-----------------------------------------------------------------------------
|
| Here is where you define your minimum width utility sizes. These can
| be percentage based, pixels, rems, or any other units. We provide a
| couple common use-cases by default. You can, of course, modify
| these values as needed.
|
| Class name: .min-w-{size}
|
*/
minWidth: {
'0': '0',
'full': '100%',
},
/*
|-----------------------------------------------------------------------------
| Minimum height https://tailwindcss.com/docs/min-height
|-----------------------------------------------------------------------------
|
| Here is where you define your minimum height utility sizes. These can
| be percentage based, pixels, rems, or any other units. We provide a
| few common use-cases by default. You can, of course, modify these
| values as needed.
|
| Class name: .min-h-{size}
|
*/
minHeight: {
'0': '0',
'full': '100%',
'screen': '100vh'
},
/*
|-----------------------------------------------------------------------------
| Maximum width https://tailwindcss.com/docs/max-width
|-----------------------------------------------------------------------------
|
| Here is where you define your maximum width utility sizes. These can
| be percentage based, pixels, rems, or any other units. By default
| we provide a sensible rem based scale and a "full width" size,
| which is basically a reset utility. You can, of course,
| modify these values as needed.
|
| Class name: .max-w-{size}
|
*/
maxWidth: {
'xs': '20rem',
'sm': '30rem',
'md': '40rem',
'lg': '50rem',
'xl': '60rem',
'2xl': '70rem',
'3xl': '80rem',
'4xl': '90rem',
'5xl': '100rem',
'full': '100%',
},
/*
|-----------------------------------------------------------------------------
| Maximum height https://tailwindcss.com/docs/max-height
|-----------------------------------------------------------------------------
|
| Here is where you define your maximum height utility sizes. These can
| be percentage based, pixels, rems, or any other units. We provide a
| couple common use-cases by default. You can, of course, modify
| these values as needed.
|
| Class name: .max-h-{size}
|
*/
maxHeight: {
'full': '100%',
'screen': '100vh',
},
/*
|-----------------------------------------------------------------------------
| Padding https://tailwindcss.com/docs/padding
|-----------------------------------------------------------------------------
|
| Here is where you define your padding utility sizes. These can be
| percentage based, pixels, rems, or any other units. By default we
| provide a sensible rem based numeric scale plus a couple other
| common use-cases like "1px". You can, of course, modify these
| values as needed.
|
| Class name: .p{side?}-{size}
|
*/
padding: {
'px': '1px',
'0': '0',
'1': '0.25rem',
'2': '0.5rem',
'3': '0.75rem',
'4': '1rem',
'5': '1.25rem',
'6': '1.5rem',
'8': '2rem',
'10': '2.5rem',
'12': '3rem',
'16': '4rem',
'20': '5rem',
'24': '6rem',
'32': '8rem',
},
/*
|-----------------------------------------------------------------------------
| Margin https://tailwindcss.com/docs/margin
|-----------------------------------------------------------------------------
|
| Here is where you define your margin utility sizes. These can be
| percentage based, pixels, rems, or any other units. By default we
| provide a sensible rem based numeric scale plus a couple other
| common use-cases like "1px". You can, of course, modify these
| values as needed.
|
| Class name: .m{side?}-{size}
|
*/
margin: {
'auto': 'auto',
'px': '1px',
'0': '0',
'1': '0.25rem',
'2': '0.5rem',
'3': '0.75rem',
'4': '1rem',
'5': '1.25rem',
'6': '1.5rem',
'8': '2rem',
'10': '2.5rem',
'12': '3rem',
'16': '4rem',
'20': '5rem',
'24': '6rem',
'32': '8rem',
},
/*
|-----------------------------------------------------------------------------
| Negative margin https://tailwindcss.com/docs/negative-margin
|-----------------------------------------------------------------------------
|
| Here is where you define your negative margin utility sizes. These can
| be percentage based, pixels, rems, or any other units. By default we
| provide matching values to the padding scale since these utilities
| generally get used together. You can, of course, modify these
| values as needed.
|
| Class name: .-m{side?}-{size}
|
*/
negativeMargin: {
'px': '1px',
'0': '0',
'1': '0.25rem',
'2': '0.5rem',
'3': '0.75rem',
'4': '1rem',
'5': '1.25rem',
'6': '1.5rem',
'8': '2rem',
'10': '2.5rem',
'12': '3rem',
'16': '4rem',
'20': '5rem',
'24': '6rem',
'32': '8rem',
},
/*
|-----------------------------------------------------------------------------
| Shadows https://tailwindcss.com/docs/shadows
|-----------------------------------------------------------------------------
|
| Here is where you define your shadow utilities. As you can see from
| the defaults we provide, it's possible to apply multiple shadows
| per utility using comma separation.
|
| If a `default` shadow is provided, it will be made available as the non-
| suffixed `.shadow` utility.
|
| Class name: .shadow-{size?}
|
*/
shadows: {
default: '0 2px 4px 0 rgba(0,0,0,0.10)',
'md': '0 4px 8px 0 rgba(0,0,0,0.12), 0 2px 4px 0 rgba(0,0,0,0.08)',
'lg': '0 15px 30px 0 rgba(0,0,0,0.11), 0 5px 15px 0 rgba(0,0,0,0.08)',
'inner': 'inset 0 2px 4px 0 rgba(0,0,0,0.06)',
'outline': '0 0 0 3px rgba(52,144,220,0.5)',
'none': 'none',
},
/*
|-----------------------------------------------------------------------------
| Z-index https://tailwindcss.com/docs/z-index
|-----------------------------------------------------------------------------
|
| Here is where you define your z-index utility values. By default we
| provide a sensible numeric scale. You can, of course, modify these
| values as needed.
|
| Class name: .z-{index}
|
*/
zIndex: {
'auto': 'auto',
'0': 0,
'10': 10,
'20': 20,
'30': 30,
'40': 40,
'50': 50,
},
/*
|-----------------------------------------------------------------------------
| Opacity https://tailwindcss.com/docs/opacity
|-----------------------------------------------------------------------------
|
| Here is where you define your opacity utility values. By default we
| provide a sensible numeric scale. You can, of course, modify these
| values as needed.
|
| Class name: .opacity-{name}
|
*/
opacity: {
'0': '0',
'25': '.25',
'50': '.5',
'75': '.75',
'100': '1',
},
/*
|-----------------------------------------------------------------------------
| SVG fill https://tailwindcss.com/docs/svg
|-----------------------------------------------------------------------------
|
| Here is where you define your SVG fill colors. By default we just provide
| `fill-current` which sets the fill to the current text color. This lets you
| specify a fill color using existing text color utilities and helps keep the
| generated CSS file size down.
|
| Class name: .fill-{name}
|
*/
svgFill: {
'current': 'currentColor',
},
/*
|-----------------------------------------------------------------------------
| SVG stroke https://tailwindcss.com/docs/svg
|-----------------------------------------------------------------------------
|
| Here is where you define your SVG stroke colors. By default we just provide
| `stroke-current` which sets the stroke to the current text color. This lets
| you specify a stroke color using existing text color utilities and helps
| keep the generated CSS file size down.
|
| Class name: .stroke-{name}
|
*/
svgStroke: {
'current': 'currentColor',
},
/*
|-----------------------------------------------------------------------------
| Modules https://tailwindcss.com/docs/configuration#modules
|-----------------------------------------------------------------------------
|
| Here is where you control which modules are generated and what variants are
| generated for each of those modules.
|
| Currently supported variants:
| - responsive
| - hover
| - focus
| - active
| - group-hover
|
| To disable a module completely, use `false` instead of an array.
|
*/
modules: {
appearance: ['responsive'],
backgroundAttachment: ['responsive'],
backgroundColors: ['responsive', 'hover', 'focus'],
backgroundPosition: ['responsive'],
backgroundRepeat: ['responsive'],
backgroundSize: ['responsive'],
borderCollapse: [],
borderColors: ['responsive', 'hover', 'focus'],
borderRadius: ['responsive'],
borderStyle: ['responsive'],
borderWidths: ['responsive'],
cursor: ['responsive'],
display: ['responsive'],
flexbox: ['responsive'],
float: ['responsive'],
fonts: ['responsive'],
fontWeights: ['responsive', 'hover', 'focus'],
height: ['responsive'],
leading: ['responsive'],
lists: ['responsive'],
margin: ['responsive'],
maxHeight: ['responsive'],
maxWidth: ['responsive'],
minHeight: ['responsive'],
minWidth: ['responsive'],
negativeMargin: ['responsive'],
opacity: ['responsive'],
outline: ['focus'],
overflow: ['responsive'],
padding: ['responsive'],
pointerEvents: ['responsive'],
position: ['responsive'],
resize: ['responsive'],
shadows: ['responsive', 'hover', 'focus'],
svgFill: [],
svgStroke: [],
tableLayout: ['responsive'],
textAlign: ['responsive'],
textColors: ['responsive', 'hover', 'focus'],
textSizes: ['responsive'],
textStyle: ['responsive', 'hover', 'focus'],
tracking: ['responsive'],
userSelect: ['responsive'],
verticalAlign: ['responsive'],
visibility: ['responsive'],
whitespace: ['responsive'],
width: ['responsive'],
zIndex: ['responsive'],
},
/*
|-----------------------------------------------------------------------------
| Plugins https://tailwindcss.com/docs/plugins
|-----------------------------------------------------------------------------
|
| Here is where you can register any plugins you'd like to use in your
| project. Tailwind's built-in `container` plugin is enabled by default to
| give you a Bootstrap-style responsive container component out of the box.
|
| Be sure to view the complete plugin documentation to learn more about how
| the plugin system works.
|
*/
plugins: [
require('tailwindcss/plugins/container')({
// center: true,
// padding: '1rem',
}),
],
/*
|-----------------------------------------------------------------------------
| Advanced Options https://tailwindcss.com/docs/configuration#options
|-----------------------------------------------------------------------------
|
| Here is where you can tweak advanced configuration options. We recommend
| leaving these options alone unless you absolutely need to change them.
|
*/
options: {
prefix: '',
important: false,
separator: ':',
},
}

View File

@@ -0,0 +1,375 @@
<?php
namespace JoeDixon\Translation\Tests;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Support\Facades\Event;
use JoeDixon\Translation\Drivers\Translation;
use JoeDixon\Translation\Events\TranslationAdded;
use JoeDixon\Translation\Exceptions\LanguageExistsException;
use JoeDixon\Translation\Language;
use JoeDixon\Translation\Translation as TranslationModel;
use JoeDixon\Translation\TranslationBindingsServiceProvider;
use JoeDixon\Translation\TranslationServiceProvider;
use Orchestra\Testbench\TestCase;
class DatabaseDriverTest extends TestCase
{
use DatabaseMigrations;
private $translation;
/**
* Setup the test environment.
*/
public function setUp(): void
{
parent::setUp();
$this->withFactories(__DIR__.'/../database/factories');
$this->translation = $this->app[Translation::class];
}
protected function getEnvironmentSetUp($app)
{
$app['config']->set('translation.driver', 'database');
$app['config']->set('database.default', 'testing');
$app['config']->set('database.connections.testing', [
'driver' => 'sqlite',
'database' => ':memory:',
]);
}
protected function getPackageProviders($app)
{
return [
TranslationServiceProvider::class,
TranslationBindingsServiceProvider::class,
];
}
/** @test */
public function it_returns_all_languages()
{
$newLanguages = factory(Language::class, 2)->create();
$newLanguages = $newLanguages->mapWithKeys(function ($language) {
return [$language->language => $language->name];
})->toArray();
$languages = $this->translation->allLanguages();
$this->assertEquals($languages->count(), 3);
$this->assertEquals($languages->toArray(), ['en' => 'en'] + $newLanguages);
}
/** @test */
public function it_returns_all_translations()
{
$default = Language::where('language', config('app.locale'))->first();
factory(Language::class)->create(['language' => 'es', 'name' => 'Español']);
factory(TranslationModel::class)->states('group')->create(['language_id' => $default->id, 'group' => 'test', 'key' => 'hello', 'value' => 'Hello']);
factory(TranslationModel::class)->states('group')->create(['language_id' => $default->id, 'group' => 'test', 'key' => 'whats_up', 'value' => "What's up!"]);
factory(TranslationModel::class)->states('single')->create(['language_id' => $default->id, 'group' => 'single', 'key' => 'Hello', 'value' => 'Hello']);
factory(TranslationModel::class)->states('single')->create(['language_id' => $default->id, 'group' => 'single', 'key' => "What's up", 'value' => "What's up!"]);
$translations = $this->translation->allTranslations();
$this->assertEquals($translations->count(), 2);
$this->assertEquals(['single' => ['single' => ['Hello' => 'Hello', "What's up" => "What's up!"]], 'group' => ['test' => ['hello' => 'Hello', 'whats_up' => "What's up!"]]], $translations->toArray()['en']);
$this->assertArrayHasKey('en', $translations->toArray());
$this->assertArrayHasKey('es', $translations->toArray());
}
/** @test */
public function it_returns_all_translations_for_a_given_language()
{
$default = Language::where('language', config('app.locale'))->first();
factory(TranslationModel::class)->states('group')->create(['language_id' => $default->id, 'group' => 'test', 'key' => 'hello', 'value' => 'Hello']);
factory(TranslationModel::class)->states('group')->create(['language_id' => $default->id, 'group' => 'test', 'key' => 'whats_up', 'value' => "What's up!"]);
factory(TranslationModel::class)->states('single')->create(['language_id' => $default->id, 'group' => 'single', 'key' => 'Hello', 'value' => 'Hello']);
factory(TranslationModel::class)->states('single')->create(['language_id' => $default->id, 'group' => 'single', 'key' => "What's up", 'value' => "What's up!"]);
$translations = $this->translation->allTranslationsFor('en');
$this->assertEquals($translations->count(), 2);
$this->assertEquals(['single' => ['single' => ['Hello' => 'Hello', "What's up" => "What's up!"]], 'group' => ['test' => ['hello' => 'Hello', 'whats_up' => "What's up!"]]], $translations->toArray());
$this->assertArrayHasKey('single', $translations->toArray());
$this->assertArrayHasKey('group', $translations->toArray());
}
/** @test */
public function it_throws_an_exception_if_a_language_exists()
{
$this->expectException(LanguageExistsException::class);
$this->translation->addLanguage('en');
}
/** @test */
public function it_can_add_a_new_language()
{
$this->assertDatabaseMissing(config('translation.database.languages_table'), [
'language' => 'fr',
'name' => 'Français',
]);
$this->translation->addLanguage('fr', 'Français');
$this->assertDatabaseHas(config('translation.database.languages_table'), [
'language' => 'fr',
'name' => 'Français',
]);
}
/** @test */
public function it_can_add_a_new_translation_to_a_new_group()
{
$this->translation->addGroupTranslation('es', 'test', 'hello', 'Hola!');
$translations = $this->translation->allTranslationsFor('es');
$this->assertEquals(['test' => ['hello' => 'Hola!']], $translations->toArray()['group']);
}
/** @test */
public function it_can_add_a_new_translation_to_an_existing_translation_group()
{
$translation = factory(TranslationModel::class)->create();
$this->translation->addGroupTranslation($translation->language->language, "{$translation->group}", 'test', 'Testing');
$translations = $this->translation->allTranslationsFor($translation->language->language);
$this->assertSame([$translation->group => [$translation->key => $translation->value, 'test' => 'Testing']], $translations->toArray()['group']);
}
/** @test */
public function it_can_add_a_new_single_translation()
{
$this->translation->addSingleTranslation('es', 'single', 'Hello', 'Hola!');
$translations = $this->translation->allTranslationsFor('es');
$this->assertEquals(['single' => ['Hello' => 'Hola!']], $translations->toArray()['single']);
}
/** @test */
public function it_can_add_a_new_single_translation_to_an_existing_language()
{
$translation = factory(TranslationModel::class)->states('single')->create();
$this->translation->addSingleTranslation($translation->language->language, 'single', 'Test', 'Testing');
$translations = $this->translation->allTranslationsFor($translation->language->language);
$this->assertEquals(['single' => ['Test' => 'Testing', $translation->key => $translation->value]], $translations->toArray()['single']);
}
/** @test */
public function it_can_get_a_collection_of_group_names_for_a_given_language()
{
$language = factory(Language::class)->create(['language' => 'en']);
factory(TranslationModel::class)->create([
'language_id' => $language->id,
'group' => 'test',
]);
$groups = $this->translation->getGroupsFor('en');
$this->assertEquals($groups->toArray(), ['test']);
}
/** @test */
public function it_can_merge_a_language_with_the_base_language()
{
$default = Language::where('language', config('app.locale'))->first();
factory(TranslationModel::class)->states('group')->create(['language_id' => $default->id, 'group' => 'test', 'key' => 'hello', 'value' => 'Hello']);
factory(TranslationModel::class)->states('group')->create(['language_id' => $default->id, 'group' => 'test', 'key' => 'whats_up', 'value' => "What's up!"]);
factory(TranslationModel::class)->states('single')->create(['language_id' => $default->id, 'group' => 'single', 'key' => 'Hello', 'value' => 'Hello']);
factory(TranslationModel::class)->states('single')->create(['language_id' => $default->id, 'group' => 'single', 'key' => "What's up", 'value' => "What's up!"]);
$this->translation->addGroupTranslation('es', 'test', 'hello', 'Hola!');
$translations = $this->translation->getSourceLanguageTranslationsWith('es');
$this->assertEquals($translations->toArray(), [
'group' => [
'test' => [
'hello' => ['en' => 'Hello', 'es' => 'Hola!'],
'whats_up' => ['en' => "What's up!", 'es' => ''],
],
],
'single' => [
'single' => [
'Hello' => [
'en' => 'Hello',
'es' => '',
],
"What's up" => [
'en' => "What's up!",
'es' => '',
],
],
],
]);
}
/** @test */
public function it_can_add_a_vendor_namespaced_translations()
{
$this->translation->addGroupTranslation('es', 'translation_test::test', 'hello', 'Hola!');
$this->assertEquals($this->translation->allTranslationsFor('es')->toArray(), [
'group' => [
'translation_test::test' => [
'hello' => 'Hola!',
],
],
'single' => [],
]);
}
/** @test */
public function it_can_add_a_nested_translation()
{
$this->translation->addGroupTranslation('en', 'test', 'test.nested', 'Nested!');
$this->assertEquals($this->translation->getGroupTranslationsFor('en')->toArray(), [
'test' => [
'test.nested' => 'Nested!',
],
]);
}
/** @test */
public function it_can_add_nested_vendor_namespaced_translations()
{
$this->translation->addGroupTranslation('es', 'translation_test::test', 'nested.hello', 'Hola!');
$this->assertEquals($this->translation->allTranslationsFor('es')->toArray(), [
'group' => [
'translation_test::test' => [
'nested.hello' => 'Hola!',
],
],
'single' => [],
]);
}
/** @test */
public function it_can_merge_a_namespaced_language_with_the_base_language()
{
$this->translation->addGroupTranslation('en', 'translation_test::test', 'hello', 'Hello');
$this->translation->addGroupTranslation('es', 'translation_test::test', 'hello', 'Hola!');
$translations = $this->translation->getSourceLanguageTranslationsWith('es');
$this->assertEquals($translations->toArray(), [
'group' => [
'translation_test::test' => [
'hello' => ['en' => 'Hello', 'es' => 'Hola!'],
],
],
'single' => [],
]);
}
/** @test */
public function a_list_of_languages_can_be_viewed()
{
$newLanguages = factory(Language::class, 2)->create();
$response = $this->get(config('translation.ui_url'));
$response->assertSee(config('app.locale'));
foreach ($newLanguages as $language) {
$response->assertSee($language->language);
}
}
/** @test */
public function the_language_creation_page_can_be_viewed()
{
$this->translation->addGroupTranslation(config('app.locale'), 'translation::translation', 'add_language', 'Add a new language');
$this->get(config('translation.ui_url').'/create')
->assertSee('Add a new language');
}
/** @test */
public function a_language_can_be_added()
{
$this->post(config('translation.ui_url'), ['locale' => 'de'])
->assertRedirect();
$this->assertDatabaseHas('languages', ['language' => 'de']);
}
/** @test */
public function a_list_of_translations_can_be_viewed()
{
$default = Language::where('language', config('app.locale'))->first();
factory(TranslationModel::class)->states('group')->create(['language_id' => $default->id, 'group' => 'test', 'key' => 'hello', 'value' => 'Hello']);
factory(TranslationModel::class)->states('group')->create(['language_id' => $default->id, 'group' => 'test', 'key' => 'whats_up', 'value' => "What's up!"]);
factory(TranslationModel::class)->states('single')->create(['language_id' => $default->id, 'key' => 'Hello', 'value' => 'Hello!']);
factory(TranslationModel::class)->states('single')->create(['language_id' => $default->id, 'key' => "What's up", 'value' => 'Sup!']);
$this->get(config('translation.ui_url').'/en/translations')
->assertSee('hello')
->assertSee('whats_up')
->assertSee('Hello')
->assertSee('Sup!');
}
/** @test */
public function the_translation_creation_page_can_be_viewed()
{
$this->translation->addGroupTranslation('en', 'translation::translation', 'add_translation', 'Add a translation');
$this->get(config('translation.ui_url').'/'.config('app.locale').'/translations/create')
->assertSee('Add a translation');
}
/** @test */
public function a_new_translation_can_be_added()
{
$this->post(config('translation.ui_url').'/'.config('app.locale').'/translations', ['group' => 'single', 'key' => 'joe', 'value' => 'is cool'])
->assertRedirect();
$this->assertDatabaseHas('translations', ['language_id' => 1, 'key' => 'joe', 'value' => 'is cool']);
}
/** @test */
public function a_translation_can_be_updated()
{
$default = Language::where('language', config('app.locale'))->first();
factory(TranslationModel::class)->states('group')->create(['language_id' => $default->id, 'group' => 'test', 'key' => 'hello', 'value' => 'Hello']);
$this->assertDatabaseHas('translations', ['language_id' => 1, 'group' => 'test', 'key' => 'hello', 'value' => 'Hello']);
$this->post(config('translation.ui_url').'/en', ['group' => 'test', 'key' => 'hello', 'value' => 'Hello there!'])
->assertStatus(200);
$this->assertDatabaseHas('translations', ['language_id' => 1, 'group' => 'test', 'key' => 'hello', 'value' => 'Hello there!']);
}
/** @test */
public function adding_a_translation_fires_an_event_with_the_expected_data()
{
Event::fake();
$data = ['key' => 'joe', 'value' => 'is cool'];
$this->post(config('translation.ui_url').'/en/translations', $data);
Event::assertDispatched(TranslationAdded::class, function ($event) use ($data) {
return $event->language === 'en' &&
$event->group === 'single' &&
$event->value === $data['value'] &&
$event->key === $data['key'];
});
}
/** @test */
public function updating_a_translation_fires_an_event_with_the_expected_data()
{
Event::fake();
$data = ['group' => 'test', 'key' => 'hello', 'value' => 'Hello there!'];
$this->post(config('translation.ui_url').'/en/translations', $data);
Event::assertDispatched(TranslationAdded::class, function ($event) use ($data) {
return $event->language === 'en' &&
$event->group === $data['group'] &&
$event->value === $data['value'] &&
$event->key === $data['key'];
});
}
}

View File

@@ -0,0 +1,380 @@
<?php
namespace JoeDixon\Translation\Tests;
use Illuminate\Support\Facades\Event;
use JoeDixon\Translation\Drivers\Translation;
use JoeDixon\Translation\Events\TranslationAdded;
use JoeDixon\Translation\Exceptions\LanguageExistsException;
use JoeDixon\Translation\TranslationBindingsServiceProvider;
use JoeDixon\Translation\TranslationServiceProvider;
use Orchestra\Testbench\TestCase;
class FileDriverTest extends TestCase
{
private $translation;
/**
* Setup the test environment.
*/
protected function setUp(): void
{
parent::setUp();
app()['path.lang'] = __DIR__.'/fixtures/lang';
$this->translation = app()->make(Translation::class);
}
protected function getPackageProviders($app)
{
return [
TranslationServiceProvider::class,
TranslationBindingsServiceProvider::class,
];
}
protected function getEnvironmentSetUp($app)
{
$app['config']->set('translation.driver', 'file');
}
/** @test */
public function it_returns_all_languages()
{
$languages = $this->translation->allLanguages();
$this->assertEquals($languages->count(), 2);
$this->assertEquals($languages->toArray(), ['en' => 'en', 'es' => 'es']);
}
/** @test */
public function it_returns_all_translations()
{
$translations = $this->translation->allTranslations();
$this->assertEquals($translations->count(), 2);
$this->assertEquals(['single' => ['single' => ['Hello' => 'Hello', "What's up" => "What's up!"]], 'group' => ['test' => ['hello' => 'Hello', 'whats_up' => "What's up!"]]], $translations->toArray()['en']);
$this->assertArrayHasKey('en', $translations->toArray());
$this->assertArrayHasKey('es', $translations->toArray());
}
/** @test */
public function it_returns_all_translations_for_a_given_language()
{
$translations = $this->translation->allTranslationsFor('en');
$this->assertEquals($translations->count(), 2);
$this->assertEquals(['single' => ['single' => ['Hello' => 'Hello', "What's up" => "What's up!"]], 'group' => ['test' => ['hello' => 'Hello', 'whats_up' => "What's up!"]]], $translations->toArray());
$this->assertArrayHasKey('single', $translations->toArray());
$this->assertArrayHasKey('group', $translations->toArray());
}
/** @test */
public function it_throws_an_exception_if_a_language_exists()
{
$this->expectException(LanguageExistsException::class);
$this->translation->addLanguage('en');
}
/** @test */
public function it_can_add_a_new_language()
{
$this->translation->addLanguage('fr');
$this->assertTrue(file_exists(__DIR__.'/fixtures/lang/fr.json'));
$this->assertTrue(file_exists(__DIR__.'/fixtures/lang/fr'));
rmdir(__DIR__.'/fixtures/lang/fr');
unlink(__DIR__.'/fixtures/lang/fr.json');
}
/** @test */
public function it_can_add_a_new_translation_to_a_new_group()
{
$this->translation->addGroupTranslation('es', 'test', 'hello', 'Hola!');
$translations = $this->translation->allTranslationsFor('es');
$this->assertEquals(['test' => ['hello' => 'Hola!']], $translations->toArray()['group']);
unlink(__DIR__.'/fixtures/lang/es/test.php');
}
/** @test */
public function it_can_add_a_new_translation_to_an_existing_translation_group()
{
$this->translation->addGroupTranslation('en', 'test', 'test', 'Testing');
$translations = $this->translation->allTranslationsFor('en');
$this->assertEquals(['test' => ['hello' => 'Hello', 'whats_up' => 'What\'s up!', 'test' => 'Testing']], $translations->toArray()['group']);
file_put_contents(
app()['path.lang'].'/en/test.php',
"<?php\n\nreturn ".var_export(['hello' => 'Hello', 'whats_up' => 'What\'s up!'], true).';'.\PHP_EOL
);
}
/** @test */
public function it_can_add_a_new_single_translation()
{
$this->translation->addSingleTranslation('es', 'single', 'Hello', 'Hola!');
$translations = $this->translation->allTranslationsFor('es');
$this->assertEquals(['single' => ['Hello' => 'Hola!']], $translations->toArray()['single']);
unlink(__DIR__.'/fixtures/lang/es.json');
}
/** @test */
public function it_can_add_a_new_single_translation_to_an_existing_language()
{
$this->translation->addSingleTranslation('en', 'single', 'Test', 'Testing');
$translations = $this->translation->allTranslationsFor('en');
$this->assertEquals(['single' => ['Hello' => 'Hello', 'What\'s up' => 'What\'s up!', 'Test' => 'Testing']], $translations->toArray()['single']);
file_put_contents(
app()['path.lang'].'/en.json',
json_encode((object) ['Hello' => 'Hello', 'What\'s up' => 'What\'s up!'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
);
}
/** @test */
public function it_can_get_a_collection_of_group_names_for_a_given_language()
{
$groups = $this->translation->getGroupsFor('en');
$this->assertEquals($groups->toArray(), ['test']);
}
/** @test */
public function it_can_merge_a_language_with_the_base_language()
{
$this->translation->addGroupTranslation('es', 'test', 'hello', 'Hola!');
$translations = $this->translation->getSourceLanguageTranslationsWith('es');
$this->assertEquals($translations->toArray(), [
'group' => [
'test' => [
'hello' => ['en' => 'Hello', 'es' => 'Hola!'],
'whats_up' => ['en' => "What's up!", 'es' => ''],
],
],
'single' => [
'single' => [
'Hello' => [
'en' => 'Hello',
'es' => '',
],
"What's up" => [
'en' => "What's up!",
'es' => '',
],
],
],
]);
unlink(__DIR__.'/fixtures/lang/es/test.php');
}
/** @test */
public function it_can_add_a_vendor_namespaced_translations()
{
$this->translation->addGroupTranslation('es', 'translation_test::test', 'hello', 'Hola!');
$this->assertEquals($this->translation->allTranslationsFor('es')->toArray(), [
'group' => [
'translation_test::test' => [
'hello' => 'Hola!',
],
],
'single' => [],
]);
\File::deleteDirectory(__DIR__.'/fixtures/lang/vendor');
}
/** @test */
public function it_can_add_a_nested_translation()
{
$this->translation->addGroupTranslation('en', 'test', 'test.nested', 'Nested!');
$this->assertEquals($this->translation->getGroupTranslationsFor('en')->toArray(), [
'test' => [
'hello' => 'Hello',
'test.nested' => 'Nested!',
'whats_up' => 'What\'s up!',
],
]);
file_put_contents(
app()['path.lang'].'/en/test.php',
"<?php\n\nreturn ".var_export(['hello' => 'Hello', 'whats_up' => 'What\'s up!'], true).';'.\PHP_EOL
);
}
/** @test */
public function it_can_add_nested_vendor_namespaced_translations()
{
$this->translation->addGroupTranslation('es', 'translation_test::test', 'nested.hello', 'Hola!');
$this->assertEquals($this->translation->allTranslationsFor('es')->toArray(), [
'group' => [
'translation_test::test' => [
'nested.hello' => 'Hola!',
],
],
'single' => [],
]);
\File::deleteDirectory(__DIR__.'/fixtures/lang/vendor');
}
/** @test */
public function it_can_merge_a_namespaced_language_with_the_base_language()
{
$this->translation->addGroupTranslation('en', 'translation_test::test', 'hello', 'Hello');
$this->translation->addGroupTranslation('es', 'translation_test::test', 'hello', 'Hola!');
$translations = $this->translation->getSourceLanguageTranslationsWith('es');
$this->assertEquals($translations->toArray(), [
'group' => [
'test' => [
'hello' => ['en' => 'Hello', 'es' => ''],
'whats_up' => ['en' => "What's up!", 'es' => ''],
],
'translation_test::test' => [
'hello' => ['en' => 'Hello', 'es' => 'Hola!'],
],
],
'single' => [
'single' => [
'Hello' => [
'en' => 'Hello',
'es' => '',
],
"What's up" => [
'en' => "What's up!",
'es' => '',
],
],
],
]);
\File::deleteDirectory(__DIR__.'/fixtures/lang/vendor');
}
/** @test */
public function a_list_of_languages_can_be_viewed()
{
$this->get(config('translation.ui_url'))
->assertSee('en');
}
/** @test */
public function the_language_creation_page_can_be_viewed()
{
$this->get(config('translation.ui_url').'/create')
->assertSee('Add a new language');
}
/** @test */
public function a_language_can_be_added()
{
$this->post(config('translation.ui_url'), ['locale' => 'de'])
->assertRedirect();
$this->assertTrue(file_exists(__DIR__.'/fixtures/lang/de.json'));
$this->assertTrue(file_exists(__DIR__.'/fixtures/lang/de'));
rmdir(__DIR__.'/fixtures/lang/de');
unlink(__DIR__.'/fixtures/lang/de.json');
}
/** @test */
public function a_list_of_translations_can_be_viewed()
{
$this->get(config('translation.ui_url').'/en/translations')
->assertSee('hello')
->assertSee('whats_up');
}
/** @test */
public function the_translation_creation_page_can_be_viewed()
{
$this->get(config('translation.ui_url').'/'.config('app.locale').'/translations/create')
->assertSee('Add a translation');
}
/** @test */
public function a_new_translation_can_be_added()
{
$this->post(config('translation.ui_url').'/en/translations', ['key' => 'joe', 'value' => 'is cool'])
->assertRedirect();
$translations = $this->translation->getSingleTranslationsFor('en');
$this->assertEquals(['Hello' => 'Hello', 'What\'s up' => 'What\'s up!', 'joe' => 'is cool'], $translations->toArray()['single']);
file_put_contents(
app()['path.lang'].'/en.json',
json_encode((object) ['Hello' => 'Hello', 'What\'s up' => 'What\'s up!'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
);
}
/** @test */
public function a_translation_can_be_updated()
{
$this->post(config('translation.ui_url').'/en', ['group' => 'test', 'key' => 'hello', 'value' => 'Hello there!'])
->assertStatus(200);
$translations = $this->translation->getGroupTranslationsFor('en');
$this->assertEquals(['hello' => 'Hello there!', 'whats_up' => 'What\'s up!'], $translations->toArray()['test']);
file_put_contents(
app()['path.lang'].'/en/test.php',
"<?php\n\nreturn ".var_export(['hello' => 'Hello', 'whats_up' => 'What\'s up!'], true).';'.\PHP_EOL
);
}
/** @test */
public function adding_a_translation_fires_an_event_with_the_expected_data()
{
Event::fake();
$data = ['key' => 'joe', 'value' => 'is cool'];
$this->post(config('translation.ui_url').'/en/translations', $data);
Event::assertDispatched(TranslationAdded::class, function ($event) use ($data) {
return $event->language === 'en' &&
$event->group === 'single' &&
$event->value === $data['value'] &&
$event->key === $data['key'];
});
file_put_contents(
app()['path.lang'].'/en.json',
json_encode((object) ['Hello' => 'Hello', 'What\'s up' => 'What\'s up!'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
);
}
/** @test */
public function updating_a_translation_fires_an_event_with_the_expected_data()
{
Event::fake();
$data = ['group' => 'test', 'key' => 'hello', 'value' => 'Hello there!'];
$this->post(config('translation.ui_url').'/en/translations', $data);
Event::assertDispatched(TranslationAdded::class, function ($event) use ($data) {
return $event->language === 'en' &&
$event->group === $data['group'] &&
$event->value === $data['value'] &&
$event->key === $data['key'];
});
file_put_contents(
app()['path.lang'].'/en/test.php',
"<?php\n\nreturn ".var_export(['hello' => 'Hello', 'whats_up' => 'What\'s up!'], true).';'.\PHP_EOL
);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace JoeDixon\Translation\Tests;
use JoeDixon\Translation\TranslationBindingsServiceProvider;
use JoeDixon\Translation\TranslationServiceProvider;
use Orchestra\Testbench\TestCase;
class PackageIsLoadedTest extends TestCase
{
protected function getPackageProviders($app)
{
return [
TranslationServiceProvider::class,
TranslationBindingsServiceProvider::class,
];
}
/** @test */
public function the_translation_pacakage_is_loaded()
{
$this->assertArrayHasKey(TranslationServiceProvider::class, app()->getLoadedProviders());
$this->assertArrayHasKey(TranslationBindingsServiceProvider::class, app()->getLoadedProviders());
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace JoeDixon\Translation\Tests;
use JoeDixon\Translation\Scanner;
use JoeDixon\Translation\TranslationBindingsServiceProvider;
use JoeDixon\Translation\TranslationServiceProvider;
use Orchestra\Testbench\TestCase;
class ScannerTest extends TestCase
{
private $scanner;
protected function getPackageProviders($app)
{
return [
TranslationServiceProvider::class,
TranslationBindingsServiceProvider::class,
];
}
protected function getEnvironmentSetUp($app)
{
$app['config']->set('translation.scan_paths', __DIR__.'/fixtures/scan-tests');
$app['config']->set('translation.translation_methods', ['__', 'trans', 'trans_choice', '@lang', 'Lang::get']);
}
/** @test */
public function it_finds_all_translations()
{
$this->scanner = app()->make(Scanner::class);
$matches = $this->scanner->findTranslations();
$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' => '']]]);
$this->assertCount(2, $matches);
}
}

View File

@@ -0,0 +1,4 @@
{
"Hello": "Hello",
"What's up": "What's up!"
}

View File

@@ -0,0 +1,6 @@
<?php
return array (
'hello' => 'Hello',
'whats_up' => 'What\'s up!',
);

View File

@@ -0,0 +1,6 @@
__('This will go in the JSON array')
__(
'This will also go in the JSON array'
)

View File

@@ -0,0 +1,2 @@
@lang('lang.first_match')

View File

@@ -0,0 +1,3 @@
Lang::get('lang_get.first')
Lang::get('lang_get.second');

View File

@@ -0,0 +1,4 @@
trans('trans.first_match');
trans('trans');
trans('trans.third_match');

View File

@@ -0,0 +1,2 @@
trans_choice('trans_choice.with_params', ['parameters' => 'Go here'])

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

View File

@@ -0,0 +1,58 @@
let mix = require('laravel-mix');
var tailwindcss = require('tailwindcss');
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel application. By default, we are compiling the Sass
| file for your application, as well as bundling up your JS files.
|
*/
mix.setPublicPath('public/assets');
// mix.setPublicPath('../../../public/vendor/translation');
mix.postCss('resources/assets/css/main.css', 'css', [
tailwindcss('./tailwind.js'),
]).js('resources/assets/js/app.js', 'js')
// Full API
// mix.js(src, output);
// mix.react(src, output); <-- Identical to mix.js(), but registers React Babel compilation.
// mix.preact(src, output); <-- Identical to mix.js(), but registers Preact compilation.
// mix.coffee(src, output); <-- Identical to mix.js(), but registers CoffeeScript compilation.
// mix.ts(src, output); <-- TypeScript support. Requires tsconfig.json to exist in the same folder as webpack.mix.js
// mix.extract(vendorLibs);
// mix.sass(src, output);
// mix.standaloneSass('src', output); <-- Faster, but isolated from Webpack.
// mix.fastSass('src', output); <-- Alias for mix.standaloneSass().
// mix.less(src, output);
// mix.stylus(src, output);
// mix.postCss(src, output, [require('postcss-some-plugin')()]);
// mix.browserSync('my-site.test');
// mix.combine(files, destination);
// mix.babel(files, destination); <-- Identical to mix.combine(), but also includes Babel compilation.
// mix.copy(from, to);
// mix.copyDirectory(fromDir, toDir);
// mix.minify(file);
// mix.sourceMaps(); // Enable sourcemaps
// mix.version(); // Enable versioning.
// mix.disableNotifications();
// mix.setPublicPath('path/to/public');
// mix.setResourceRoot('prefix/for/resource/locators');
// mix.autoload({}); <-- Will be passed to Webpack's ProvidePlugin.
// mix.webpackConfig({}); <-- Override webpack.config.js, without editing the file directly.
// mix.babelConfig({}); <-- Merge extra Babel configuration (plugins, etc.) with Mix's default.
// mix.then(function () {}) <-- Will be triggered each time Webpack finishes building.
// mix.extend(name, handler) <-- Extend Mix's API with your own components.
// mix.options({
// extractVueStyles: false, // Extract .vue component styling to file, rather than inline.
// globalVueStyles: file, // Variables file to be imported in every component.
// processCssUrls: true, // Process/optimize relative stylesheet url()'s. Set to false, if you don't want them touched.
// purifyCss: false, // Remove unused CSS selectors.
// uglify: {}, // Uglify-specific options. https://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin
// postCss: [] // Post-CSS options: https://github.com/postcss/postcss/blob/master/docs/plugins.md
// });