first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View 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);
}
}

View 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');
}
}

View File

@@ -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']]);
}
}
}

View 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;
}
}

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

View 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);
}
}

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