Convert Laravel PHP array to JSON
Laravel supports JSON translation files out of the box, and most JavaScript i18n libraries (vue-i18n, i18next) consume JSON directly, so converting PHP lang arrays to JSON is the most common migration step.
What actually changes when you convert
A Laravel PHP language file is executable code: it returns an array, and the framework resolves keys against it at runtime. A JSON language file is inert data parsed by json_decode(). The conversion looks like a formatting change, but it moves your translations from something PHP evaluates into something PHP reads, and that has consequences worth understanding before you commit the output to your repository.
The values themselves are the easy part. Every string survives byte for byte, including Laravel placeholders such as :name and :count, pipe-separated plural forms, HTML fragments, and any Unicode you have in there. What changes is the structure around those values and, crucially, how you will look them up in your Blade templates afterwards.
Input — lang/en/messages.php
<?php
return [
'welcome' => 'Welcome back, :name!',
'cart' => [
'empty' => 'Your cart is empty',
'items' => ':count item|:count items',
],
];
Output — messages.json
{
"welcome": "Welcome back, :name!",
"cart": {
"empty": "Your cart is empty",
"items": ":count item|:count items"
}
}
Note that the nesting is preserved rather than flattened. cart.empty does not become a literal key called "cart.empty" — it stays a nested object. That is the correct behaviour for most JavaScript i18n libraries, and it is what makes the conversion reversible without loss.
Laravel has two translation systems — pick the right one
This is the single most common source of confusion, and getting it wrong produces an app where every translated string silently renders as its own key. Laravel supports two entirely separate mechanisms, and they use JSON very differently.
The first is short keys. Files live at lang/{locale}/{file}.php and you address them by a dotted path that starts with the filename: __('messages.cart.empty'). This is the system your PHP array currently belongs to.
The second is translation strings as keys. A single file lives at lang/{locale}.json — note that it sits directly in lang/, not in a locale subdirectory — and the keys are the English source sentences themselves: __('Your cart is empty'). Laravel looks the sentence up and returns the translated version, falling back to the key itself when there is no match.
| Short keys | Strings as keys | |
|---|---|---|
| File location | lang/es/messages.php |
lang/es.json |
| Lookup | __('messages.welcome') |
__('Welcome back, :name!') |
| Missing translation | Renders the raw key | Renders the English source |
| Nesting | Supported and idiomatic | Not used — keys are flat sentences |
If you drop this converter's nested output into lang/es.json, Laravel will not find anything, because it expects sentence keys at the top level. Nested JSON belongs in a front-end bundle or in a tool that consumes it — not in Laravel's strings-as-keys file.
So the practical rule is: convert to JSON when the consumer is JavaScript, a translation platform, or an API. Keep PHP arrays when the consumer is Laravel itself and you are already using short keys. Converting your short-key files to JSON purely to "modernise" them means rewriting every __() call in your codebase, which is rarely worth it.
Feeding the output to a JavaScript i18n library
The most common legitimate reason to run this conversion is a Laravel back end sharing copy with a Vue, React, or Inertia front end. Both vue-i18n and i18next consume nested JSON directly, so the output drops straight into a locale bundle.
vue-i18n
import { createI18n } from 'vue-i18n'
import en from './locales/en.json'
import es from './locales/es.json'
const i18n = createI18n({
legacy: false,
locale: 'en',
fallbackLocale: 'en',
messages: { en, es },
})
// In a component: {{ t('cart.empty') }}
One incompatibility to plan for: Laravel's :name placeholder syntax is not what these libraries expect. vue-i18n uses {name} and i18next uses {{name}}. The converter deliberately leaves values untouched, so you will need a small transform pass if you want the same JSON to serve both runtimes.
Rewriting placeholders for vue-i18n
const toVueI18n = (obj) =>
Object.fromEntries(
Object.entries(obj).map(([key, value]) => [
key,
typeof value === 'object'
? toVueI18n(value)
: value.replace(/:(\w+)/g, '{$1}'),
])
)
Plural forms need attention too. Laravel's pipe syntax happens to match vue-i18n's own pipe convention for simple cases, but the two disagree the moment you use explicit ranges like {0} none|[1,19] some|[20,*] many. Test your plural strings against real numbers rather than assuming they carry over.
Where this conversion earns its keep
-
Sharing copy with a SPA. One source of truth in
lang/, exported to JSON at build time so the front end never drifts from the back end. - Uploading to a translation platform. Almost every TMS accepts JSON; far fewer parse PHP arrays, and those that do often mangle nested structures.
- Diffing translations in review. JSON diffs cleanly in a pull request. PHP arrays produce noisier diffs because formatting, trailing commas and array syntax all vary.
- Feeding a static site or mobile app. React Native, Flutter and most static generators read JSON natively and have no PHP runtime at all.
- Writing validation scripts. Checking that every locale has the same key set is a few lines against JSON; against PHP it means booting the framework or parsing code.
A build-step example: add a Composer or npm script that regenerates the front-end bundle whenever the canonical PHP files change, so the two never diverge. Because the conversion is deterministic, the generated JSON can safely be gitignored and rebuilt in CI.
Mistakes worth avoiding
- 1 Do not convert files containing PHP logic. If a lang file calls a function, concatenates variables, or returns anything other than literal strings and arrays, the parser will reject it. Language files should be pure data; if yours are not, fix that first.
-
2
Watch for numeric-looking keys. A PHP array with keys like
1,2,3is a list, and JSON will faithfully represent it as an object with string keys. That is usually what you want for translations, but it will surprise code expecting an array. -
3
Check your encoding. The output is UTF-8 without escaping, so accented characters and non-Latin scripts appear literally rather than as
\u00e9sequences. That is more readable and equally valid, but some older parsers insist on ASCII-escaped JSON. -
4
Do not lose your comments. PHP arrays can carry
//comments explaining tricky strings to translators. JSON has no comment syntax, so that context is dropped. Move it into a translator-notes field in your TMS before you convert.
Finally, treat the conversion as a migration rather than a sync. Running it once and committing the result is fine. Running it repeatedly in both directions, by hand, across a dozen locales is exactly the kind of work that quietly rots — which is the problem a managed translation layer exists to solve.
One more structural consideration: Laravel resolves short keys per file, so lang/en/messages.php and lang/en/validation.php are separate namespaces that never collide. JSON has no equivalent boundary. If you merge several PHP files into one JSON bundle, prefix each file's tree with its original filename, otherwise a title key from one file will quietly overwrite a title key from another and you will lose a string with no error anywhere.
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 Laravel PHP array map one-to-one to nested structures in JSON; 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.
Other free tools
Untranslated Key Detector
Scan any website for leaked i18n keys: dot-notation identifiers, missing-translation markers and unrendered placeholders your users can see.
Blade Hardcoded String Scanner
Paste a Blade template and find text that should be behind __() — with suggested keys and a generated lang file.
Pluralization Tester
Pick a language, type a number, see which CLDR plural category applies — live, via your browser's Intl.PluralRules.
AI Translation Cost Calculator
Keys × languages = quota units. Estimate the initial run and the monthly churn for your project.
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.