Skip to content

Optional analytics, session recording and advertising stay off unless you accept. You can reject them and use Transglot. Change your choice anytime in Cookie preferences. Cookie Policy

Back to the blog
Developer platform

Check Laravel JSON translations before they reach production

Check Laravel JSON translations for missing keys and changed placeholders, then validate reviewed locale files before release with an executable PHP example.

by transglot team
published November 9, 2026
8 min read

A translation can be valid JSON and still break a screen. Change :name to :nom, for example, and the application no longer has the placeholder it expects. Remove a message and a fallback can make the page look usable while hiding an incomplete language release.

This tutorial builds a small check for Laravel JSON translation files. It compares keys, checks named placeholders, and returns a failing exit code when a catalog needs attention. Then it connects that check to a translation workflow, with a separate review step before translated files enter a release.

The local exercise uses prepared English and French fixtures. You can run it without a translation account, API token, or network request. The optional Transglot section shows how to bring the same check into an existing localization process.

What you need

Use PHP 8.2 or newer for the standalone checker. To run the framework smoke check, use an existing Laravel application with its Composer dependencies installed; follow the PHP requirements for your Laravel version. The example was verified with Laravel 13 and PHP 8.5.

You will also need a disposable working directory or branch. The example adds lang/en.json, lang/fr.json, and files under scripts. Merge these into an existing project carefully if those paths already contain your own work.

Node.js 20+ is needed only for the optional Transglot CLI step. That step also requires a project with English as its source locale, French as a target, and an API-enabled plan, currently Starter or higher. A project admin must create a deploy token with both push and pull abilities.

1. Give the checker a small catalog

Create lang/en.json:

jsonjson
{
"Welcome, :name!": "Welcome, :name!",
"Save changes": "Save changes",
"Your plan renews on :date.": "Your plan renews on :date."
}

Create lang/fr.json:

jsonjson
{
"Welcome, :name!": "Bienvenue, :name !",
"Save changes": "Enregistrer les modifications",
"Your plan renews on :date.": "Votre abonnement sera renouvelé le :date."
}

These are illustrative translations prepared for the exercise, rather than captured service output. A language reviewer should approve wording for a real product.

Laravel's JSON catalogs use source phrases as keys, as described in its localization documentation. Keep those keys identical across locales. The translated text belongs in the value. This example uses a flat object and ordinary :name and :date placeholders, keeping the validation rule small enough to inspect.

In a Blade view, the greeting would be:

bladeblade
{{ __('Welcome, :name!', ['name' => 'Ada']) }}
Matching English and French paper slips preserve :name, while a separate slip with :prenom cannot fit the same cyan connector.
Translate the sentence while preserving the variable name the Laravel application passes.

2. Compare keys and placeholder names

Save the following as scripts/check-laravel-json.php. It has no Composer dependencies and does not modify either input file.

phpphp
<?php
declare(strict_types=1);
function catalog(string $path): array
{
if (!is_file($path) || !is_readable($path)) {
throw new RuntimeException("Cannot read {$path}");
}
$data = json_decode(file_get_contents($path), false, 512, JSON_THROW_ON_ERROR);
if (!$data instanceof stdClass) {
throw new RuntimeException("{$path} must contain a JSON object");
}
$entries = (array) $data;
foreach ($entries as $key => $value) {
if (!is_string($value)) {
throw new RuntimeException("{$path}: {$key} must have a string value");
}
}
return $entries;
}
function placeholders(string $text): array
{
preg_match_all('/:[A-Za-z_][A-Za-z0-9_]*/', $text, $matches);
$names = array_values(array_unique($matches[0]));
sort($names, SORT_STRING);
return $names;
}
if ($argc !== 3) {
fwrite(STDERR, "Usage: php check-laravel-json.php source.json target.json\n");
exit(2);
}
try {
$source = catalog($argv[1]);
$target = catalog($argv[2]);
$errors = [];
foreach (array_diff_key($source, $target) as $key => $_) {
$errors[] = "Missing key: {$key}";
}
foreach (array_diff_key($target, $source) as $key => $_) {
$errors[] = "Unexpected key: {$key}";
}
foreach (array_intersect_key($source, $target) as $key => $value) {
$expected = placeholders($value);
$found = placeholders($target[$key]);
if ($expected !== $found) {
$errors[] = "{$key}: placeholders differ (expected: "
.(implode(', ', $expected) ?: 'none').'; found: '
.(implode(', ', $found) ?: 'none').')';
}
}
if ($errors !== []) {
fwrite(STDERR, 'FAIL: '.implode("\nFAIL: ", $errors)."\n");
exit(1);
}
fwrite(STDOUT, 'OK: '.count($source)." keys; placeholders match.\n");
} catch (Throwable $error) {
fwrite(STDERR, 'ERROR: '.$error->getMessage()."\n");
exit(2);
}

Run it from your project root:

shsh
php scripts/check-laravel-json.php lang/en.json lang/fr.json

Expected output:

texttext
OK: 3 keys; placeholders match.

The checker compares placeholder names as a set. Translators can move a token within a sentence. Adding a token, deleting one, or changing its name causes a failure. A name used twice still appears once in the comparison; repetition is outside this check's scope.

Matching key sets is a release policy for this catalog. Projects intentionally shipping partial locale catalogs should define their own completeness rule instead of treating every missing key as acceptable by accident.

Source phrases are identifiers in this format. Changing punctuation in the English key creates a different lookup, even when the intended meaning stays the same. Review source edits alongside translated files and decide how to handle retired keys. A translation with an old identifier cannot satisfy a newly named message just because the two sentences look similar.

English and French Laravel JSON catalogs enter a structural checker. Exit 1 or 2 stops CI; exit 0 continues to review.
Compare source and target keys and placeholders before release. Failures stop CI; a passing check still needs language and layout review.

