Testing API Validation Errors With Different Locales
Published on by Paul Redmond
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-apicd locale-api
Next, let’s create the files we’ll use to demonstrate working with Locale in stateless APIs:
php artisan make:middleware DetectLocalephp artisan make:controller ProfileControllerphp 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.