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,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();
});
}
}