Instead of creating multiple tables and handling translations via a different table, this package uses a single table and a JSON column to store the translations.
Installation
Installation guide for this package is really simple and consists only of two steps:
Require the package via composer:
composer require spatie/laravel-translatable
And for the models you want to translate add the Spatie\Translatable\HasTranslations
trait with $translatable
property:
Model
use Spatie\Translatable\HasTranslations; class Post extends Model{ use HasTranslations; public $translatable = ['title'];}
That is it! Now if you set up the database column title
to be a JSON column (or TEXT in unsupported databases), you can start using the package.
Usage
Here's a quick example of how we used this package:
Migration
Schema::create('posts', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained(); $table->dateTime('publish_date')->nullable(); $table->json('title'); // <--- JSON column for title $table->json('post'); // <--- JSON column for post $table->softDeletes(); $table->timestamps();});
app/Models/Post.php
use Spatie\Translatable\HasTranslations; // ... class Post extends Model{ use SoftDeletes; use HasTranslations; public $translatable = ['title', 'post']; protected $fillable = [ 'user_id', 'publish_date', 'title', 'post' ];}
app/Http/Controllers/PostController.php
use App\Models\Post;use App\Models\User;use Illuminate\Http\RedirectResponse;use Illuminate\Http\Request; class PostController extends Controller{ public function index() { $posts = Post::all(); return view('posts.index', compact('posts')); } public function create() { $authors = User::pluck('name', 'id'); return view('posts.create', compact('authors')); } public function store(Request $request): RedirectResponse { $rules = [ 'publish_date' => ['nullable', 'date'], 'author_id' => ['required', 'numeric'], ]; // Adding validation for each available locale foreach (config('app.supportedLocales') as $locale) { $rules += [ 'title.' . $locale => ['required', 'string'], 'post.' . $locale => ['required', 'string'], ]; } $this->validate($request, $rules); $post = Post::create([ 'user_id' => $request->input('author_id'), 'publish_date' => $request->input('publish_date'), 'title' => $request->input('title'), // <-- This will be an array of translations 'post' => $request->input('post'), // <-- This will be an array of translations ]); return redirect()->route('posts.index'); } public function edit(Post $post) { $authors = User::pluck('name', 'id'); return view('posts.edit', compact('post', 'authors')); } public function update(Request $request, Post $post): RedirectResponse { $rules = [ 'publish_date' => ['nullable', 'date'], 'author_id' => ['required', 'numeric'], ]; // Adding validation for each available locale foreach (config('app.supportedLocales') as $locale) { $rules += [ 'title.' . $locale => ['required', 'string'], 'post.' . $locale => ['required', 'string'], ]; } $this->validate($request, $rules); $post->update([ 'user_id' => $request->input('author_id'), 'publish_date' => $request->input('publish_date'), 'title' => $request->input('title'), // <-- This will be an array of translations 'post' => $request->input('post'), // <-- This will be an array of translations ]); return redirect()->route('posts.index'); } public function destroy(Post $post): RedirectResponse { $post->delete(); return redirect()->route('posts.index'); }}
And finally the views:
resources/views/posts/index.blade.php
<table class="w-full"> <thead> <tr> <th>ID</th> <th>Title</th> <th>Excerpt</th> <th>Published at</th> <th>Actions</th> </tr> </thead> <tbody> @foreach($posts as $post) <tr> <td>{{ $post->id }}</td> <td>{{ $post->title }}</td> {{-- As you can see, we get just the title. The package handles the rest. --}} <td>{{ Str::of($post->post)->limit() }}</td> {{-- As you can see, we get just the post. The package handles the rest. --}} <td>{{ $post->publish_date ?? 'Unpublished' }}</td> <td> {{-- ... --}} </td> </tr> @endforeach </tbody></table>
As you see - we didn't have to specify which language we want to display as default. It is done by the package itself!
On create we will make an array of translations for each field:
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> @foreach(config('app.supportedLocales') as $locale) {{-- Looping through all available locales to create an array for `title` and `post` fields with locales --}} <fieldset class="border-2 w-full p-4 rounded-lg mb-4"> <label>Text for {{ $locale }}</label> <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) }}"> @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) }}</textarea> @error('post.'.$locale) <div class="text-red-500 mt-2 text-sm"> {{ $message }} </div> @enderror </div> </fieldset> @endforeach <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>
The same for editing, but we will have to load specific translations for each field:
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> @foreach(config('app.supportedLocales') as $locale) {{-- Looping through all available locales to create an array for `title` and `post` fields with locales --}} <fieldset class="border-2 w-full p-4 rounded-lg mb-4"> <label>Text for {{ $locale }}</label> <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->getTranslation('title', $locale)) }}"> @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->getTranslation('post', $locale)) }}</textarea> @error('post.'.$locale) <div class="text-red-500 mt-2 text-sm"> {{ $message }} </div> @enderror </div> </fieldset> @endforeach <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>
In edit, you will see $post->getTranslation('title', $locale)
and $post->getTranslation('post', $locale)
. This is how we get the translation for a specific locale. We can also use $post->title
and $post->post
but this will return the translation for the current locale which is not good if we want to edit all the locales.
Repository: https://github.com/LaravelDaily/laravel11-localization-course/tree/lesson/modelPackages/spatie
I want add thing if want add more than languages so you need add tab like this ex: @foreach (config('locales.languages') as $key => $val)
Meta-Bereich
namespace App\Translators;
use GuzzleHttp\Client; use Illuminate\Support\Facades\Cache;
class DeepLTranslator { public function translate($text, $target_lang = 'de') { // dd($text); if (\is_array($text)){ $text = $text['de']; } // Check if the translation is already in the cache $cachedTranslation = Cache::get("translation_{$text}_{$target_lang}"); if ($cachedTranslation) { return $cachedTranslation; }
try{ $response = $client->post('https://api.deepl.com/v2/translate', [ 'headers' => [ 'Content-Type' => 'application/x-www-form-urlencoded', 'User-Agent' => 'name' ], 'form_params' => [ 'auth_key' => env('DEEPL_API_KEY'), 'text' => $text, 'target_lang' => $target_lang ] ]); } catch (\Exception $e) { return $e->getMessage(); }
}
in index blade nothing is shows when applying the package
can you explain what the issue is a bit more?
im trying to use spatie/laravel-translatable to translate model called ProductCategory, in create and edit forms worked fine, but when i tried to display the traslated fileds according to the current local it not showin anything at all. hit: im using quick admin panel generator, so the default generated code for locale inside config/panel.php not in config/app.php so that is the only bit of code i have changed in the above lesson
Have you added:
To your models?
If yes, I would just dump what the models returns or what the database contains.
Model ProductCategory:
use Spatie\Translatable\HasTranslations;
class ProductCategory extends Model implements HasMedia { use SoftDeletes, InteractsWithMedia, Auditable, HasFactory, HasTranslations;
#attributes: array:6 [▼ "id" => 1 "name" => "{"English":"Oil","Arabic":"\u0632\u064a\u0648\u062a"}" "description" => "{"English":"Officiis qui enim al","Arabic":"\u0632\u064a\u0648\u062a"}" "created_at" => "2023-05-28 07:38:31" "updated_at" => "2023-05-28 07:38:31" "deleted_at" => null
It seems that the package works, yet I'm a little bit confused why it has full language names as it should be
en
orar
instead of full names. So please check how you are saving the data :)Fixed it, and here was the problem this is panel.php content return [ 'date_format' => 'Y-m-d', 'time_format' => 'H:i:s', 'primary_language' => 'en', 'available_languages' => [ 'en' => 'English', 'ar' => 'Arabic', ], 'registration_default_role' => '2',
];
i was saveing the value of the locale instead of key @foreach(config('panel.available_languages')) as $locale the correct is: @foreach(config('panel.available_languages')) as $key => $value then use the key instead of using the value.
Thanks a lot of quick responding
Great Team