Craftable

Translatable

Translatable makes your content translatable in defined languages (locales). To sum up, the package:

  • publishes a config, that defines locales (languages) used in your project,
  • introduces a HasTranslations trait that makes your Eloquent model translatable (extending spatie/laravel-translatable),
  • introduces a TranslatableFormRequest class that you can use as a base class for your Request classes to extend from, which simplify the definition of the rules for translatable data.

Requirements

This package requires PHP 7.0+ and Laravel 5.5.

This package can benefit from JSON columns, so MySQL 5.7+ or PostgreSQL 9.5+ is recommended.

Installation

{danger.fa-exclamation-triangle} This section is only when you want to use this package as a standalone package. If you are using with Craftable, then this package is already installed.

Let's add a new requirement:

composer require brackets/translatable

That's it. Provider will be discovered automatically.

Additionally, you may want to publish the config file, so you can define the languages your app uses:

php artisan vendor:publish --provider="Brackets\Translatable\TranslatableServiceProvider" --tag=config

Working with locales

You can retrieve all the locales your have set up in your config with Translatable facade:

Translatable::getLocales()

All locales are also available in $locales variable in all views:

<ul>
    @foreach($locales as $locale)
    <li><a href="/{{ $locale }}">{{ $locale }}</a></li>
    @endforeach
</ul>

{info} TIP: In your application you may want to set up current app locale based on either some route properties or maybe based on authenticated user preferences. Example of the latter: app()->setLocale(Auth::user()->language);. If you are using this package with brackets/admin-auth this is exactly what is done automatically using ApplyUserLocale middleware that is pushed into the web middleware group.

Make your model translatable

To make your Eloquent model translatable just extend our trait and define all the attributes that are translatable.

use Illuminate\Database\Eloquent\Model;
use Brackets\Translatable\Traits\HasTranslations;

class Movie extends Model
{
    use HasTranslations;

    public $translatable = ['name'];
}

All translatable columns should be of type jsonb (recommended when using with MySQL 5.7+ or PostgreSQL 9.5+) or text.

That's it. Now when you access your attribute, you will get only one translation (according to the current locale set):

$movie->name; // this will get he name in English

You can call setLocale() method on model. All following calls on attribute access, toArray and toJson calls are going to work with this locale.

$movie->setLocale('fr');
$movie->name; // returns name in French
$movie->toArray(); // array has only French translations

{info} Locale on model is by default set to Laravel's default locale from app.locale config.

Storing translations

Storing translation for one locale is simple:

$movie->setTranslation('name', 'en', 'Godfather');

$movie->save();

But typically you want to store all translations at the same time, so you can still pass an array like you would expect:

$movie->name = [
    'en' => 'Godfather'
    'fr' => 'Le parrain'
];
$movie->save();

or when creating:

Movie::create([
   'name' => [
      'en' => 'Godfather'
      'fr' => 'Le parrain'
   ]
]);

Querying translatable attributes

If you use jsonb data type for housing translations in the db, you can query translatable columns like this:

Movie::where('name->en', 'Godfather')->get();

Make your request translatable

If you already have a translatable model you probably want to store it. That's why you need to define the rules for your FormRequest. To simplify the rule definition for translatable Form Requests, you can extend our TranslatableFormRequest like this:

use Brackets\Translatable\TranslatableFormRequest;

class StoreMovie extends TranslatableFormRequest
{
    // define all the regular rules
    public function untranslatableRules()
    {
        return [
            'published_at' => ['required', 'datetime'],
        ];
    }

    // define all the rules for translatable columns
    public function translatableRules($locale)
    {
        return [
            'title' => ['required', 'string'],
            'body' => ['nullable', 'text'],
        ];
    }
}

All rules for translatable columns are going to be auto suffixed with all locales currently available. So i.e. for locales ['en', 'de', 'fr'] the above is equivalent to writing rules like this:

class StoreMovie extends FormRequest
{
    ...
    public function rules()
    {
        return [
            'published_at' => ['required', 'datetime'],
            'title.en' => ['required', 'string'],
            'title.de' => ['required', 'string'],
            'title.fr' => ['required', 'string'],
            'body.en' => ['nullable', 'text'],
            'body.de' => ['nullable', 'text'],
            'body.fr' => ['nullable', 'text'],
        ];
    }

Once defined, you can use this request like it was regular non-translatable request:

class MoviesController extends FormRequest
{
    ...
    public function store(StoreMovie $request)
    {
        // get all validated data
        $data = $request->validated();

        $movie = Movie::create($data);

        ...
    }

Required locales

By default, request's parameters of all locales are required. If you want to change this behaviour (i.e. you may want only first locale should be required), you can override method defineRequiredLocales like this:

use Brackets\Translatable\TranslatableFormRequest;
use Illuminate\Support\Collection;

class StoreMovie extends TranslatableFormRequest
{
    // define all the regular rules
    public function untranslatableRules()
    {
        return [
            'published_at' => ['required', 'datetime'],
        ];
    }

    // define all the rules for translatable columns
    public function translatableRules($locale)
    {
        return [
            'title' => ['required', 'string'],
            'body' => ['nullable', 'text'],
        ];
    }

    // make only first 2 locales required
    public function defineRequiredLocales() : Collection {
        return collect(['en', 'de']);
    }

This generates same set of rules like writing:

class StoreMovie extends FormRequest
{
    ...
    public function rules()
    {
        return [
            'published_at' => ['required', 'datetime'],
            'title.en' => ['required', 'string'],
            'title.de' => ['required', 'string'],
            'title.fr' => ['nullable', 'string'], // only en & de locales are required
            'body.en' => ['nullable', 'text'],
            'body.de' => ['nullable', 'text'],
            'body.fr' => ['nullable', 'text'],
        ];
    }