How to build a localized app with Laravel – Part 4

Frontend with multi-language styling

In the previous chapters, we looked at what an international application is and the different things to consider when making one. We started building our tour guide application and made the backend of the application. We also added multilingual support for the basic pages and content.

In this chapter, we will focus on making the frontend of the application. We will build a simple destination page and customize it for each language using css.

Prerequisites

You have read all previous chapters.

Getting started

In this guide, we will look at how to use CSS to adjust our display based on the app language. We will make the pages for the application and generate different language versions of the page where necessary.

Before we proceed, we need to create a bookings and a destinations folder inside our views folder.

$ mkdir resources/views/booking
$ mkdir resources/views/destination

Next, the following files in ./resources/views

$ touch resources/views/booking/index.blade.php
$ touch resources/views/booking/create.blade.php
$ touch resources/views/booking/userpage.blade.php
$ touch resources/views/destination/index.blade.php
$ touch resources/views/destination/show.blade.php

Next, we need to make a few adjustments. The register page generated by the Laravel auth scaffolding does not have fields for phone and country. Let us add that to the page before we continue with other pages.

Open ./resources/views/auth/register.blade.php and add the following:

// resources/views/auth/register.blade.php
[...]

<form method="POST" action="{{ route('register') }}">
@csrf

[...]

<div class="form-group row">
    <label for="phone" class="col-md-4 col-form-label text-md-right">{{ __('Phone') }}</label>

    <div class="col-md-6">
        <input id="phone" type="text" class="form-control{{ $errors->has('phone') ? ' is-invalid' : '' }}" name="phone" value="{{ old('phone') }}" required autofocus>

        @if ($errors->has('phone'))
            <span class="invalid-feedback">
                <strong>{{ $errors->first('phone') }}</strong>
            </span>
        @endif
    </div>
</div>

<div class="form-group row">
    <label for="country" class="col-md-4 col-form-label text-md-right">{{ __('Country') }}</label>

    <div class="col-md-6">
        <input id="country" type="text" class="form-control{{ $errors->has('country') ? ' is-invalid' : '' }}" name="country" value="{{ old('country') }}" required autofocus>

        @if ($errors->has('country'))
            <span class="invalid-feedback">
                <strong>{{ $errors->first('country') }}</strong>
            </span>
        @endif
    </div>
</div>

[...]
</form>

[...]

Then, add the routes for all our pages. Open ./routes/web.php and edit as follows:

// routes/web.php
Route::get('/', function () {
    return view('welcome');
});


Route::prefix('{lang}')->group(function () {
        Route::get('/destinations', "DestinationController@index");
    Route::get('/destinations/{destination}', "DestinationController@show");
});

Auth::routes();
Route::get('/booking/destination/{destination}', "BookingController@create");
Route::post('/booking', "BookingController@store");
Route::get('/dashboard', "BookingController@index");
Route::get('/home', 'BookingController@userPage')->name('home');

Also, we need to create an admin user that can see all the booking requests. We will seed the user table with the admin user information.

Run the following command to create the database seeder:

$ php artisan make:seed UserTableSeeder

Then, open the database/seeds/UserTableSeeder.php file and replace with the following:

// database/seeds/UserTableSeeder.php
<?php

use Illuminate\Database\Seeder;
use App\User;
use Illuminate\Support\Facades\Hash;

class UserTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $user = new User;
        $user->name = "Admin";
        $user->country = "Canada";
        $user->phone = "12345678";
        $user->email = "admin@example.com";
        $user->password = Hash::make("secret");
        $user->is_admin = true;
        $user->save();
    }
}

Run the seeder:

$ php artisan db:seed --class=UserTableSeeder

Making the destinations pages

The destinations pages are the primary pages of our application. They hold key information that will enable visitors decide to use our service. We want to modify the pages slightly for different visitors to improve their experience and enable them to make buying decisions.

We will assume the following:

  • The Germans and French like their websites being a representation of their national flags.
  • The rest of the world will go with our default design .

We will use the colors of the flags of France and Germany to make the style variations on the pages.

Create a file style.css in the ./public/css directory with this command directory

$ touch public/css/style.css

Paste in the following lines of code into our style.css file:

:lang(de) body{
        background: #000000;
        color:#FFCE00;
}
:lang(fr) body{
        background: #0055A4;
        color:#ffffff;
}
.text-tiny {
        font-size: 1rem;
}
:lang(fr) .text-tiny {
        background: #EF4135;
        color:#ffffff;
        font-weight: 900;
        padding: 0px 10px;
}
:lang(fr) .col-md-4 a, :lang(fr) .col-md-12 a {
        color:#ffffff;
        text-decoration: underline;
        font-weight: 900;
}
:lang(fr) .col-md-4 a:hover, :lang(fr) .col-md-12 a:hover {
        color:#EF4135;
}
:lang(de) .text-tiny {
        background: #DD0000;
        color:#ffffff;
        font-weight: 900;
        padding: 0px 10px;
}

