Build a localized app with Laravel - Part 4: Frontend with multi-language styling
Please read and follow the previous tutorials, also linked in this tutorial.
In this final tutorial of the series, 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.
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.
Prerequisites
You have read all previous chapters.
Here are the part 1, part 2 and part 3 in case you missed it!
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 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 to 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 color of the respective flags and the other two colors 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 web pages 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.
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.
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.
21 August 2018
by Fisayo Afolayan