Testing API Validation Errors With Different Locales

Published on by

Testing API Validation Errors With Different Locales image

Have you ever wondered how to provide API validation errors and translations for different locales? This beginner post will show you how to get started with setting a locale in a stateless Laravel API and testing various locale validation messages.

On a related note, if you want a quick way to pull translations into your project for validation, we’ve covered a package called Laravel-lang – Translations for Laravel. Larvel-lang has translations for over 68 languages and including translations for things like authentication and validation (among other things).

For now we’ll skip installing this package and get started with the built-in features that Laravel provides for Locale support.

Getting Started

First, we’re going to create a new Laravel project to get started. This tutorial isn’t going to leverage any Locale packages.

laravel new locale-api
cd locale-api

Next, let’s create the files we’ll use to demonstrate working with Locale in stateless APIs:

php artisan make:middleware DetectLocale
php artisan make:controller ProfileController
php artisan make:test LocaleDetectionTest
 
mkdir -p resources/lang/es/
touch resources/lang/es/validation.php

We will set the API Locale through a middleware, which will try to set a preferred locale. We create a controller and test so that we can demonstrate some validation tests we can perform with our API.

Last, we are creating a new validation file for Spanish to demonstrate the Localization of our API validation errors.

We’re not going to install the full Laravel-lang package, but for this tutorial, grab the contents of es/validation.php and add it to the resources/lang/es/validation.php file we created above.

The Middleware

Though I typically use TDD, we’re going to create the middleware and controller first, and then create a test as a means of using our middleware. The DetectLocale middleware is simple for our purposes since APIs are stateless; we will rely on the user sending an Accept-Locale header along with the request. You could also check for a query string param to set the locale, but we’ll keep it simple:

<?php
 
namespace App\Http\Middleware;
 
use Closure;
 
class DetectLocale
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param mixed ...$locales
* @return mixed
*/
public function handle($request, Closure $next, ...$locales)
{
$locales = $locales ?: ['en'];
 
if ($language = $request->getPreferredLanguage($locales)) {
app()->setLocale($language);
}
 
return $next($request);
}
}

Our middleware takes a $locales configuration, which is an array of supported languages. We then call the super-convenient Request::getPreferredLanguage() method provided by the Symfony HTTP Foundation component, which takes an ordered array of available locales and returns the preferred locale.

If the method returns a preferred locale, we set it via Laravel’s App::setLocale() method.

Next, we configure the middleware in the app/Http/Kernel.php file:

protected $routeMiddleware = [
// ...
'locale' => \App\Http\Middleware\DetectLocale::class,
];

Last, in the same file we add the locale middleware to the api group with the preferred locales:

protected $middlewareGroups = [
// ...
'api' => [
'throttle:60,1',
'bindings',
'locale:en,es',
],
];

The Controller

We’re not going to write out a full controller; we are only going to create a simple validation check to trigger some validation rules:

<?php
 
namespace App\Http\Controllers;
 
use Illuminate\Http\Request;
 
class ProfileController extends Controller
{
public function store(Request $request)
{
$request->validate([
'name' => 'required',
'email' => 'required|email'
]);
 
return response()->json(['status' => 'OK']);
}
}

This controller is going to validate name and email inputs, which will throw an exception if validation fails, returning a 422 Unprocessable Entity statue code and JSON validation errors.

Next, we need to define the route in routes/api.php:

<?php
 
Route::post('/profile', 'ProfileController@store');

Next, we’ll use the example test case in the feature test we created in LocaleDetectionTest:

<?php
 
namespace Tests\Feature;
 
use Illuminate\Http\Response;
use Tests\TestCase;
 
class LocaleDetectionTest extends TestCase
{
/**
* A basic feature test example.
*
* @return void
*/
public function testExample()
{
$response = $this
->withHeaders([
'Accept-Language' => 'es',
])
->postJson('/api/profile', [
'email' => 'invalid.email',
]);
 
$response
->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY)
->assertJsonPath('errors.email', ['correo electrónico no es un correo válido'])
->assertJsonPath('errors.name', ['El campo nombre es obligatorio.']);
}
}

Our test makes a JSON post request to our profile endpoint and sends the Accept-Language header with es for Spanish. As you can see by running the tests, they pass when we assert the JSON returned using the newly contributed assertJsonPath() added in Laravel 6.0.4.

If you dump out the JSON as an array, you can see that the message is in English:

dd($response->decodeResponseJson());
..array:2 [
"message" => "The given data was invalid."
"errors" => array:2 [
"name" => array:1 [
0 => "El campo nombre es obligatorio."
]
"email" => array:1 [
0 => "correo electrónico no es un correo válido"
]
]
]

We won’t dive into solving this in this tutorial, but perhaps we will write a follow-up going more into detail in translating other parts of an API. A simple solution might be to catch ValidationException messages and use a translation for the message.

Another thing to consider is experimenting with what happens when you send a language that you don’t support. I’ll give you a hint: Symonfy’s getPreferredLanguage() will return the first preferred language that you pass it (in our case, en, which is first in the array). Play around with changing the order of preferred locales and testing with the use of an unsupported locale.

Well, that’s it for our brief walk-through of a simple way to set locale in a Laravel API. I’d like to credit Yaz Jallad (@ninjaparade), who was discussing this topic with me yesterday. From our discussion I was curious about how to add validation translations to an API—which I’ve never had to do before—and figured I’d write a quick post after digging in for a bit.

To learn more about Localization in Laravel, check out the Localization documentation.

Paul Redmond photo

Staff writer at Laravel News. Full stack web developer and author.

Cube

Laravel Newsletter

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

image
No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project.

Visit No Compromises
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
LoadForge logo

LoadForge

Easy, affordable load testing and stress tests for websites, APIs and databases.

LoadForge
Paragraph logo

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.

Paragraph
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
DocuWriter.ai logo

DocuWriter.ai

Save hours of manually writing Code Documentation, Comments & DocBlocks, Test suites and Refactoring.

DocuWriter.ai
Rector logo

Rector

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

Rector

The latest

View all →
Generate Code Coverage in Laravel With PCOV image

Generate Code Coverage in Laravel With PCOV

Read article
Non-backed Enums in Database Queries and a withSchedule() bootstrap method in Laravel 11.1 image

Non-backed Enums in Database Queries and a withSchedule() bootstrap method in Laravel 11.1

Read article
Laravel Pint --bail Flag image

Laravel Pint --bail Flag

Read article
Laravel Herd for Windows is now released! image

Laravel Herd for Windows is now released!

Read article
The Laravel Worldwide Meetup is Today image

The Laravel Worldwide Meetup is Today

Read article
Cache Routes with Cloudflare in Laravel image

Cache Routes with Cloudflare in Laravel

Read article