Avoiding unnecessary missing translation warnings when validating in Laravel when using handleMissingKeysUsing

Janez Cergolj

Hey everyone! Today, I want to share a quick tip about Laravel's Lang::handleMissingKeysUsing method. If you haven't heard of it, no worries – it's a neat feature that gets triggered when a translation key is missing.

The catch? Well, it tends to fire every time validation error messages are generated. Let's take a peek at the method responsible for this:

/**
     * Get the validation message for an attribute and rule.
     *
     * @param  string  $attribute
     * @param  string  $rule
     * @return string
     */
    protected function getMessage($attribute, $rule)
    {
        $attributeWithPlaceholders = $attribute;

        $attribute = $this->replacePlaceholderInString($attribute);

        $inlineMessage = $this->getInlineMessage($attribute, $rule);

        // First we will retrieve the custom message for the validation rule if one
        // exists. If a custom validation message is being used we'll return the
        // custom message, otherwise we'll keep searching for a valid message.
        if (! is_null($inlineMessage)) {
            return $inlineMessage;
        }

        $lowerRule = Str::snake($rule);

        $customKey = "validation.custom.{$attribute}.{$lowerRule}";

        $customMessage = $this->getCustomMessageFromTranslator(
            in_array($rule, $this->sizeRules)
                ? [$customKey.".{$this->getAttributeType($attribute)}", $customKey]
                : $customKey
        );

        // First we check for a custom defined validation message for the attribute
        // and rule. This allows the developer to specify specific messages for
        // only some attributes and rules that need to get specially formed.
        if ($customMessage !== $customKey) {
            return $customMessage;
        }

        // If the rule being validated is a "size" rule, we will need to gather the
        // specific error message for the type of attribute being validated such
        // as a number, file or string which all have different message types.
        elseif (in_array($rule, $this->sizeRules)) {
            return $this->getSizeMessage($attributeWithPlaceholders, $rule);
        }

        // Finally, if no developer specified messages have been set, and no other
        // special messages apply for this rule, we will just pull the default
        // messages out of the translator service for this validation rule.
        $key = "validation.{$lowerRule}";

        if ($key !== ($value = $this->translator->get($key))) {
            return $value;
        }

        return $this->getFromLocalArray(
            $attribute, $lowerRule, $this->fallbackMessages
        ) ?: $key;
    }

Alright, that's a pretty lengthy method. In a nutshell, it checks if a custom validation rule exists. If not, it looks for a general rule, and if all else fails, it falls back to a default message. The problem? Every time a translation is missing, Lang::handleMissingKeysUsing gets activated – not exactly what we want, right?

Below is my solution:

if (Str::of($key)->startsWith('validation.custom.') ||
    Str::of($key)->startsWith('validation.values.')) {
    return;
}

And here's the complete AppServiceProvider class, the place where my Lang::handleMissingKeysUsing is located:

<?php

namespace App\Providers;

use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Lang::handleMissingKeysUsing(function (string $key, array $replacements, string $locale) {
            if (! App::environment(['production', 'staging'])) {
                return;
            }

            if (Str::of($key)->startsWith('validation.custom.') ||
                Str::of($key)->startsWith('validation.values.')) {
                return;
            }

            // be notified of missing translation

            return $key;
        });
    }
}

That's all there is to it. Cheers!