★ Making Nova fields translatable
Tags: laravel, php, nova
Published: 2020-01-02 23:06:19

---
title: ★ Making Nova fields translatable
published: true
date: 2018-11-05 12:00:00 UTC
tags: laravel, php, nova
canonical_url: https://freek.dev/1200-making-nova-fields-translatable
---

[Laravel Nova](https://nova.laravel.com) is cool package to quickly create admin interfaces in a Laravel app. Unfortunately there's no support for multiple locales out of the box. A while ago we published [a package called nova-translatable](https://github.com/spatie/nova-translatable) that makes any of the [built in field types](https://nova.laravel.com/docs/1.0/resources/fields.html#fields) translatable.

## Using the package #

In order to use nova-translatable you must install [spatie/laravel-translatable](https://github.com/spatie/laravel-translatable) into your Laravel app. In a nutshell, laravel-translatable will store translations for your model in a json column in your table.

You can let `nova-translatable` know which locales it should use by calling `defaultLocales`. You can put this code in AppServiceProvider or a dedicated service provider of your own:

```
\Spatie\NovaTranslatable\Translatable::defaultLocales(['en', 'nl']);
```

Let's now take a look at simple Nova resource.

```
public function fields(Request $request)
{
    return [
        ID::make()->sortable(),
        Text::make('title'),
        Trix::make('text'),
    ];
}
```

In order to make the `title` and `text` fields translatable, the only thing you need to do is to pass them to an instance of `Spatie\NovaTranslatable\Translatable`.

```
public function fields(Request $request)
{
    return [
        ID::make()->sortable(),

        Translatable::make([
            Text::make('title'),
            Trix::make('text'),
        ]),
    ];
}
```

With this in place, this is how your Nova resource will look like:

![nova-translatable in action](https://freek.dev/uploads/media/nova-translatable/screenshot.png)

## Behind the scenes #

There's actually not so much code involved to make `nova-translatable` work. The [main `Translatable` class](https://github.com/spatie/nova-translatable/blob/f9ae979eb05fbcd1d313ff3a68ef54709e601766/src/Translatable.php) is only 140 lines long. `Translatable` extends Nova's native `MergeValue` class which is used to group fields (a [Nova Field Panel](https://nova.laravel.com/docs/1.0/resources/fields.html#field-panels) is a `MergeValue` tool).

An instance of `MergeValue` holds its fields in the `data` property. Whenever a new `Translatable` is newed up, we are going [to call a function](https://github.com/spatie/nova-translatable/blob/f9ae979eb05fbcd1d313ff3a68ef54709e601766/src/Translatable.php#L48) named `createTranslatableFields` which will replace that data. Let's take a look at that function.

```
protected function createTranslatableFields()
{
    if ($this->onIndexPage()) {
        $this->data = $this->originalFields;
        return;
    }
  
    $this->data = [];
  
    collect($this->locales)
        ->crossJoin($this->originalFields)
        ->eachSpread(function (string $locale, Field $field) {
            $this->data[] = $this->createTranslatedField($field, $locale);
        });
}
```

If we're on the index, aka the list representation of a resource, we won't replace the original fields. We assume you just want to original fields there. But if we're on any other screen of the resource we will start replacing the data. For each combination of a field and of the desired locales we will create a new entry in `data`. If you want some more info on `crossJoin` and `eachSpread`, read [this post](https://freek.dev/avoid-nesting-using-the-crossjoin-and-eachspread-collection-functions).

Here's the code of `createTranslatedField`. I've added some comments so you can understand better what's going on.

```
protected function createTranslatedField(Field $originalField, string $locale): Field
{
    /*
     * Let's start by clone the original field so we can make some
     * modifications for the given $locale
     */
    $translatedField = clone $originalField;

    /*
     * To make this work, the attribute most be an attribute that is actually on
     * the underlying model. You don't need to understand why this happens. Hopefully
     * this will not be needed anymore in future version of Nova.
     *
     * More info in this issue on Nova:
     * https://github.com/laravel/nova-issues/issues/827
     */
    $originalAttribute = $translatedField->attribute;
    $translatedField->attribute = 'translations';

    /*
     * If your app uses more than 1 locale we're going to change the label next to it a little
     * By default `displayLocalizedNameUsingCallback` contains a closure that will append
     * the locale between brackets to the label, so `name` will become `name (en)`.
     */
    $translatedField->name = (count($this->locales) > 1)
        ? ($this->displayLocalizedNameUsingCallback)($translatedField, $locale)
        : $translatedField->name;

    /*
     * `resolveUsing` will get called before the field gets displayed. It should return the value
     * of the form field being displayed
     */
    $translatedField
        ->resolveUsing(function ($value, Model $model) use ($translatedField, $locale, $originalAttribute) {
            /*
             * Here we sneakily update the `attribute`. It will get used as the `id` of the form field 
             * that Nova will render. When the resource gets saved this name will be used in the response.
             */
            $translatedField->attribute = 'translations_'.$originalAttribute.'_'.$locale;

            /*
             * Here we will return the translation of the $locale we're processing. This value
             * will get displayed in the form field that Nova will render.
             */
            return $model->translations[$originalAttribute][$locale] ?? '';
        });

    /*
     * `fillUsing` will get called when the user submitted the form. It contains logic on how
     * the response values should get mapped to the underlying model.
     */
    $translatedField->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
        /*
         * Remember that `attribute` we set `resolveUsing`? Here we are going to split it up
         * again so we know which field and which locale we should store the value from the request.
         */
        $requestAttributeParts = explode('_', $requestAttribute);
        $locale = array_last($requestAttributeParts);

        array_shift($requestAttributeParts);
        array_pop($requestAttributeParts);

        $key = implode('_', $requestAttributeParts);

        $model->setTranslation($key, $locale, $request->get($requestAttribute));
    });

    return $translatedField;
}
```

## In closing #

You might wonder why we didn't render the translatable fields in tabs, panels or with magical unicorns displayed next to them. The truth is that everybody wants translations to be displayed a bit different. That's why we opted to keep it very simple for now. If Nova gains the ability to better structure a long form natively, we'd probably start leveraging that in a new major version of the package.

If you like [nova-translatable](https://github.com/spatie/nova-translatable) be sure to check out or other Nova packages as well:

- [nova-tail-tool](https://github.com/spatie/nova-tail-tool)
- [nova-backup-tool](https://github.com/spatie/nova-backup-tool)
- [nova-tags-field](https://github.com/spatie/nova-tags-field)

Even if you're not using Laravel Nova, you probably will find something for your next project in [this list of packages](https://spatie.be/open-source/packages) [the team at Spatie](https://spatie.be) has made previously.