3. Make the failure visible

Create a separate file named fixtures/fr-broken.json with the same French catalog, changing only the greeting value to Bienvenue, :nom !.

shsh
php scripts/check-laravel-json.php lang/en.json fixtures/fr-broken.json

Expected output on standard error:

texttext
FAIL: Welcome, :name!: placeholders differ (expected: :name; found: :nom)

The process exits with code 1. Missing and unexpected keys also exit 1. Unreadable files, invalid JSON, arrays at the root, and non-string values exit 2. Successful validation exits 0, which makes the checker usable as a build step without interpreting its printed message.

Keep the broken fixture separate from lang/fr.json. A useful exercise should make the failure repeatable without leaving an application catalog damaged.

4. Check one message in Laravel itself

A file comparison does not prove the framework loads the file you intended. Save this as scripts/smoke-laravel.php in your installed Laravel application:

phpphp
<?php
declare(strict_types=1);
require getcwd().'/vendor/autoload.php';
use Illuminate\Filesystem\Filesystem;
use Illuminate\Translation\FileLoader;
use Illuminate\Translation\Translator;
$translator = new Translator(new FileLoader(new Filesystem, __DIR__.'/../lang'), 'fr');
$translator->setFallback('en');
$actual = $translator->get('Welcome, :name!', ['name' => 'Ada']);
if ($actual !== 'Bienvenue, Ada !') {
fwrite(STDERR, "FAIL: unexpected French greeting: {$actual}\n");
exit(1);
}
fwrite(STDOUT, $actual."\n");

Run php scripts/smoke-laravel.php from the application root. It should print Bienvenue, Ada !. It uses the installed translator without booting your application or connecting to a database. In your application tests, also exercise the actual locale selection and views; this isolated check does not cover them.

Laravel documents parameter replacement and locale selection separately. If you expose a locale in a request, restrict it to the languages your application supports.

5. Add a reviewed translation workflow

Once the local checks work, you can obtain translations through your usual process. Here is the optional CLI version using Transglot. Install it in the application:

shsh
npm install --save-dev transglot
npx transglot --version

Save this as transglot.json after confirming the base URL shown by your project's Deploy screen:

jsonjson
{
"url": "https://api.transglot.ai",
"sources": [
{
"path": "lang/en.json",
"format": "laravel_json",
"locales": ["en"]
}
],
"pull": [
{
"path": "lang/{locale}.json",
"format": "laravel_json",
"locales": ["fr"],
"min_state": "reviewed"
}
]
}

Set TRANSGLOT_TOKEN in your shell or CI secret store to a project deploy token created under Settings → Developer → API tokens. Keep the token out of the config file and source control. The token selects the project; this config does not contain a project ID.

Use the API base host, including https://, without appending /v1; the CLI adds the versioned path. For this exercise, use a dedicated project containing just this catalog. Pull exports project translations for the requested locale, so unrelated keys from another catalog would correctly trigger unexpected-key errors. An existing project with several catalogs needs a deliberate file and key mapping beyond this minimal example.

With auto-translate enabled on that project, upload the source and wait:

shsh
npx transglot push --wait

This sends source strings to the service and may consume its translation allowance. --wait waits for batches actually queued by that push. It does not enable automatic translation, generate work when nothing changed, or approve a translation. If auto-translate is off, translate through the editor instead.

Review the French messages in the editor and mark approved entries reviewed. Then download and validate them:

shsh
npx transglot pull
php scripts/check-laravel-json.php lang/en.json lang/fr.json
git diff -- lang/fr.json

The reviewed filter excludes entries that have not reached that state. A partially reviewed catalog may therefore fail key parity, which is useful: it keeps an incomplete language release visible. When the server returns zero entries, the CLI can refuse to overwrite an existing populated file. Investigate review state and translation progress before overriding that safeguard.

The initial push keeps existing server keys by default. If a source phrase was renamed, an older key can remain in an export. Decide whether it is still needed by another application version before removing it. The checker reports that extra key; it does not silently delete it from either the project or your files.

The developer localization workflow has further integration context. The local checker remains useful regardless of which translation service or review process supplies your files.

6. Validate the files you will release

Add the checker to your existing build or pull-request job:

shsh
php scripts/check-laravel-json.php lang/en.json lang/fr.json

Let its nonzero exit stop the job, and review translation diffs alongside source changes. Run the checker against the files that will actually ship. A successful check of an older checked-in file does not prove a remote translation is current.

For a larger application, run the comparison for each supported locale and keep the language list in one reviewed place. Use a small representative rendering test for messages with parameters, then exercise important user journeys in the browser. That combination gives a failed check a clear owner and a concrete message to fix.

This script covers flat JSON structure, catalog completeness, and simple named ASCII placeholders. It does not detect duplicate JSON members, interpret Laravel plural selectors, validate markup, assess fluency, or test layout. Add trans_choice cases for pluralized messages and browser checks for long text and right-to-left interfaces. Structural checks catch concrete mistakes; a reviewer still decides whether the words communicate the right thing.

colophon

Who wrote this, and what to read next

wrote this

transglot team

The byline on this post has no profile yet. Author profiles are managed alongside the posts themselves.

Engineering
keep reading

React localization: check keys, variables and plurals

Build a small React localization workflow with i18next. Catch missing keys, renamed variables and plural errors before deployment with a Node.js CI check.

November 9, 2026
10 min read
works with what you already run

41 connectors, already built.

the pipeline this post describes

Try it on your own strings. Free, no card.

Free plan: 100,000 words to start (25,000 at sign-up, 75,000 after a phone check), then 10,000 a month, 15 locales including your source, 3 seats.