In the above style, we used the :lang() selector to apply unique styles to portions of our page based on the language of the page. We made the backgrounds take the first colour of the respective flags and the other two colours were used for text and highlighting.

The lang() selector can be used on any tag or class. This way, you can target specific parts of your webpage and style them differently when the page is in a particular language.

<html lang="{{ app()->getLocale() }}"> is a very important part of our application. It sets the language of the current webpage a user is viewing. It is also what the lang() CSS selector uses to detect the language of the page, to style it accordingly. If you omit it, language dependent styles on your webpages will not apply.

The lang attribute can be set on any tag. The tag can also have a different language from the rest of the webpage. While it is possible to have multiple languages on the same page and the browser will know about them, it is a strongly discouraged practice. It impacts the experience of the user and can impact on what the user receives if she translates your webpage into an entirely different language.

Open the destination/index.blade.php file and add the following:

@extends('layouts.app')
@section('styles')
    <link href="{{ asset('css/style.css') }}" rel="stylesheet">
@endsection
@section('content')
<div class="container">
        <div class="row">
                @foreach($destinations as $destination)
                <div class="col-md-4">
                        <img src="{{$destination->image}}" class="img img-fluid">
                        <h2>{{$destination->name}} <span class="float-right text-tiny">{{$destination->location}}</span></h2>
                        <hr/>
                        <a href="{{url(app()->getLocale().'/destinations/'.$destination->id)}}">{{__('View More')}}</a>
                </div>
                @endforeach
        </div>
</div>
@endsection

We extended the main app container file and added two sections. One for styles and the other for content. The page is simple. It lists out all the destinations we have and puts a link to view more on each destination.

The __() method, as we explained in the last chapter, handles translations. The string maps we created for the French-English texts are used to find replacements for the English word when the language of the page is in French. Similarly, the string map for German-English will be used when the page is in German. The French string map is in fr.json file in the ./resources/lang directory.

Now, open the layouts/app.blade.php file and add the yield for styles and scripts

<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
    [...]
    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
    @yield('styles')
</head>
[...]
        <main class="py-4">
            @yield('content')
        </main>
    </div>
    @yield('scripts')
</body>
</html>

Finally, open the destination/show.blade.php file and add the following:

@extends('layouts.app')
@section('styles')
    <link href="{{ asset('css/style.css') }}" rel="stylesheet">
@endsection
@section('content')
<div class="container">
        <div class="row">
                <div class="col-md-12">
                        <img src="{{$destination->image}}" class="img img-fluid">
                        <h2>{{$destination->name}} <span class="float-right text-tiny">{{$destination->location}}</span></h2>
                        <hr/>
                        <p>{{$destination->translated_description}}</p>
                        <a href="{{url('booking/destination/'.$destination->id)}}">{{__('Book Now')}}</a>
                </div>
        </div>
</div>
@endsection

Here, we have displayed the content of each destination and added a link to book it. That’s it for the destination pages.

Making the tour booking page

The tour booking page is going to be simple. It should show the destination information a user clicked on and a little form for the user to provide additional information for the booking. We will translate the page based on language as well.

Open the booking/create.blade.php file and add the following:

@extends('layouts.app')
@section('styles')
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.8.0/css/bootstrap-datepicker.min.css">
@endsection
@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{__('Make a booking')}}</div>

                <div class="card-body">
                    <div class="row">
                        <div class="col-md-6">
                            <img src="{{$destination->image}}" class="img img-fluid">
                            <h2>{{$destination->name}}</h2>
                            <small>{{$destination->location}}</small>
                        </div>
                        <div class="col-md-6">
                            <form method="post" action="{{url('booking')}}">
                                @csrf
                                <input type="hidden" name="destination_id" value="{{$destination->id}}">
                                <div class="form-group row">
                                    <div class="col-md-12">
                                        <label for="number_of_tourists">
                                            {{__('How many people are coming?')}}
                                        </label>
                                        <input id="number_of_tourists" type="text" class="form-control{{ $errors->has('name') ? ' is-invalid' : '' }}" name="number_of_tourists" value="{{ old('number_of_tourists') }}" required autofocus placeholder="{{__('e.g')}} 4">

                                        @if ($errors->has('name'))
                                            <span class="invalid-feedback">
                                                <strong>{{ $errors->first('number_of_tourists') }}</strong>
                                            </span>
                                        @endif
                                    </div>
                                </div>

                                <div class="form-group row">
                                    <div class="col-md-12">
                                        <label for="visit_date">
                                            {{__('When would you like to visit?')}}
                                        </label>
                                        <div class="input-group date" data-provide="datepicker">
                                            <input type="text" class="form-control datepicker" required autofocus placeholder="{{__('e.g')}} 01/26/2019" name="visit_date">
                                            <div class="input-group-addon">
                                                <span class="glyphicon glyphicon-th"></span>
                                            </div>
                                        </div>

                                        @if ($errors->has('name'))
                                            <span class="invalid-feedback">
                                                <strong>{{ $errors->first('number_of_tourists') }}</strong>
                                            </span>
                                        @endif
                                    </div>
                                </div>
                                <div class="form-group row">
                                    <div class="col-md-12">
                                        <button type="submit" class="btn btn-primary">
                                            {{ __('Book Now') }}
                                        </button>
                                    </div>
                                </div>
                            </form>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection
