Simple Eloquent Model Translations

Making you models translatable could be an issue, especially if you are running an application that is multilingual. For static texts, we can use the built-in translation engine, but for models, we need to solve a more complex issue. Let’s take a look at a simple yet flexible solution.

Before We Start

If you need a fully covered solution, we suggest using a package. But we believe, it’s the best when you try to solve your problem, even if you won’t use it at the end. Perfect case for practicing, so let’s do it.

The Translation Mechanism

Let’s say we have different models with a different structure. But we want them to be translatable the same way and also, we want the system to translate to models automatically based on the current language.

We assume the user can change the language manually and our app handles the change somehow. Maybe we store the current language in the session and use a middleware to set the language based on the session data. But now we focus on the translation method only, not the language change mechanism.

In other words: we have a default language – in this case, english – and we store the default data in the model itself. For the rest of the languages we want to create translations that represents the model’s structure, but the content is in the proper language.

If the application language changes, we translation which is in the current language (if there is any) gets activated instead of the original model’s content.

The Translations Migration

First of all, let’s create the migration for the model, then we talk about the data types and why we chose them.

Schema::create('translations', function (Blueprint $table) {
    $table->increments('id');
    $table->unsignedInteger('translatable_id')->index();
    $table->string('translatable_type');
    $table->string('language');
    $table->json('content');
    $table->timestamps();
    $table->unique(['translatable_type', 'translatable_id', 'language']);
});
You need MySql 5.7+ to use the JSON data type.

We prepare the polymorphic relationship by adding the translatable_type and the translatable_id to the schema. Also, we add unique constraints to prevent duplications.

And the key point, the content column. As you can see, we use JSON data type for the content. It allows us to keep the content as dynamic as we need. That means we are not restricted to one model structure, we have the flexibility to use the translation model where we want.

The Translation Model

The model itself is very simple, we define the basic things and we are ready to go!

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Translation extends Model
{
    /**
     * The attributes that should be casted to native types.
     *
     * @var array
     */
    protected $casts = [
        'content' => 'array',
    ];

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'content',
        'language',
    ];

    /**
     * Get all of the owning translatable models.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function translatable()
    {
        return $this->morphTo();
    }
}

We define the fillable fields, also we cast the  content attribute to array. We need to cast it, because we don’t want to use it as text but as an array.

Also, we define the polymorphic relationship’s one side.

If you are not familiar with polymorphic relationships, you can check the docs.

The Translatable Trait

Because, we want the translation mechanism to be reusable  we create a trait and put the logic there. If we want to translate the model, we just add the trait and we are ready to go.

In the trait we want to do the following things: define the relationship’s other side, also we want to get the translation as an attribute based on the current language. Let’s see the code:

<?php

namespace App;

use App\Translation;
use Illuminate\Support\Facades\App;

trait Translatable
{
    /**
     * Get all of the models's translations.
     *
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function translations()
    {
        return $this->morphMany(Translation::class, 'translatable');
    }

    /**
     * Get the translation attribute.
     *
     * @return \App\Translation
     */
    public function getTranslationAttribute()
    {
        return $this->translations->firstWhere('language', App::getLocale());
    }
}
Note, the firstWhere() is a quite new feature. You can replace it with where(…)->first(). Also, don’t forget if we don’t have the translation for the given language, the translation attribute will be null.

So we did what we wanted here, let’s see some example how does it work.

Basic Example

Let’s say we have a page model, what has to be multilingual. If a user changes the language, we need to get the correct translation of the page.

The page model has two fields title and body. We save the default content into these fields and everything else is stored in the translations.

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Page extends Model
{
    use Translatable;
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'title',
        'body',
    ];
}
An important note: there are several ways to store your translations. You can set up nested controllers for them or put the translating login anywhere you want. It’s your job to find out.

A translation’s content should look like the following for the page model in any language:

{
    title: 'Foreign Title',
    body: 'Foreign Body'
}

As you can see, we can cast this JSON structure to an array and use it easily from our blade templates.

<!-- The title -->
{{ $page->translation->content['title'] ?? $page->title }}

<!-- The content -->
{{ $page->translation->content['body'] ?? $page->body }}

If there is no translation attribute (maybe because we have not translated it yet or the current language is english) we show the original content. Every other case we show the current translation.

Summary

This is a very simple way to use translations with your models. We did not represent many things that should be included to such a feature like this. We just wanted to show the basics of a possible solution.

As we said at the beginning, if you need a more complex solution you may use a package. But if you are okey with a simpler yet flexible way, we hope you can use some parts of this post.

As an extra, we created a simple package with some neat features. You can find the repo here: https://github.com/thepinecode/translatable