Testing API Validation Errors With Different Locales

Testing API Validation Errors With Different Locales

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.


Filed in: Laravel Tutorials / tutorial


Newsletter

Join the weekly newsletter and never miss out on new tips, tutorials, and more.

Laravel News Partners

Laravel Jobs

Senior Laravel Developer (Full-Stack)
Munich
Volunteer Vision GmbH
Full-Stack Developer
Tampa
Nu Image Medical
Senior Software Developer
Remote (US ONLY)/Kenilworth, NJ
Diversified
Senior Back-end Laravel API Developer - Immediate Contract (Full Time)
Remote
ApproveMe // Document Signing
Web Development Project Manager
Houston, TX
Swyft Filings
Experienced Laravel/VueJS Developer (Freelance)
Remote (preferably in the US)
Prosperly LLC
Senior React / Laravel Developer
Remote
The C.A.S.E. Engineering Group