Convert JSON to Laravel PHP array

Moving translations from a JavaScript stack or a translation platform export back into classic lang/ PHP files keeps them compatible with every Laravel version and package.

Why teams move back to PHP arrays

JSON is an excellent interchange format and a mediocre authoring format. Translations that arrive as JSON — exported from a translation platform, extracted from a JavaScript project, or handed over by an agency — are usually easier to live with once they are back in Laravel's native lang/{locale}/*.php structure. This converter rebuilds that structure, restoring nesting and PHP array syntax from the JSON tree.

Input — messages.json

{
    "welcome": "Welcome back, :name!",
    "cart": {
        "empty": "Your cart is empty",
        "items": ":count item|:count items"
    }
}

Output — lang/en/messages.php

<?php

return [
    'welcome' => 'Welcome back, :name!',
    'cart' => [
        'empty' => 'Your cart is empty',
        'items' => ':count item|:count items',
    ],
];

The output is a plain return statement with no dependencies, which means it works identically on Laravel 5 and Laravel 12, in a package, or in a legacy application that has never seen a JSON language file.

Values are copied verbatim. Laravel placeholders, pipe-separated plural forms, embedded HTML and apostrophes all survive, with quoting handled so that a string like It's empty does not break the generated file. Keys are emitted in the order the JSON supplied them rather than being alphabetised, so a diff against your previous language file stays readable instead of reshuffling every line.

What PHP arrays give you that JSON does not

  • Comments. You can annotate a string with the screen it appears on, its character limit, or a warning that changing it breaks a test. JSON has no comment syntax at all.
  • Opcache. PHP files are compiled once and cached as opcodes. A JSON language file is re-read and re-decoded on every request that touches it unless you cache it yourself.
  • No parse-failure cliff. A malformed JSON file throws at runtime and takes the page with it. A malformed PHP file fails at deploy time, in CI, where you want failures to happen.
  • Namespacing by file. Splitting translations across validation.php, passwords.php and invoices.php keeps merge conflicts local. One giant JSON blob guarantees every translator touches the same file.
  • Static analysis. PHPStan and IDE tooling can follow a PHP array. Nothing follows a JSON key.

Flat dotted keys versus real nesting

Translation platforms disagree about how to serialise nested keys. Some export true nesting; others flatten everything into dotted strings at the top level. Both are valid JSON and both are common, so check which one you have before converting.

Flat export — every key is a literal dotted string

{
    "cart.empty": "Your cart is empty",
    "cart.items": ":count item|:count items"
}

Converted literally, that produces a PHP array whose keys contain dots. Laravel will not resolve __('messages.cart.empty') against it, because the translator splits on dots and looks for a nested cart array that does not exist. The lookup fails and you get the raw key rendered on the page.

What you actually need

<?php

return [
    'cart' => [
        'empty' => 'Your cart is empty',
        'items' => ':count item|:count items',
    ],
];

If your JSON uses flat dotted keys, convert it to CSV first and back to PHP — the CSV path expands dot notation into real nesting by design. Alternatively use Arr::undot() on the decoded array before writing it out.

Typical migration paths

  1. 1 Leaving a translation platform. You export every locale as JSON, convert each to lang/{locale}/messages.php, commit them, and you own your copy again with no vendor in the critical path.
  2. 2 Absorbing a JavaScript front end. A Vue or React app is being folded into Blade. Its en.json becomes the seed for your PHP lang files, and the {name} placeholders get rewritten to :name on the way in.
  3. 3 Publishing a package. Package language files are conventionally PHP arrays so they can be published with vendor:publish and overridden per application. JSON is unusual there.
  4. 4 Recovering from strings-as-keys. A project that started with lang/es.json and full English sentences as keys becomes unmaintainable once copy changes. Converting to short-key PHP files is the first step out.

That last case deserves a warning: converting the file gets you the structure, but every __('Some full English sentence') call in your Blade templates still has to be rewritten to __('messages.some_key'). Budget for that work rather than discovering it halfway through.

Placeholder syntax differences to fix on arrival

JSON that came from a JavaScript project almost certainly uses a different interpolation syntax to Laravel. The converter treats values as opaque strings and will not rewrite them for you, which is the safe default — silent rewriting of message bodies is how translations get corrupted. Handle it explicitly:

Source Placeholder Laravel equivalent
i18next {{name}} :name
vue-i18n {name} :name
ICU MessageFormat {name} :name
Symfony %name% :name
Rails %{name} :name

Pluralisation is the harder half. ICU's {count, plural, one {# item} other {# items}} has no direct Laravel equivalent — Laravel expects :count item|:count items and picks a form using its own rules. Any string containing an ICU plural block needs rewriting by hand or with the pluralization tester to confirm the form count for each target locale.

Once the files are in place, run php artisan lang:missing or your own key-parity script across locales. Converted files are only as complete as the export you started from, and platforms routinely omit untranslated keys entirely rather than emitting empty strings.

Verifying the conversion before you commit

A converted language file can be syntactically perfect and still be wrong in ways that only surface in production, usually on the one page nobody clicked during review. Three checks catch almost everything, and all three are cheap enough to run in CI.

The first is key parity. Every locale should expose exactly the same key set as your source language. A translation platform that skips untranslated keys will hand you a Spanish file with four hundred keys where English has six hundred, and Laravel will silently fall back for the missing two hundred — or render raw keys if no fallback is configured.

A key-parity check you can run in CI

<?php

$source = require lang_path('en/messages.php');
$target = require lang_path('es/messages.php');

$missing = array_diff_key(Arr::dot($source), Arr::dot($target));

if ($missing !== []) {
    fwrite(STDERR, "Missing in es: " . implode(', ', array_keys($missing)) . PHP_EOL);
    exit(1);
}

The second is placeholder parity. If the English string contains :name and the translated one does not, a user sees a sentence with a hole in it. Worse, if a translator typed :nombre, Laravel will leave the literal text :nombre on the page. Extract every :token from both sides and compare the sets.

The third is plural form count. Laravel selects a form by splitting on pipes, so a locale needing three forms will misbehave if the translated string only supplies two. Count the pipes per key and compare against what the locale actually requires — Polish, Russian, Arabic and Welsh all need more forms than English.

Run all three before the pull request, not after the deploy. Converted files tend to be committed in bulk, so a single bad export can regress an entire locale at once, and translation bugs are notoriously invisible to reviewers who do not read the language.

Frequently asked questions

Does the converter keep :placeholders and plural pipes intact?

Yes. Values are treated as opaque strings, so Laravel placeholders like :name or :count and pipe-separated plural forms come through unchanged.

How is nesting handled?

Nested arrays in JSON map one-to-one to nested structures in Laravel PHP array; nothing is flattened.

Is my data uploaded or stored?

The conversion runs on our server but nothing is persisted: files are converted in memory and the response is returned immediately. For PHP input, only string, number and array literals are parsed — code is never executed.

Is there a size limit?

Yes, 200 KB per conversion, which covers even very large lang files. If you need bulk conversion across many locales, import your files into LangSyncer and export any format.

Converting files by hand gets old fast

Import your lang files into LangSyncer once, edit and translate them in a dashboard, and export any format — or skip files entirely with live translations.

We use cookies to improve your experience and analyze site traffic. Cookie Policy

Cookie Preferences

Essential

Required for the site to work

Analytics

Help us improve the site

Marketing

Personalized ads and content