Building a Laravel Translation Package – The Database Driver

Published on by

Building a Laravel Translation Package – The Database Driver image

In the previous article of the series, we talked about how to handle missing translations, which brings us very close to making the package feature complete. To finish up the build phase of this series, we will discuss how we go about adding a database driver.

Signing the contract

As, you may recall from earlier in the series we used an interface to define all the methods the file deriver needed to implement to function. The main reason for doing that was it was always on the package roadmap to be able to support multiple drivers. Defining an interface meant we had a contract on which all new drivers could rely to provide the required functionality.

Migrations

To kick off the database driver, we’ll need some database tables in which to store our translations. This doesn’t need to be too complex – a table to store languages and another to store the translations is sufficient.

// languages table
public function up()
{
Schema::create(config('translation.database.languages_table'), function (Blueprint $table) {
$table->increments('id');
$table->string('name')->nullable();
$table->string('language');
$table->timestamps();
});
}
 
// translations table
public function up()
{
Schema::create(config('translation.database.translations_table'), function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('language_id');
$table->foreign('language_id')->references('id')->on(config('translation.database.languages_table'));
$table->string('group')->nullable();
$table->text('key');
$table->text('value')->nullable();
$table->timestamps();
});
}

We use the package configuration to decide the names of the tables in the migration. This allows users of the package to name their own and avoid any potential conflicts.

It’s also worth pointing out the presence of the language_id foreign key on the translations table. This tells us which language each translation belongs to.

There is also a nullable field called group which will be used to track whether it’s a single or group translation.

Finally, there are fields for key and value which, as the names suggest, store the key and value of each translation.

In order for the migrations to be automatically run when our users run php artisan migrate, we need to load them in our service provider by using the loadMigrationsFrom method and passing in the path to the newly created migrations.

public function boot()
{
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
}

Models

Our models can also be kept relatively simple. Given that we are allowing users to define their own table names, each model will need to explicitly define its associated table. We can do this in the constructor of the model by assigning the value that’s set in the configuration.

public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->table = config('translation.database.languages_table');
}

Finally, each table will need to define its relation to the other:

...
 
class Langauge extends Model
{
...
 
public function translations()
{
return $this->hasMany(Translation::class);
}
}
...
 
class Translation extends Model
{
...
 
public function language()
{
return $this->belongsTo(Language::class);
}
}

Wiring Up the Driver

With the migrations and models defined, we are now in a position to start building out the database driver implementation. To do this, we will create a new class called Database and implement the driver interface. We’ll also extend the Translation abstract class which contains some methods common to all driver implementations.

class Database extends Translation implements DriverInterface
{
...
}

Now it is just a case of building out the required methods. For this, we will lean quite heavily on Eloquent. I have documented some of the more interesting methods below, but if you are interested in seeing the full implementation, you can check it out on GitHub.

public function allLanguages()
{
return Language::all()->mapWithKeys(function ($language) {
return [$language->language => $language->name ?: $language->language];
});
}
 
// $this->allLanguages();
 
// [
// ‘en’ => ‘en’,
// ‘fr’ => ‘fr’,
// ‘es’ => ‘es’,
// ];

Here, we use Eloquent to get all the languages from the database and map over each to return the languages in the same format as the file driver.

public function getGroupTranslationsFor($language)
{
$translations = $this->getLanguage($language)
->translations()
->whereNotNull('group')
->where('group', 'not like', '%single')
->get()
->groupBy('group');
 
return $translations->map(function ($translations) {
return $translations->mapWithKeys(function ($translation) {
return [$translation->key => $translation->value];
});
});
}
 
// $this->getGroupTranslationsFor('en');
 
// [
// ‘auth’ => [
// ‘failed’ => ‘These credentials do not match our records’,
// ],
// ]

Here, we are returning all the group translations for a given language. We do this by using eloquent to get all the translations for said language where the group field doesn’t include single. We then map over the Eloquent collection to return the results in the correct format.

public function addGroupTranslation($language, $key, $value = '')
{
list($group, $key) = explode('.', $key);
 
Language::where('language', $language)
->first()
->translations()
->updateOrCreate([
'group' => $group,
'key' => $key,
], [
'group' => $group,
'key' => $key,
'value' => $value,
]);
}

Here, we explode the key on the period to get the group and the key for the translation. Then we use Eloquent’s updateOrCreate method to determine if the translation already exists. If it does, we update it in case the value has changed. If not, we create a new translation for that language.

Using the Driver

In order to tell Laravel which driver to instantiate, we will create a new TranslationManager class which uses the package configuration to determine which driver to return.

public function resolve()
{
$driver = $this->config['driver'];
$driverResolver = studly_case($driver);
$method = "resolve{$driverResolver}Driver";
 
return $this->{$method}();
}
 
protected function resolveFileDriver()
{
return new File(...);
}
 
protected function resolveDatabaseDriver()
{
return new Database(...);
}

Here, we take the driver value defined in the configuration file, turn that into a method which is responsible for understanding how to instantiate the driver and return it. Doing it this way means adding new drivers in the future will be a much easier task. As an example, if we were to add a new driver defined as cloud in the configuration, we would add a method called resolveCloudDriver to the TranslationManager which return a new instance of that driver.

Now, in the register method of our service provider, we can bind the translation driver as a singleton to the container. This means any time we grab it from the container, the same instance will be returned.

use JoeDixon\Translation\Drivers\Translation;
 
...
 
$this->app->singleton(Translation::class, function ($app) {
return (new TranslationManager($app, $app['config']['translation'], $app->make(Scanner::class)))->resolve();
});

You may notice the class we are actually binding is the abstract Translation class. This is possible because all of our drivers extend this class. It’s useful because it means no matter which driver is instantiated, it can be obtained from the container in the same way.

With this driver in place, our package is nearly ready to be released to the world. In the next article of the series, we’ll talk about documentation and what is needed to help get your users up and running.

As usual, if you have any questions in the mean time, 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.

Filed in:
Cube

Laravel Newsletter

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

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

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

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
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant 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
Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate logo

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate

Build your SaaS application in hours. Out-of-the-box multi-tenancy and seamless Stripe integration. Supports subscriptions and one-time purchases, allowing you to focus on building and creating without repetitive setup tasks.

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate
Rector logo

Rector

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

Rector
MongoDB logo

MongoDB

Enhance your PHP applications with the powerful integration of MongoDB and Laravel, empowering developers to build applications with ease and efficiency. Support transactional, search, analytics and mobile use cases while using the familiar Eloquent APIs. Discover how MongoDB's flexible, modern database can transform your Laravel applications.

MongoDB

The latest

View all →
PHPxWorld - The resurgence of PHP meet-ups with Chris Morrell image

PHPxWorld - The resurgence of PHP meet-ups with Chris Morrell

Read article
Herd Executable Support and Pest 3 Mutation Testing in PhpStorm 2024.3 image

Herd Executable Support and Pest 3 Mutation Testing in PhpStorm 2024.3

Read article
Hide and safeguard emails from bots with the Muddle Laravel package image

Hide and safeguard emails from bots with the Muddle Laravel package

Read article
Dynamic Cache, Database, and Mail Builders in Laravel 11.31 image

Dynamic Cache, Database, and Mail Builders in Laravel 11.31

Read article
PHPStan 2.0 is Here image

PHPStan 2.0 is Here

Read article
Run multiple CLI commands locally at once with Solo for Laravel image

Run multiple CLI commands locally at once with Solo for Laravel

Read article