Введение

Laravel - сам по себе классный фреймворк PHP. У него есть свои плюсы и минусы. У меня в компании используется laravel почти на всех проектах компании. В большинстве случаях в качестве административной панели используется laravel nova. И всё бы ничего если бы на одном из проектов, заказчик не захотел локализацию всей админки для модераторов из разных стран, с возможностью добавления языков.

laravel nova icon
laravel nova icon

Основные проблемы

Задавшись этой сложной задачей передо мной встало несколько проблем:

  • Добавления языков

  • Хранения данных полей админ панели

  • Переключатель языков

  • И последняя проблема появившиеся в конце всего процесса - СЕССИИ Laravel nova.

Добавление языков

Самое простое в локализации laravel nova это хранение языков. Так мне необходимо было создать хранилище языков с возможностью добавления. Я создал таблицу для хранения и ресурс nova.

 public function up()
    {
        Schema::create('languages', function (Blueprint $table) {
            $table->id();
            $table->string('key_lang', 255);
            $table->string('title', 255);
            $table->timestamps();
        });
    }
//миграция таблицы языков
public function fields(Request $request)
    {
        return [
            ID::make(__('ID'), 'id')->sortable(),

            Text::make(__("LanguageKey"), 'key_lang')
                ->rules('required')
                ->sortable(),

            Text::make(__("Title"), 'title')
                ->rules('required')
                ->sortable()
        ];
    }
//ресурс nova языков

Хранения данных полей админ панели

Так все основные поля nova хранятся в директории resources/lang/vendor/nova в json файлах имеющих в качестве названия ключ языка. Мне пришла идея создать новую таблицу для хранения данных локализации и создать observer который будет брать существующий файл json laravel и дополнять его информацией при сохранении с таблицы.

Для того чтобы хранить данные и в БД, я использовал json поле. Таким образом миграция приняла следующий вид:

 public function up()
    {
        Schema::create('admin_panels', function (Blueprint $table) {
            $table->id();
            $table->foreignId('key_lang_id')->unique()->constrained('languages');
            $table->json('content');
            $table->timestamps();
        });
    }
    //таблица хранения полей для административной панели

