Building a Laravel Translation Package – Handling Missing Translation Keys
Published on by Joe Dixon
In the last instalment of this series, we talked about building the frontend translation management tool. In this article, we are going to move away from the frontend and follow the process of building another backend feature.
One of the most frustrating things about translation management in a Laravel application is forgetting to add translations to the corresponding language file. This has the undesirable result of either the translation key or the default language being rendered on the page rather than the translation for the current language.

To mitigate this issue, the Laravel Translation package provides a way to scan a project for translations that don’t exist in the translation files for all languages supported by the application.
We achieve the scanning part of this process by recursively looking through all files of the configured directories for instances of any of the Laravel translation retrieval methods (e.g. __()
, trans()
). These methods are captured using regular expressions and the translation strings extracted. These strings can be compared against the existing translations to see whether or not they already exist or need to be created.
Configuration
In the package configuration, there are two keys defined to make the scanner more flexible and efficient.
'translation_methods' => ['trans', '__']
The translation_methods
option allows the user to provide an array of translation retrieval methods which the scanner uses to look for keys.
'scan_paths' => [app_path(), resource_path()]
The scan_paths
key allows the user to define an array of directories for the scanner to look through when searching for translation keys.
Of course, it’s possible to use base_path()
here to search through the whole project, but defining only the directories where you expect to find translations will add significant speed improvements to the scanning process.
Implementation
The scanning functionality is handled by a single class. This class accepts an instance of Laravel’s Illuminate\Filesystem\Filesystem
, which it uses to traverse directories and interact with files, along with the array of translation methods and scan paths outlined above.
The class contains a single method findTranslations
, which is responsible for carrying out the task. It utilises the following regular expression which is influenced by a combination of those found in Barry vd. Heuvel’s Laravel Translation Manager package and Mohamed Said’s Laravel Language Manager package.
$matchingPattern = '[^\w]'. // Must not start with any alphanum or _ '(?<!->)'. // Must not start with -> '('.implode('|', $this->translationMethods).')'. // Must start with one of the functions "\(". // Match opening parentheses "[\'\"]". // Match " or ' '('. // Start a new group to match: '.+'. // Must start with group ')'. // Close group "[\'\"]". // Closing quote "[\),]"; // Close parentheses or new parameter
The method recursively iterates over all of the files in the provided directories using the regular expression to find instances of the provided translation retrieval methods, returning an array of all of the matches.
On each match, an additional check is performed to determine whether the match is a group translation (php array style) or single translation (JSON style). This is done by simply checking whether or not the match contains a period. If so, everything before the period is the file and everything after is the key (e.g. you could find the translation for validation.accepted
by getting the accepted
key from the validation.php
file).
In the end, we have an array which looks similar to the following:
[ 'single' => [ 'Welcome' => 'Welcome' ], 'group' => [ 'validation' => [ 'accepted' => 'The :attribute must be accepted.', ... ], ],];
Of course, doing this will give us every translation found in the configured paths, so how do we go about determining those which are missing? It’s simple really, we now have all the tagged translations in the app in an array and we can use our file driver created earlier in the series to get all the translations in the language files in an array format.
Not only that, but the format of the two arrays will be the same, so all we need to do is diff the two. We can conclude that anything in the array of translations gathered from scanning the app which doesn’t appear in the language file translations needs to be added to the relevant language file.
To do this, we simply iterate over the missing translations and utilise the methods already built in the file driver to add them.
$missingTranslations = $this->findMissingTranslations($language); if (isset($missingTranslations['single'])) { foreach ($missingTranslations['single'] as $key => $value) { $this->addSingleTranslation($language, $key); }} if (isset($missingTranslations['group'])) { foreach ($missingTranslations['group'] as $group => $keys) { foreach ($keys as $key => $value) { $this->addGroupTranslation($language, "{$group}.{$key}"); } }}
This article brings us to the end of the file-based functionality of the translation package. Next time, we will utilise the groundwork we have in place to start our database driver. See you next time, and as usual, if you have any questions, please feel free to contact me on Twitter.