How To Add Multilingual Support to Eloquent


September 7th, 2015

How To Add Multilingual Support to Eloquent

Did you know that several countries throughout the world have more than one official language? For example, my home country Belgium has three; French, Dutch, and German. Most Belgians also speak English.

Laravel’s Eloquent ORM is a very powerful part of Laravel. Unfortunately, it doesn’t provide support for multilingual models out of the box. Adding that functionality yourself isn’t that difficult.

First let’s take a look at a migration with no multilingual support whatsoever:

1Schema::create('articles', function (Blueprint $table) {
2 $table->increments('id');
3 $table->string('name');
4 $table->text('text');
5 $table->timestamps();

We could easily make the table hold multiple languages by adding columns for the specific languages:

1Schema::create('articles', function (Blueprint $table) {
2 $table->increments('id');
3 $table->string('name_en');
4 $table->text('text_en');
5 $table->string('name_fr');
6 $table->text('text_fr');
7 $table->boolean('online');
8 $table->timestamps();

Now, this table has support for storing text in English and French. However, this approach has a significant drawback: every time you want to add support for a new language you’ll need to add fields to every table with multilingual support. That’s very cumbersome when the amout of languages starts to grow or if you have many multilingual tables.

A better way to go about this is to separate the fields that must be translated to another table.

1Schema::create('articles', function (Blueprint $table) {
2 $table->increments('id');
3 $table->boolean('online');
4 $table->timestamps();
7Schema::create('article_translations', function (Blueprint $table) {
8 $table->increments('id');
9 $table->integer('article_id')->unsigned();
10 $table->string('locale')->index();
12 $table->string('name');
13 $table->text('text');
15 $table->unique(['article_id','locale']);
16 $table->foreign('article_id')->references('id')->on('articles')->onDelete('cascade');

Using this migration you don’t need to create extra fields when your application needs to support an extra language.

The locale-field is used to determine the language of the stored record. In the next examples of this article, we are going to store an iso-code in this field.

Adding a foreign key with cascading deletes will make sure that when a record in the articles-table gets removed, it’s corresponding translations will be removed as well.

Ok, now that the database is ready for storing translatable content, let’s think about how we’d like to work with it.

1$frenchText = $article->getTranslation('fr')->text;

That’s nice and readable. Be we can do ever better. It would be very useful if the article model would return the French translation of the text when the app’s locale is French:

2$frenchText = $article->text;

This will make working with translatable data significantly easier. Consider this small view:

1The article with title {{ $article->title }} is {{ $this->online ? 'online' : 'offline' }}.

Notice that there isn’t any difference in using a translated field and a non-translated field. Nice!

You could create the functions to allow for this behaviour yourself, but Dimitris Savvopoulos created a package called laravel-translatable that does all of this and more. I use it on every project.

After installing the service provider you must update your models to use the Translatable-trait. The model should also have a $translatableAttributes-property containing an array with the names of the fields than can be translated:

1// app/Article.php
2class Article extends Model
4 use \Dimsav\Translatable\Translatable;
6 public $translatedAttributes = ['name', 'text'];
8 ...
11// app/ArticleTranslation.php
12class ArticleTranslation extends Model
14 public $timestamps = false;
16 ...

Now that all the setup is completed we can store some new translatable articles. If you’re following along just add this route to your Laravel app.

1Route::get('create', function($locale) {
2 $article = new Article();
3 $article->online = true;
4 $article->save();
6 foreach (['en', 'nl', 'fr', 'de'] as $locale) {
7 $article->translateOrNew($locale)->name = "Title {$locale}";
8 $article->translateOrNew($locale)->text = "Text {$locale}";
9 }
11 $article->save();
13 echo 'Created an article with some translations!';

When you look in the database after you’ve browsed to ‘/create’ you’ll see that the articles-table holds one record. The article_translations-table contains three records: one for every locale in the example above.

Now let’s retrieve the translations. Add this route to your Laravel app:

1Route::get('{locale}', function($locale) {
2 app()->setLocale($locale);
4 $article = Article::first();
6 return view('article')->with(compact('article'));

Add a article.blade.php-view with this content:

1<h1>{{ $article->name }}</h1>
2{{ $article->text }}

When you browse to ‘/en’ you’ll see the English translations of the article. Browse to ‘/nl’, ‘/fr’ or ‘/de’ to see the localized article for those locales.

Congratulations! You’ve now learned how to add multilingual support to an Eloquent model. I’ve created a repository containing all examples of this post on GitHub. If you want to know more, I recommend reading the examples in the readme of Dimitris’ package. Still hungry for more knowledge? Dive into his package code on GitHub. It’s not that hard to follow, and you’ll learn a lot from it.

Filed in:

Freek Van der Herten

PHP developer, Laravel enthousiast at, blogger, and package creator.