176
common/Localizations/Commands/ExportTranslations.php
Executable file
176
common/Localizations/Commands/ExportTranslations.php
Executable file
@@ -0,0 +1,176 @@
|
||||
<?php namespace Common\Localizations\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use Common\Auth\Permissions\Permission;
|
||||
use Common\Core\Values\ValueLists;
|
||||
use Error;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
use SplFileInfo;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class ExportTranslations extends Command
|
||||
{
|
||||
protected $signature = 'translations:export';
|
||||
|
||||
protected $description = 'Export default laravel translations as flattened json file.';
|
||||
|
||||
public function __construct(protected Filesystem $fs)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$messages = array_merge(
|
||||
$this->getLaravelTranslationMessages(),
|
||||
$this->getDefaultMenuLabels(),
|
||||
$this->getPermissionNamesAndDescriptions(),
|
||||
);
|
||||
|
||||
$messages = array_filter(
|
||||
$messages,
|
||||
fn($value, $key) => $value && $key,
|
||||
ARRAY_FILTER_USE_BOTH,
|
||||
);
|
||||
|
||||
$this->fs->put(
|
||||
resource_path('server-translations.json'),
|
||||
json_encode($messages),
|
||||
);
|
||||
|
||||
$this->info('Translation lines exported as json.');
|
||||
}
|
||||
|
||||
protected function getLaravelTranslationMessages()
|
||||
{
|
||||
$files = collect(
|
||||
(new Finder())
|
||||
->in([
|
||||
base_path('app'),
|
||||
base_path('common'),
|
||||
resource_path('views'),
|
||||
resource_path('lang/en'),
|
||||
base_path('vendor/laravel'),
|
||||
])
|
||||
->name(['*.php'])
|
||||
->files(),
|
||||
);
|
||||
|
||||
$lines = $files
|
||||
->map(function (SplFileInfo $file) {
|
||||
$functions = ['__', 'trans', '@lang', 'Lang::get'];
|
||||
$lines = [];
|
||||
$contents = $file->getContents();
|
||||
|
||||
if (Str::contains($contents, 'extends BaseFormRequest')) {
|
||||
$lines = array_merge(
|
||||
$lines,
|
||||
$this->getCustomValidationMessages($file),
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($functions as $function) {
|
||||
if (
|
||||
preg_match_all(
|
||||
'/(' .
|
||||
$function .
|
||||
')\([\r\n\s]{0,}\h*[\'"](.+)[\'"]\h*[\r\n\s]{0,}[),]/U',
|
||||
$file->getContents(),
|
||||
$matches,
|
||||
)
|
||||
) {
|
||||
$lines[] = $matches[2];
|
||||
}
|
||||
}
|
||||
|
||||
return $lines;
|
||||
})
|
||||
->flatten()
|
||||
->map(fn(string $string) => stripslashes($string))
|
||||
->unique()
|
||||
->filter(function (string $string) {
|
||||
// ignore laravel short translation keys
|
||||
// (pagination.next for example) and only use json keys
|
||||
return !preg_match('/^[^.\s]\S*\.\S*[^.\s]$/', $string);
|
||||
});
|
||||
|
||||
return $lines->combine($lines)->toArray();
|
||||
}
|
||||
|
||||
private function getDefaultMenuLabels(): array
|
||||
{
|
||||
$menus = Arr::first(
|
||||
config('common.default-settings'),
|
||||
fn($setting) => $setting['name'] === 'menus',
|
||||
);
|
||||
|
||||
if ($menus) {
|
||||
return collect(json_decode($menus['value'], true))
|
||||
->pluck('items.*.label')
|
||||
->flatten()
|
||||
->mapWithKeys(fn($key) => [$key => $key])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom validation messages from Laravel Request files.
|
||||
*/
|
||||
private function getCustomValidationMessages(SplFileInfo $file): array
|
||||
{
|
||||
//make namespace from file path
|
||||
$namespace = str_replace(
|
||||
[base_path() . DIRECTORY_SEPARATOR, '.php'],
|
||||
'',
|
||||
$file->getPathname(),
|
||||
);
|
||||
$namespace = ucfirst(str_replace('/', '\\', $namespace));
|
||||
|
||||
$messages = [];
|
||||
try {
|
||||
foreach ((new $namespace())->messages() as $message) {
|
||||
$messages[$message] = $message;
|
||||
}
|
||||
} catch (Error $e) {
|
||||
//
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
private function getPermissionNamesAndDescriptions(): array
|
||||
{
|
||||
Auth::login(User::findAdmin());
|
||||
$lines = app(ValueLists::class)
|
||||
->permissions()
|
||||
->map(function (Permission $permission) {
|
||||
$restrictionLines = $permission->restrictions
|
||||
->map(function ($restriction) {
|
||||
return [
|
||||
ucfirst(str_replace('_', ' ', $restriction['name'])),
|
||||
$restriction['description'],
|
||||
];
|
||||
})
|
||||
->flatten()
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
$permission['display_name'],
|
||||
$permission['group'],
|
||||
$permission['description'],
|
||||
...$restrictionLines,
|
||||
];
|
||||
})
|
||||
->flatten(1)
|
||||
->unique()
|
||||
->toArray();
|
||||
|
||||
return array_combine($lines, $lines);
|
||||
}
|
||||
}
|
||||
65
common/Localizations/Commands/GenerateFooTranslations.php
Executable file
65
common/Localizations/Commands/GenerateFooTranslations.php
Executable file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Localizations\Commands;
|
||||
|
||||
use Common\Localizations\LocalizationsRepository;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class GenerateFooTranslations extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'translations:foo';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (
|
||||
$existing = app(LocalizationsRepository::class)->getByNameOrCode(
|
||||
'foo',
|
||||
)
|
||||
) {
|
||||
app(LocalizationsRepository::class)->delete($existing->id);
|
||||
}
|
||||
$localization = app(LocalizationsRepository::class)->create([
|
||||
'name' => 'Foo',
|
||||
'language' => 'foo',
|
||||
]);
|
||||
$localization->loadLines();
|
||||
|
||||
$translatedLines = [];
|
||||
$count = 1;
|
||||
foreach ($localization->lines as $key => $line) {
|
||||
$translatedLines[$key] = "Foo Bar $count";
|
||||
$count++;
|
||||
}
|
||||
|
||||
app(LocalizationsRepository::class)->update($localization['id'], [
|
||||
'lines' => $translatedLines,
|
||||
]);
|
||||
|
||||
$this->info('Localization created');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Localizations\Listeners;
|
||||
|
||||
use App\Models\User;
|
||||
use Common\Settings\Events\SettingsSaved;
|
||||
|
||||
class UpdateAllUsersLanguageWhenDefaultLocaleChanges
|
||||
{
|
||||
/**
|
||||
* @param SettingsSaved $event
|
||||
*/
|
||||
public function handle(SettingsSaved $event)
|
||||
{
|
||||
$settings = $event->envSettings;
|
||||
// change language for all users to new default locale as well
|
||||
if (array_key_exists('app_locale', $settings) && config('app.locale') !== $settings['app_locale']) {
|
||||
app(User::class)->where('language', '!=', $settings['app_locale'])
|
||||
->update(['language' => $settings['app_locale']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
58
common/Localizations/Localization.php
Executable file
58
common/Localizations/Localization.php
Executable file
@@ -0,0 +1,58 @@
|
||||
<?php namespace Common\Localizations;
|
||||
|
||||
use Common\Core\BaseModel;
|
||||
|
||||
class Localization extends BaseModel
|
||||
{
|
||||
const MODEL_TYPE = 'localization';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
public function loadLines()
|
||||
{
|
||||
if (!$this->exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
$path = $this->getLinesFilePath();
|
||||
if (file_exists($path)) {
|
||||
$this->lines = json_decode(file_get_contents($path), true);
|
||||
}
|
||||
}
|
||||
|
||||
public function getLinesFilePath(): string
|
||||
{
|
||||
return resource_path("lang/$this->language.json");
|
||||
}
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'language' => $this->language,
|
||||
'created_at' => $this->created_at->timestamp ?? '_null',
|
||||
'updated_at' => $this->updated_at->timestamp ?? '_null',
|
||||
];
|
||||
}
|
||||
|
||||
public static function filterableFields(): array
|
||||
{
|
||||
return ['id', 'created_at', 'updated_at'];
|
||||
}
|
||||
|
||||
public function toNormalizedArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->language,
|
||||
'model_type' => self::MODEL_TYPE,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getModelTypeAttribute(): string
|
||||
{
|
||||
return self::MODEL_TYPE;
|
||||
}
|
||||
}
|
||||
117
common/Localizations/LocalizationsController.php
Executable file
117
common/Localizations/LocalizationsController.php
Executable file
@@ -0,0 +1,117 @@
|
||||
<?php namespace Common\Localizations;
|
||||
|
||||
use Common\Core\BaseController;
|
||||
use Common\Database\Datasource\Datasource;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LocalizationsController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
protected Request $request,
|
||||
protected LocalizationsRepository $repository,
|
||||
) {
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$this->authorize('index', Localization::class);
|
||||
|
||||
$dataSource = new Datasource(
|
||||
app(Localization::class)->newQuery(),
|
||||
request()->all(),
|
||||
);
|
||||
|
||||
return $this->success(['pagination' => $dataSource->paginate()]);
|
||||
}
|
||||
|
||||
public function show(string|int $idOrLangCode)
|
||||
{
|
||||
$localization = Localization::where('id', $idOrLangCode)
|
||||
->orWhere('language', $idOrLangCode)
|
||||
->firstOrFail();
|
||||
$this->authorize('show', $localization);
|
||||
|
||||
$localization->loadLines();
|
||||
|
||||
return $this->success([
|
||||
'localization' => $localization,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(int $id)
|
||||
{
|
||||
$this->authorize('update', Localization::class);
|
||||
|
||||
$this->validate($this->request, [
|
||||
'name' => 'string|min:1',
|
||||
'language' => 'string|min:2|max:5',
|
||||
'lines' => 'array|min:1',
|
||||
]);
|
||||
|
||||
$localization = $this->repository->update(
|
||||
$id,
|
||||
$this->request->all(),
|
||||
true,
|
||||
);
|
||||
return $this->success(['localization' => $localization]);
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
$this->authorize('store', Localization::class);
|
||||
|
||||
$this->validate($this->request, [
|
||||
'name' => 'required|unique:localizations',
|
||||
'language' => 'string|min:2|max:5|unique:localizations',
|
||||
]);
|
||||
|
||||
$localization = $this->repository->create($this->request->all());
|
||||
return $this->success(['localization' => $localization]);
|
||||
}
|
||||
|
||||
public function destroy(string $ids)
|
||||
{
|
||||
$localizationIds = explode(',', $ids);
|
||||
|
||||
$this->authorize('destroy', Localization::class);
|
||||
|
||||
foreach ($localizationIds as $id) {
|
||||
if (Localization::count() === 1) {
|
||||
return $this->error(
|
||||
__('There must be at least one localization.'),
|
||||
);
|
||||
}
|
||||
$this->repository->delete((int) $id);
|
||||
}
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
public function download(int $id)
|
||||
{
|
||||
$localization = Localization::findOrFail($id);
|
||||
|
||||
$this->authorize('show', $localization);
|
||||
|
||||
return response()->download($localization->getLinesFilePath());
|
||||
}
|
||||
|
||||
public function upload(int $id)
|
||||
{
|
||||
$localization = Localization::findOrFail($id);
|
||||
|
||||
$this->authorize('update', $localization);
|
||||
|
||||
$data = $this->validate($this->request, [
|
||||
'file' => 'required|file|mimes:json',
|
||||
]);
|
||||
|
||||
$this->repository->storeLocalizationLines(
|
||||
$localization,
|
||||
json_decode(file_get_contents($data['file']->getRealPath()), true),
|
||||
true,
|
||||
);
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
156
common/Localizations/LocalizationsRepository.php
Executable file
156
common/Localizations/LocalizationsRepository.php
Executable file
@@ -0,0 +1,156 @@
|
||||
<?php namespace Common\Localizations;
|
||||
|
||||
use Arr;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
|
||||
class LocalizationsRepository
|
||||
{
|
||||
/**
|
||||
* Path to files with default localization language lines.
|
||||
*/
|
||||
const DEFAULT_TRANS_PATHS = [
|
||||
'client-translations.json',
|
||||
'server-translations.json',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected Filesystem $fs,
|
||||
protected Localization $localization,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getByNameOrCode(
|
||||
string $name,
|
||||
bool $loadLines = true,
|
||||
): ?Localization {
|
||||
$localization = $this->localization
|
||||
->where('name', $name)
|
||||
->orWhere('language', $name)
|
||||
->first();
|
||||
if (!$localization) {
|
||||
return null;
|
||||
}
|
||||
if ($loadLines) {
|
||||
$localization->loadLines();
|
||||
}
|
||||
return $localization;
|
||||
}
|
||||
|
||||
public function update(
|
||||
int $id,
|
||||
array $data,
|
||||
$overrideLines = false,
|
||||
): ?Localization {
|
||||
$localization = Localization::findOrFail($id);
|
||||
$localization->updated_at = now();
|
||||
$language = Arr::get($data, 'language');
|
||||
|
||||
if (isset($data['name']) && $data['name'] !== $localization->name) {
|
||||
$localization->name = $data['name'];
|
||||
}
|
||||
|
||||
if ($language && $language !== $localization->language) {
|
||||
$this->renameLocalizationLinesFile($localization, $language);
|
||||
$localization->language = $language;
|
||||
}
|
||||
|
||||
if (isset($data['lines']) && $data['lines']) {
|
||||
$this->storeLocalizationLines(
|
||||
$localization,
|
||||
$data['lines'],
|
||||
$overrideLines,
|
||||
);
|
||||
}
|
||||
|
||||
$localization->save();
|
||||
|
||||
return $localization;
|
||||
}
|
||||
|
||||
public function create(array $params): ?Localization
|
||||
{
|
||||
$localization = $this->localization->create([
|
||||
'name' => $params['name'],
|
||||
'language' => $params['language'],
|
||||
]);
|
||||
|
||||
$lines = $this->getDefaultTranslationLines();
|
||||
$this->storeLocalizationLines($localization, $lines);
|
||||
|
||||
return $localization;
|
||||
}
|
||||
|
||||
public function delete(int $id): bool
|
||||
{
|
||||
$localization = $this->localization->findOrFail($id);
|
||||
|
||||
$this->fs->delete($this->makeLocalizationLinesPath($localization));
|
||||
|
||||
return $localization->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default translations lines for the application.
|
||||
*/
|
||||
public function getDefaultTranslationLines(): array
|
||||
{
|
||||
$combined = [];
|
||||
|
||||
foreach (self::DEFAULT_TRANS_PATHS as $path) {
|
||||
if (!$this->fs->exists(resource_path($path))) {
|
||||
continue;
|
||||
}
|
||||
$combined = array_merge(
|
||||
$combined,
|
||||
json_decode($this->fs->get(resource_path($path)), true),
|
||||
);
|
||||
}
|
||||
|
||||
return $combined;
|
||||
}
|
||||
|
||||
public function storeLocalizationLines(
|
||||
Localization $localization,
|
||||
$newLines,
|
||||
$override = false,
|
||||
) {
|
||||
$path = $this->makeLocalizationLinesPath($localization);
|
||||
$oldLines = [];
|
||||
|
||||
if (!$override && file_exists($path)) {
|
||||
$oldLines = json_decode(file_get_contents($path), true);
|
||||
}
|
||||
|
||||
$merged = array_merge($oldLines, $newLines);
|
||||
|
||||
return file_put_contents(
|
||||
$path,
|
||||
json_encode($merged, JSON_UNESCAPED_UNICODE),
|
||||
);
|
||||
}
|
||||
|
||||
public function getLocalizationLines(Localization $localization): array
|
||||
{
|
||||
$path = $this->makeLocalizationLinesPath($localization);
|
||||
|
||||
if (file_exists($path)) {
|
||||
return json_decode(file_get_contents($path), true);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public function makeLocalizationLinesPath(Localization $localization): string
|
||||
{
|
||||
return resource_path("lang/$localization->language.json");
|
||||
}
|
||||
|
||||
public function renameLocalizationLinesFile(
|
||||
Localization $localization,
|
||||
string $newLangCode,
|
||||
): bool {
|
||||
$oldPath = $this->makeLocalizationLinesPath($localization);
|
||||
$newPath = resource_path("lang/$newLangCode.json");
|
||||
return $this->fs->move($oldPath, $newPath);
|
||||
}
|
||||
}
|
||||
41
common/Localizations/UserLocaleController.php
Executable file
41
common/Localizations/UserLocaleController.php
Executable file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Localizations;
|
||||
|
||||
use Common\Core\BaseController;
|
||||
|
||||
class UserLocaleController extends BaseController
|
||||
{
|
||||
const COOKIE_NAME = 'selected_locale';
|
||||
|
||||
public function update()
|
||||
{
|
||||
$localeCode = request()->get('locale');
|
||||
if (!$localeCode) {
|
||||
return $this->error(__('Locale code is required'));
|
||||
}
|
||||
|
||||
if ($user = request()->user()) {
|
||||
$user->fill(['language' => $localeCode])->save();
|
||||
} else {
|
||||
cookie()->queue(
|
||||
self::COOKIE_NAME,
|
||||
$localeCode,
|
||||
1260,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
$locale = app(LocalizationsRepository::class)->getByNameOrCode(
|
||||
$localeCode,
|
||||
);
|
||||
|
||||
return $this->success([
|
||||
'locale' => $locale,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user