Building a Laravel Translation Package – Building The Frontend

Published on by

Building a Laravel Translation Package – Building The Frontend image

In the previous article of this series, I talked you through my process of putting everything needed in place to start building out the frontend. This article will build on that groundwork to complete the user interface of the package.

What Should the User Interface Do?

First things first, we need to define the scope of the task. What is the purpose of the user interface and what should it allow users to do? Sometimes, with this kind of task, it can be challenging to limit the scope. Luckily for me, this is a tool I have needed for a long time, so I have a really good idea of the minimum needed to solve my issue:

  • List all languages
  • Create new languages
  • List all translations for a language
  • Switch active language for translations
  • Search all keys and translations
  • Add new translations
  • Update existing translations

As I mentioned in the last article, I am happy building this tool predominantly using the Laravel backend to handle the heavy lifting. Call me old-fashioned but I actually like the visual cue of a page load to let me know something is happening. The only place I might not want a page load is when updating an existing translation – it would be more elegant if this happened behind the scenes.

Routing

Based on the list above, it looks like we need to define seven routes.

  • GET /languages
  • GET /languages/create
  • POST /languages
  • GET /languages/{language}/translations
  • GET /languages/{language}/translations/create
  • POST /languages/{language}/translations
  • PUT /languages/{language}/translations

Controllers

In the controllers, we heavily lean on the class built earlier in the series. In fact, we will resolve this from Laravel’s container in the constructor of the controller.

public function __construct(Translation $translation)
{
$this->translation = $translation;
}

Now, interacting with the translations in the controllers is a relatively simple task. For instance, getting a list of languages to pass to the view is as easy as this:

public function index(Request $request)
{
$languages = $this->translation->allLanguages();
 
return view('translation::languages.index', compact('languages'));
}

Note You might notice the unusual view path above containing translation::. This just tells Laravel to load the views from the package namespace defined in our service provider.

$this->loadViewsFrom(__DIR__.'/../resources/views', 'translation');

The only complex controller method is the translation index. This is complex because we will not only list translations but also add the search and filter functionality.

$translations = $this->translation->filterTranslationsFor($language, $request->get('filter'));
 
if ($request->get('group') === 'single') {
$translations = $translations->get('single');
$translations = new Collection(['single' => $translations]);
} else {
$translations = $translations->get('group')->filter(function ($values, $group) use ($request) {
return $group === $request->get('group');
});
$translations = new Collection(['group' => $translations]);
}
 
return view('translation::languages.translations.index', compact('translations'));

Here, we get the filtered translations from our translation service using the search term passed in from the user. We then determine if need to filter for only single or group type translations, and filter the returned collection accordingly, before passing to the view.

Views & Assets

Each view extends from a main layout.blade.php file which includes the HTML scaffolding and links to our Javascript and CSS assets built using Laravel Mix.

Styling

For those of you who don’t know, Tailwind CSS is a utility first framework. It provides a series of low-level classes which can be layered together to produce complex designs. Have a look at the documentation for more information. Here is my approach to harnessing the power of Tailwind.

Tailwind ships with a configuration file which makes it easy to set the base color palette and default fonts. This file ships with some really nice defaults, so quite often I don’t make any changes at all unless it’s imperative I bring through some branding.

Typically, I will start by applying utility classes to my HTML elements until it looks the way I want. If this is an element I don’t need to reuse, I will quite often leave it that way in the markup.

<div class="bg-red-lightest text-red-darker p-6 shadow-md" role="alert">
<div class="flex justify-center">
<p>{!! Session::get('error') !!}</p>
</div>
</div>

However, if it is something I will reuse in multiple places across my application, I will use Tailwind’s @apply directive to extract the utilities into a custom component class.

// before
<div class="p-4 text-lg border-b flex items-center font-thin">
{{ __('translation::languages.languages') }}
</div>
 
// after
.panel-header {
@apply p-4 text-lg border-b flex items-center font-thin
}
 
<div class="panel-header">
{{ __('translation::languages.languages') }}
</div>

In the last article, I explained how to wire PostCSS and Tailwind’s custom PostCSS plugin into Laravel Mix’s build pipeline. When this plugin runs, it will look for the @apply directive in the project’s CSS files, pull in the styles from the utilities it finds and inject them into the containing class.

The following image shows the styling of the user interface I created with Tailwind.

Javascript

As mentioned previously in this series, we’re going to let the backend do a lot of the hard work on this project. This way, we can concentrate on only using Javascript where it enhances the user experience.