Для того чтобы для каждого поля не писать в ресурсе для каждого необходимого поля свое поле для отображения, я решил собрать все необходимые мне ключи локализации в константу массива, с генерировать текстовые поля в массиве и поместить их в поле armincms/json (https://github.com/armincms/json).

protected static $adminField = [
     'LoggingProfileChanges', 'ChangedByWhom', 'ListOfChanges' ...... // Все ключи локализации 
];

public function fields(Request $request)
    {
        foreach (self::$adminField as $field) {
            $res[] = Text::make(__($field), $field)
                ->rules('required')
                ->hideFromIndex();
        }
        $result = [
            ID::make(__('ID'), 'id')->sortable(),

            BelongsTo::make(__("LanguageKey"), 'key_lang', Language::class)
                ->searchable(true)
                ->creationRules('unique:admin_panels,key_lang_id')
                ->updateRules('unique:admin_panels,key_lang_id,{{resourceId}}')
                ->sortable(),

            Json::make("content", $res)
        ];
        return $result;
    }

После того как всё это проделано оставалось настроить сохранение в json файлы nova используя observer. Написав рекурсивную замену значений json файла я добился успеха.

public function created(AdminPanel $adminPanel)
    {
        $data = $adminPanel->getAttributes();
        $lang = Language::find($data['key_lang_id']);
        $path = resource_path('/lang/vendor/nova/'.$lang->key_lang.'.json');
        if (file_exists($path)) {
            $file = file_get_contents($path);
            $original = json_decode($file, True);
            $original = array_replace_recursive($original, json_decode($data['content'], True));
            $handle = fopen($path, 'w+');
            fputs($handle, json_encode($original));
            fclose($handle);
        } else {
            $handle = fopen($path, 'w+');
            fputs($handle, $data['content']);
            fclose($handle);
        }
    }
    
    public function updated(AdminPanel $adminPanel)
    {
        $idLang = $adminPanel->getOriginal('key_lang_id');
        $data = $adminPanel->getChanges();
        if (isset($data['key_lang_id'])) {
            $lang = Language::find($data['key_lang_id']);
        } else {
            $lang = Language::find($idLang);
        }
        $path = resource_path('/lang/vendor/nova/'.$lang->key_lang.'.json');
        $file = file_get_contents($path);
        $original = json_decode($file, True);
        $original = array_replace_recursive($original, json_decode($data['content'], True));
        $handle = fopen($path, 'w+');
        fputs($handle, json_encode($original));
        fclose($handle);
    }

Сессии и переключатель языков

Подключение переключателя языков по началу казалось лёгкой задачей так, как я думал можно взять готовый переключаетель с laravel nova packages, но я ошибался. В большестве случаев, либо они не работали на текущей версии laravel nova(v3.23.2 ), либо сталкивался с проблемой того что языки брались из конфига, и даже при попытке добавить туда язык нужно было чистить кэш, что мне вовсе не подходило так как всё должно происходить автоматически.

В конечном итиоге пришёл к выводу что нужно делать свой переключатель. Сделав его и залив на сервер, появилась главная проблема - отсутсвие сессий в laravel nova. И тогда я уже думал что это провал. До демо перед заказкчиком было 3 дня. Пытаясь найти решение на протяжении всего дня, пришел к выводу что нужно как то прикручивать сессии вручную. Для меня это было сложной задачей так опыта в написании на laravel меньше пол года.

Посидев над проблемой два дня мне всё таки удалось прикрутить сессии и настроить шаблон resources/views/vendor/nova/partials/user.blade.php

user.blade.php

<dropdown-trigger class="h-9 flex items-center">
    @isset($user->email)
        <img
            src="https://secure.gravatar.com/avatar/{{ md5(\Illuminate\Support\Str::lower($user->email)) }}?size=512"
            class="rounded-full w-8 h-8 mr-3"
        />
    @endisset

    <span class="text-90">
        {{ $user->name ?? $user->email ?? __('Nova User') }}
    </span>
</dropdown-trigger>

<dropdown-menu slot="menu" width="200" direction="rtl">
        <ul class="list-reset">
        @foreach (App\Http\Controllers\LanguageController::getLangs() as $lang => $language)
            @if ($lang != Illuminate\Support\Facades\App::getLocale())
                <li>
                <a class="block no-underline text-90 hover:bg-30 p-3" href="{{ route('lang.switch', $lang) }}"> {{$language}}</a>
                </li>
            @endif
        @endforeach
        </ul>
    <ul class="list-reset">
        <li>
            <a href="{{ route('nova.logout') }}" class="block no-underline text-90 hover:bg-30 p-3">
                {{ __('Logout') }}
            </a>
        </li>
    </ul>
</dropdown-menu>

LanguageController.php

 public function switchLang($lang)
    {
        if (array_key_exists($lang, self::getLangs())) {
            Session::put('applocale', $lang);
        }
        return Redirect::back();
    }

    public static function getLangs()
    {
        $result = [];
        $allLang = Language::all();

        foreach ($allLang as $lang) {
            $result[$lang->key_lang] = $lang->title;
        }

        return $result;
    }

LanguageMiddleware.php

 public function handle($request, Closure $next)
    {
        if (Session()->has('applocale') AND array_key_exists(Session()->get('applocale'), LanguageController::getLangs())) {
            App::setLocale(Session()->get('applocale'));
        }
        else { 
            App::setLocale(config('app.fallback_locale'));
        }
        return $next($request);
    }

В конце нужно добавить класс в Kernel:

 protected $middlewareGroups = [
        'web' => [
           ...
            \App\Http\Middleware\Language::class,
        ],
					...
    ];

Так же можно это использовать заменив получение языков из контроллера, на получение языков из конфига.

Итог

В конечном результате у меня вышел функционал, с добавлением языков и переводов в административную панель и переключением языков админки с сессиями.

Комментарии (2)


  1. mrBarabas
    09.09.2021 21:30

    Никогда не имел дело с Нова, но проблемы запустить сессию, в мидлваре брать из базы список языков и делать на его основании форму с селектом для выбора языка и по сабмиту этой формы менять язык в отдельном роуте через сетЛокаль (собственно примерно это у Вас в статье есть по кусочкам) и как это в Нове нет сессии, если она на основании лары работает, которая по умолчанию запускает сессию и как вы тогда аутентифицирветесь, если у Вас нет сессии?


    1. firason Автор
      10.09.2021 12:51

      У laravel nova есть сессия аутентификации. Но когда я пытался взять готовые библиотеки, при смене языка у одного пользователя язык административной панели менялся у всех модераторов. Почему так происходило я не могу сказать точно. Но всё заработало когда я вручную начал запускать сессии самой laravel.