@section('scripts')
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.8.0/js/bootstrap-datepicker.min.js" defer>
    $(document).ready(function() {
        $('.datepicker').datepicker();
    });
</script>
@endsection

On the page, we used bootstrap-datepicker package to make it easy for our visitors to select the date of their booking in a way our application can easily interpret.

Making the user page for viewing bookings

This page is like the mission control for a user. It helps the user see all the tours they have previously booked. It does not do so much, but it is informative enough for the user.

Open the booking/userpage.blade.php file and add the following:

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">Dashboard</div>

                <div class="card-body">
                    @if(empty($bookings))
                    <h3>{{__('You do not have any reservations')}}</h3>
                    @else
                    @foreach($bookings as $booking)
                    <div class="col-md-12">
                        <div class="row">
                            <div class="col-md-6">
                                <img src="{{$booking->destination->image}}" class="img img-fluid">
                                <h2>{{$booking->destination->name}}</h2>
                                <small>{{$booking->destination->location}}</small>
                            </div>
                            <div class="col-md-6">
                                <h5>{{__('Number of tourists')}}: <br/><strong>{{$booking->number_of_tourists}}</strong></h5>
                                <h5>{{__('Tour date')}}: <br/><strong>{{$booking->visit_date->toDateString()}}</strong></h5>
                            </div>
                        </div>
                    </div>
                    @endforeach
                    @endif
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

Making the admin page to view bookings

This page is like mission control for the admin user. It helps the admin see all the tours users have booked and information on the users who booked these tours. We will not offer translations for this page. We assume the administrator speaks English.

Open the booking/index.blade.php file and add the following:

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">Admin Dashboard</div>

                <div class="card-body">
                    @if(empty($bookings))
                    <h3>{{__('You do not have any reservations')}}</h3>
                    @else
                        @foreach($bookings as $booking)
                    <div class="col-md-12">
                        <div class="row">
                            <div class="col-md-6">
                                <img src="{{$booking->destination->image}}" class="img img-fluid">
                                <h2>{{$booking->destination->name}}</h2>
                                <small>{{$booking->destination->location}}</small>
                            </div>
                            <div class="col-md-6">
                                <h5>Number of tourists: <br/><strong>{{$booking->number_of_tourists}}</strong></h5>
                                <h5>Tour date: <br/><strong>{{$booking->visit_date->toDateString()}}</strong></h5>
                                <hr />
                                <h5><strong>User's name:</strong> <br/>{{$booking->user->name}}</h5>
                                <h5><strong>Contact information:</strong><br/>
                                    Phone: {{$booking->user->phone}}<br/>
                                    Email: {{$booking->user->email}}<br/>
                                    Country: {{$booking->user->country}}
                                </h5>
                            </div>
                        </div>
                    </div>
                        @if(!$loop->last)
                    <hr/>
                        @endif
                        @endforeach
                    @endif
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

How our application looks now

Let us see what the different versions look like. Run the following command to start the page:

$ php artisan serve

Visit 127.0.0.1:8000 to view our international application.

To see the pages in different language versions, change the language settings on your browser language settings. If you are on a chrome browser, read about how to change your language here.

French version of the homepage

Notice that when you set the browser language to French, the text on the homepage shows in French. Then when you have it in English, the text on the page is in English.

German version of the destination page

French version of the destination single page

English version of the destination single page

Tour booking page

Admin dashboard

Conclusion

We have looked at what it takes to build an international application. We built an international application with support for multiple languages. We did string translations for French and German. We saw how to style different sections of our page based on language using the CSS lang() selector.

At the end of the day, we have a simple international application. There is a lot that can be added to it like dynamically generated links for alternative language versions. I hope this guide helps you fully understand what to look out for when building an international application. Checkout out these quick tips from W3 on making international applications.

The source code to the application in this article is available on GitHub.

This post was originally posted on Pusher.