Translating content can be a tiresome task, so we want to make sure the translation experience is as slick and streamlined as possible for our users. One area I think Javascript can help is by autosaving the content as the user moves through the list of translations. This prevents an extra button click and page reload where it is definitely not necessary. To enhance the experience even further, we will create a visual indication to let them know changes have been applied.

To do this, we’ll create a new Vue component called TranslationInput which will handle this functionality for us.

export default {
props: ['initialTranslation', 'language', 'group', 'translationKey', 'route'],
 
data: function() {
return {
isActive: false,
hasSaved: false,
hasErrored: false,
isLoading: false,
hasChanged: false,
translation: this.initialTranslation
}
},
....
}

Here, we are defining that the component should receive the initialTranslation, the language, the language group, the translation key and the route as properties whenever it is used. This provides all the data needed to utilize the update route we created earlier to save the translation.

The data property sets some state on the component. It is important to note that we set translation to the value of initialTranslation passed to the component as a prop.

In the component template, we bind the value of the translation property to the value of the input.

<textarea
v-model="translation"
v-bind:class="{ active: isActive }"
v-on:focus="setActive"
v-on:blur="storeTranslation"
></textarea>

Additionally, we set the input class to active when the isActive property of the data object is true. This is set when the input is focused and handled by the setActive method.

setActive: function() {
this.isActive = true;
},

When the user navigates away from the input, we want to make a call to our update endpoint with the user’s changes. You can see above that we use Vue’s v-on directive to listen for the blur event, which calls the storeTranslation method. It’s in this method that we make that call.

storeTranslation: function() {
this.isActive = false;
this.isLoading = true;
window.axios.put(`/${this.route}/${this.language}`, {
language: this.language,
group: this.group,
key: this.translationKey,
value: this.translation
}).then((response) => {
this.hasSaved = true;
this.isLoading = false;
this.hasChanged = false;
}).catch((error) => {
this.hasErrored = true;
this.isLoading = false;
})
}

Here, we use axios, an HTTP client for JavaScript, to make a call to our update endpoint. We use all the props passed in to the component to generate the URL and the latest translation made by the user which we wish to save.

We update the state of the component in accordance with the result of the call and give the user a visual indication of whether or not their action was successful. We do this by rendering an appropriate SVG icon.

<svg v-show="!isActive && isLoading">...</svg> // default state, pencil icon
<svg v-show="!isActive && hasSaved">...</svg> // success state, green check icon
<svg v-show="!isActive && hasErrored">...</svg> // error state, red cross icon
<svg v-show="!isActive && !hasSaved && !hasErrored && !isLoading">...</svg> // saving state, disk icon

In this article, we’ve had a tour of the most important components of the frontend build. We have made an interface that not only looks nice but also functions well for our users.

In the next part of the series, we will build out the ability to scan a project for translations which may be missing from the language files. In the meantime, if you have any questions, please feel free to reach out on Twitter.

Joe Dixon photo

Founder and CTO of ubisend. Proud Father to two tiny heroes, Husband, developer, occasional globetrotter.

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

image
Paragraph

Manage your Laravel app as if it was a CMS – edit any text on any page or in any email without touching Blade or language files.

Visit Paragraph
Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Bespoke software solutions built for your business. We ♥ Laravel

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
Larafast: Laravel SaaS Starter Kit logo

Larafast: Laravel SaaS Starter Kit

Larafast is a Laravel SaaS Starter Kit with ready-to-go features for Payments, Auth, Admin, Blog, SEO, and beautiful themes. Available with VILT and TALL stacks.

Larafast: Laravel SaaS Starter Kit
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector

The latest

View all →
DirectoryTree Authorization is a Native Role and Permission Management Package for Laravel image

DirectoryTree Authorization is a Native Role and Permission Management Package for Laravel

Read article
Sort Elements with the Alpine.js Sort Plugin image

Sort Elements with the Alpine.js Sort Plugin

Read article
Anonymous Event Broadcasting in Laravel 11.5 image

Anonymous Event Broadcasting in Laravel 11.5

Read article
Microsoft Clarity Integration for Laravel image

Microsoft Clarity Integration for Laravel

Read article
Apply Dynamic Filters to Eloquent Models with the Filterable Package image

Apply Dynamic Filters to Eloquent Models with the Filterable Package

Read article
Property Hooks Get Closer to Becoming a Reality in PHP 8.4 image

Property Hooks Get Closer to Becoming a Reality in PHP 8.4

Read article