Article image

HOW TO MAKE TRANSLATIONS FOR YOUR LARAVEL AND VUE.JS APPLICATION

Javascript -

Oct 02 2019

Dino Numic

Having translations on your website is one of the ways to attract users and potential customers as the website becomes more appealing to foreign language speakers. Maybe the content of your application is highly sought after, but due to it being in only one language, it is not realizing it’s full potential.

With Laravel, it is quite easy to setup translations. Language strings are stored in resources/lang folder where you can add as many languages as you want. You create a new sub folder for each language you want to support.

/resources

   /lang

       /en

           validation.php

       /es

           validation.php

For example, in validation.php file under en we have an array of various validation rules with translation strings.

return [
     …
     'array' => 'The :attribute must be an array.',
     'boolean' => 'The :attribute field must be true or false.',
     …
]

The default locale is set in config/app.php.

'locale' => 'en'

In order to change the locale for the user at runtime you may use the setLocale method on the App facade. For example, a dropdown in navigation menu that calls a set-locale route which updates the current locale.

Route::get('set-locale/{locale}', function ($locale) {  
    App::setLocale($locale);
});

In blade we can display translations using {{ }} syntax or @lang directive.

<h4 
    {{ __(‘article.title') }}
</h4>
 
@lang(‘article.title’)

If anything, it’s just a tedious process of creating new files and adding translation strings. Conceptually, it’s quite easy.

Everything is cool if we are using blade on our frontend. What if we are using Vue in spa? Luckily, we can use two awesome plugins: laravel-vue-i18n-generator and vue-i18n. 

But first, let’s prepare our project for the translations. I will create a simple Products page. It will have a dropdown for changing locales, a table with products, and some action buttons. Each product will have translatable properties in the database that will update whenever we change language. I need to create a model, migration, seeder, controller, and a route. We will keep the logic of changing locale in our store but also keep the last selected language in local storage in case that user comes back to our website or refreshes the page.

Let’s get going.

We can quickly create a model, migration, and controller by running

php artisan make:model Models\Product -mc

Product model

class Product extends Model
{
    protected $fillable = ['name', 'description', 'price', 'quantity'];
}

Product migration

Schema::create('products', function (Blueprint $table) {
    $table->bigIncrements('id');

    $table->string('name');
    $table->string('description');
    $table->string('price');
    $table->string('quantity');

    $table->timestamps();
});

Product seeder

public function run()
{
    Product::create([
        'name'        => json_encode(['en' => 'Laptop',             'es' => 'Ordenador portátil']),
        'description' => json_encode(['en' => 'White color laptop', 'es' => 'Portátil de color blanco']),
        'price'       => 999,
        'quantity'    => 6,
    ]);

    Product::create([
        'name'        => json_encode(['en' => 'Bicycle',           'es' => 'Bicicleta']),
        'description' => json_encode(['en' => 'Red color bicycle', 'es' => 'Bicicleta de color rojo']),
        'price'       => 300,
        'quantity'    => 12,
    ]);

    Product::create([
        'name'        => json_encode(['en' => 'Chair',                    'es' => 'Silla']),
        'description' => json_encode(['en' => 'Wooden brown color chair', 'es' => 'Silla de madera color marrón']),
        'price'       => 120,
        'quantity'    => 9,
    ]);

    Product::create([
        'name'        => json_encode(['en' => 'Fridge',                   'es' => 'Refrigerador']),
        'description' => json_encode(['en' => 'Large white color fridge', 'es' => 'Nevera grande de color blanco']),
        'price'       => 650,
        'quantity'    => 20,
    ]);
}

We will keep all translations as a json data type.

Don’t forget to add ProductSeeder to DatabaseSeeder

$this->call(ProductsTableSeeder::class);

Now run

php artisan migrate –seed

to create and populate products table.

Product route

Route::get('/products', 'ProductController@index')->name('products.index');

Product controller

class ProductController extends Controller
{
    public function index()
    {
        return view('pages.product.index', ['products' => Product::all()]);
    }
}

Product blade file

@extends('layouts.app')

@section('content')
    <product-index :products="{{ $products }}"></product-index>
@endsection

I will pass all products to product-index as products prop. In a real-world situation, I would probably use axios, and asynchronously fetch records.

Product Index Vue component

<template>
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-12">
                <div class="card">
                    <div class="card-header"></div>
 
                    <div class="card-body">
 
                    </div>
                </div>
            </div>
        </div>
    </div></template>
 
 <script>
    export default {
        name: "ProductComponent",
       props: {
              products: {
              type: Array,
                   default: () => {
                         return []
                    }
               }
         }
    }
 </script>

Let’s create some translations for our products view.

Inside en products.php we have

<?phpreturn [
    'table' => [
        'products'    => 'Products',
        'name'        => 'Name',
        'description' => 'Description',
        'price'       => 'Price',
        'quantity'    => 'Quantity',
        'buttons' => [
            'new'         => 'Create new',
            'edit'        => 'Update',
            'delete'      => 'Delete'
        ]
    ]
];

And inside es products.php we have

<?phpreturn [
    'table' => [
        'products'    => 'Productos',
        'name'        => 'Nombre',
        'description' => 'Descripción',
        'price'       => 'Precio',
        'quantity'    => 'Cantidad',
        'buttons' => [
            'new'         => 'Crear nuevo',
            'edit'        => 'Actualizar',
            'delete'      => 'Eliminar'
       ]
    ]
 ];

No offense to my Spanish readers if I have messed this up. Google did the translations.

In order to use these translations with Vue we need to install the two beforementioned packages.

laravel-vue-i18n-generator will help us convert Laravel translations into vue-i18n compatible json translations. How awesome.

We can install it with composer

composer require martinlindhe/laravel-vue-i18n-generator

Publish configuration file

php artisan vendor:publish --provider="MartinLindhe\VueInternationalizationGenerator\GeneratorProvider"

We can install Vue I18n with npm

npm install vue-i18n

In order to convert translations into json object we use this command

php artisan vue-i18n:generate

Each time you update lang files you will have to run the command again.

After running the command, we will get this file vue-i18n-locales.generated.js containing all our translations. You can change the config so that it creates a file for each language in case you want to lazy load it.

Now we need to import Vue i18n and our translations into our Vue app.

import VueInternationalization from 'vue-i18n';
import Locale from './vue-i18n-locales.generated';
 
Vue.use(VueInternationalization);
const lang = localStorage.getItem('locale') || 'en'; 
 
const i18n = new VueInternationalization({
    locale: lang,
    messages: Locale
 });

const app = new Vue({
    el: '#app',
    i18n, 
    store,
    render: h => h(App)
 });


We will try to get a language from local storage otherwise we will default to english. We also tell Vue i18n where to find translations.

Our store.js

import Vue from 'vue'
import Vuex from 'vuex'
import locale from "./modules/locale";
 
Vue.use(Vuex)
 
export default new Vuex.Store({
   modules: {
       locale
   },
  …
});

In store we will import locale module.

Our locale.js

const CHANGE_LOCALE = "CHANGE_LOCALE";
 
 export default {
    state: {
        locale: localStorage.getItem('locale') || 'en'
    },
    mutations: {
        [CHANGE_LOCALE] (state, language) {
            state.locale = language;
        },
    },
    actions: {
        changeLocale ({commit}, language) {
            return new Promise((resolve, reject) => {
                localStorage.setItem('locale', language);
                commit(CHANGE_LOCALE);
                resolve('Success');
            });
        }
    },
    getters: {
 
    }
 }

Here we have a simple action which commits a mutation of our locale state. Locale is used by Vue i18n.

I will create a LanguageDropdown component. This component will let us pick the language of our website.

<template>
    <div class="dropdown">
        <button class="btn dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
            {{ $i18n.locale }}
        </button>
        <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
            <a v-for="(locale, index) in locales" :key="index" class="dropdown-item" href="#"
               @click.prevent="setLocale(locale)"
            >
                {{ locale }}
            </a>
        </div>
    </div></template><script>
    export default {
        data() {
            return {
                locales: ['en', 'es'],
            }
        },
        methods: {
            setLocale(language) {
                this.$store.dispatch('changeLocale', language)
                    .then(() => {
                        this.$i18n.locale = language;
                    }).catch((error => {
                }));
            }
        }
    }
 </script>

Now we have all the pieces, we just have to connect them in our ProductComponent.

<template>
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-12">
                <div class="card">
                    <LocaleDropdown></LocaleDropdown>
                    <div class="card-header">
                        {{ $t('products.table.products') }}
                        <button class="btn btn-sm btn-success float-right">
                            {{ $t('products.table.buttons.new') }}
                        </button>
                    </div>

                    <div class="card-body">
                        <table class="table table-hover">
                            <thead>
                            <tr>
                                <th scope="col">{{ $t('products.table.name') }}</th>
                                <th scope="col">{{ $t('products.table.description') }}</th>
                                <th scope="col">{{ $t('products.table.quantity') }}</th>
                                <th scope="col">{{ $t('products.table.price') }}</th>
                                <th scope="col">{{ $t('products.table.actions') }}</th>
                            </tr>
                            </thead>
                            <tbody>
                            <tr v-for="(product, index) in products" :key="product.id">
                                <td>{{ translate(product.name) }}</td>
                                <td>{{ translate(product.description) }}</td>
                                <td>{{ product.quantity }}</td>
                                <td>{{ product.price }}</td>
                                <td>
                                    <div class="btn-group" role="group" aria-label="Tag Action Buttons">
                                        <button  class="btn btn-sm btn-primary mr-1">
                                            <i class="fas fa-pencil-alt mr-1"></i>
                                            {{ $t('products.table.buttons.edit') }}
                                        </button>
                                        <button class="btn btn-sm btn-danger">
                                            <i class="fas fa-trash mr-1"></i>
                                            {{ $t('products.table.buttons.delete') }}
                                        </button>
                                    </div>
                                </td>
                            </tr>
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    import LocaleDropdown from "../Helpers/LocaleDropdown";

    export default {
        name: "ProductComponent",
        props: {
            products: {
                type: Array,
                default: () => {
                    return []
                }
            }
        },
        components: {
            LocaleDropdown
        },
        methods: {
            translate(name) {
                try {
                    JSON.parse(name);
                } catch (e) {
                    return name;
                }
                let translated = JSON.parse(name);
                return translated[this.$i18n.locale]
            },
        }
    }
</script>

In order to translate with Vue i18n we use {{ $t() }} syntax. However, for the actual products we need to determine which of the translations to use based on the current locale. I have created a simple method, that you should create as a mixin to prevent code duplication.

English version

Spanish version

That is it pretty much. Not so hard after all. I hope you found this article useful.