Back to Course |
Multi-Language Laravel 11: All You Need to Know

Translating Models - With Livewire

In this lesson, we are going to re-create the same scenario as in the previous lesson, but using Livewire to create the form with more dynamic elements.

Our base logic and setup are the same as in the previous lesson, so we will skip that part and start modifying the forms to support a nice user experience.


Installing Livewire

To install Livewire, we need to run the following command:

composer require livewire/livewire

Now we are ready to use Livewire.


Creating Livewire Components

For this example, we'll create a single component that will handle switching languages and form inputs:

php artisan make:livewire PostContentPerLanguage

With the following code on the PHP side:

app/Http/Livewire/PostContentPerLanguage.php

use App\Models\Post;
use Livewire\Component;
 
class PostContentPerLanguage extends Component
{
public ?Post $post;
public string $currentLanguage;
public array $languagesList;
 
public function mount(): void
{
$this->currentLanguage = app()->getLocale();
$this->languagesList = config('app.supportedLocales', []);
}
 
public function render()
{
return view('livewire.post-content-per-language');
}
 
public function changeLocale($locale): void
{
if (in_array($locale, $this->languagesList)) {
$this->currentLanguage = $locale;
}
}
}

And the following code on View:

resources/views/livewire/post-content-per-language.blade.php

<div>
<div class="w-full mb-4">
@foreach($languagesList as $locale)
<button type="button" wire:click="changeLocale('{{ $locale }}')"
@class([
'bg-blue-500 text-white px-4 py-2 rounded font-medium inline' => $locale == $currentLanguage,
'bg-gray-200 text-gray-500 px-4 py-2 rounded font-medium inline' => $locale != $currentLanguage,
])
>
Translations for: {{ $locale }}
</button>
@endforeach
</div>
 
@foreach($languagesList as $locale)
<fieldset @class(['hidden' => $locale != $currentLanguage, 'border-2 w-full p-4 rounded-lg mb-4'])>
<div class="mb-4">
<label for="title[{{$locale}}]" class="sr-only">Title</label>
<input type="text" name="title[{{$locale}}]" id="title[{{$locale}}]"
placeholder="Title"
class="bg-gray-100 border-2 w-full p-4 rounded-lg @error('title') border-red-500 @enderror"
value="{{ old('title.'. $locale, $post?->translations->where('locale', $locale)->first()?->title) }}">
@error('title.'.$locale)
<div class="text-red-500 mt-2 text-sm">
{{ $message }}
</div>
@enderror
</div>
<div class="">
<label for="post[{{$locale}}]" class="sr-only">Body</label>
<textarea name="post[{{$locale}}]" id="post[{{$locale}}]" cols="30" rows="4"
placeholder="Post"
class="bg-gray-100 border-2 w-full p-4 rounded-lg @error('post'.$locale) border-red-500 @enderror">{{ old('post'.$locale, $post?->translations->where('locale', $locale)->first()?->post) }}</textarea>
@error('post.'.$locale)
<div class="text-red-500 mt-2 text-sm">
{{ $message }}
</div>
@enderror
</div>
</fieldset>
@endforeach
</div>

The next step is to modify our form to use this component:

resources/views/posts/create.blade.php

<form action="{{ route('posts.store') }}" method="POST">
@csrf
 
<div class="mb-4">
<label for="author_id" class="sr-only">Author</label>
<select name="author_id" id="author_id"
class="bg-gray-100 border-2 w-full p-4 rounded-lg @error('author_id') border-red-500 @enderror">
<option value="">Select author</option>
@foreach($authors as $id => $name)
<option value="{{ $id }}">{{ $name }}</option>
@endforeach
</select>
@error('author_id')
<div class="text-red-500 mt-2 text-sm">
{{ $message }}
</div>
@enderror
</div>
 
<livewire:post-content-per-language/>
 
<div class="mb-4">
<label for="publish_date" class="sr-only">Published at</label>
<input type="datetime-local" name="publish_date" id="publish_date"
placeholder="Published at"
class="bg-gray-100 border-2 w-full p-4 rounded-lg @error('publish_date') border-red-500 @enderror"
value="{{ old('publish_date') }}">
@error('publish_date')
<div class="text-red-500 mt-2 text-sm">
{{ $message }}
</div>
@enderror
</div>
<div>
<button type="submit" class="bg-blue-500 text-white px-4 py-3 rounded font-medium w-full">
Create
</button>
</div>
</form>

And edit view:

resources/views/posts/edit.blade.php

<form action="{{ route('posts.update', $post->id) }}" method="POST">
@csrf
@method('PUT')
 
<div class="mb-4">
<label for="author_id" class="sr-only">Author</label>
<select name="author_id" id="author_id"
class="bg-gray-100 border-2 w-full p-4 rounded-lg @error('author_id') border-red-500 @enderror">
<option value="">Select author</option>
@foreach($authors as $id => $name)
<option value="{{ $id }}" @selected(old('author_id', $post->user_id) === $id)>{{ $name }}</option>
@endforeach
</select>
@error('author_id')
<div class="text-red-500 mt-2 text-sm">
{{ $message }}
</div>
@enderror
</div>
 
<livewire:post-content-per-language
:post="$post"/>
 
<div class="mb-4">
<label for="publish_date" class="sr-only">Published at</label>
<input type="datetime-local" name="publish_date" id="publish_date"
placeholder="Published at"
class="bg-gray-100 border-2 w-full p-4 rounded-lg @error('publish_date') border-red-500 @enderror"
value="{{ old('publish_date', $post->publish_date) }}">
@error('publish_date')
<div class="text-red-500 mt-2 text-sm">
{{ $message }}
</div>
@enderror
</div>
<div>
<button type="submit" class="bg-blue-500 text-white px-4 py-3 rounded font-medium w-full">
Update
</button>
</div>
</form>

As you can see, we removed the big and ugly foreach loop and replaced it with a single component:

<livewire:post-content-per-language/> {{-- For create form --}}
{{-- And --}}
<livewire:post-content-per-language :post="$post"/> {{-- For edit form --}}

Due to the basic implementation of just the switcher with hidden classes - we don't have to modify our controller at all. We can just use the same code as before!

Opening our create form, we can see that it works as expected:

And the same for the edit form:

While it might not be perfect, it's a great example of how you can use Livewire to create reactive components and handle situations like this.


So, we have looked at how we can add localized models in a simple way, without any packages. However, this might become a bit tedious if we have a lot of Models to translate. In the upcoming lessons, I will show example demos with two packages that may help:

  • spatie/laravel-translatable
  • astrotomic/laravel-translatable

Spatie Laravel Translatable

This package allows you to store your translations in a single database table with a JSON-type column. It will store all languages in a single database column. This is a great way to keep your database clean and simple.

Package Link


Astrotomic Laravel Translatable

This package is different due to its requirement to store translations on a separate database table. It requires more setup initially but provides a lot of flexibility.

Package Link


Repository: https://github.com/LaravelDaily/laravel11-localization-course/tree/lesson/translating-content-livewire