Emil Moe
Emil Moe

Emil Moe

Laravel Translatable Attributes

Translate model attributes (table columns) easily into any number of languages.

Emil Moe's photo
Emil Moe

Published on Sep 1, 2021

4 min read

Subscribe to my newsletter and never miss my upcoming articles

In order to fluently support multilingual, I created this trait for Laravel. My ambition was to make it as seamlessly integrated as possible and to follow the semantics of Laravel.

You may also choose to install it with composer:

composer require cloudmonitor/translatable

File location

As the namespace indicates, I chose to store the trait in app\Models\Traits\Translatable.php, but it's free for you to change this of course.

I have to document the necessary steps in the trait itself, to keep it in a nice single component file style concept, however, for convenience, I will walk through the steps here.

Prepare Eloquent models

Similar to other special attributes in Eloquent, such as $fillables, translatable attributes must be defined as an array. It is as simple as giving the name of the database column:

protected $translatable = [
    'name',
];

Translatable will now only be observing these attributes and skip the rest.

Using translations

As Translatable uses Laravels app()->getLocale() it means it will figure out which language to use when you query name.

For instance, your locale is currently da (Danish), so you want to update a book title. Simply do it as there was no translation implementation:

$book = Book::find($id);
$book->name = 'New name for Danish version';
$book->save();

Or as an update method:

Book::find($id)->update(['name' => 'New name for Danish version']);

Similar when you want to get the name in the current locale you simply query it:

return Book::find($id)->name;

Other locales

Sometimes you want to update all translations or in a specific language or simply in a different than you are using. It could be a Danish moderator who wants to update the English title, titles for several languages, or something different.

Book::find($id)->setTranslation('name', 'en', 'Name in English');

Similarly, a specific language can be queried:

Book::find($id)->getTranslation('name', 'en');

Migrations

Behind the scenes Translatable uses JSON columns in the database to store multiple versions in the same column:

$table->json('name');

The code

<?php

namespace App\Models\Traits;

/**
 * Trait allowing certain properties (columns) in Eloquent to be
 * translatable into different locales.
 * 
 * Default behavior is to return in the currently active locale.
 * 
 * Translatable properties are set in the Eloquent model with $translatable = [],
 * such as protected $translatable = ['name'];
 * 
 * Data stored as JSON (casted automatically), such as for name:
 * {"en": "Value in English", "da": "Value in Danish"}
 * 
 * $model->name will return either the value from en or da based on app()->getLocale().
 * Likewise, $model->name = 'New value' will set the value on the language based on app()->getLocale()
 * 
 * $model->getTranslation('name', 'da') will always return the Danish.
 * Likewise $model->setTranslation('name', 'da', 'New value in Danish')
 */
trait Translatable
{
    /**
     * Override getCasts() to allow trait to set casts.
     *
     * @return array
     */
    public function getCasts()
    {
        $class = static::class;

        foreach (class_uses_recursive($class) as $trait) {
            $method = 'get'. class_basename($trait) .'Casts';

            if (method_exists($class, $method)) {
                $this->casts = array_merge(
                    $this->casts,
                    $this->{$method}()
                );
            }
        }

        return parent::getCasts();
    }

    /**
     * Get casts for the current trait.
     * 
     * @return array
     */
    public function getTranslatableCasts()
    {
        $casts = [];

        collect($this->translatable)->each(function ($item) use(&$casts) {
            $casts[$item] = 'json';
        });

        return $casts;
    }

    /**
     * Get translation if matches value in protected $translatable = [].
     * Otherwise calling parent __get($key)
     * 
     * @return mixed
     */
    public function __get($key)
    {
        if (in_array($key, $this->translatable)) {
            return $this->getTranslation($key);
        }

        return parent::__get($key);
    }

    /**
     * Set property value in current language if match found in protected $translatable = [].
     * Otherwise calling parent __set($key, $value)
     * 
     * @return void
     */
    public function __set($key, $value)
    {
        if (in_array($key, $this->translatable)) {
            if (is_array($value)) {
                foreach($value as $locale => $val) {
                    $this->setTranslation($key, $locale, $val);
                }
            } else {
                $this->setTranslation($key, app()->getLocale(), $value);
            }
        }

        parent::__set($key, $value);
    }

    /**
     * Set translation in a given locale.
     * 
     * @param String $key
     * @param String $locale
     * @return void
     */
    public function setTranslation(String $key, String $locale, $value): void
    {
        if (isset($this->attributes[$key])) {
            $attribute = json_decode($this->attributes[$key]);
        }
        else {
            $attribute = [];
        }

        $attributes[$locale] = $value;
        $this->attributes[$key] = json_encode($attribute);
    }

    /**
     * Get translation.
     * 
     * @param string $key
     * @param string $locale
     * @return String
     */
    public function getTranslation(string $key, string $locale = null): String
    {
        $locale = $locale ? $locale : app()->getLocale();

        return json_decode($this->attributes[$key])->{$locale};
    }
}
 
Share this