Convert YAML to Laravel PHP array
Migrating from Symfony or Rails to Laravel usually starts with converting the YAML message catalogs into lang/ PHP arrays.
Bringing a Symfony or Rails catalogue into Laravel
Most YAML translation files arriving in a Laravel project come from one of three places: a Symfony application being rewritten, a Rails application being replaced, or a localisation platform whose canonical export format is YAML. In every case the destination is the same — lang/{locale}/{file}.php — and the message tree converts cleanly. What does not convert cleanly is the syntax inside the strings, which is where this migration actually costs time.
Input — messages.en.yaml
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',
],
];
Indentation becomes nesting, and the resulting array resolves under Laravel's dotted short keys: __('messages.cart.empty'). The output is a plain return with no dependencies, so it works in any Laravel version and in packages.
Treat the file conversion as the cheap part of the migration. Structure moves across in seconds; placeholders, plural forms and the assumptions your old framework baked into its catalogue are what take an afternoon. The sections below cover each in the order you will hit them, and it is worth reading them before converting twelve locales rather than after.
Strip the Rails locale root first
Rails wraps every translation file in a top-level key naming the locale. Symfony does not. If your YAML came from Rails and you convert it as-is, every key in your Laravel app gains an extra segment and nothing resolves.
Rails ships this
en:
welcome: 'Welcome back, %{name}!'
cart:
empty: 'Your cart is empty'
Converted literally, that produces $messages['en']['cart']['empty'], which means your lookups become __('messages.en.cart.empty') — with the locale hard-coded into the key, defeating the entire point of having locales. Remove the root key before converting, so welcome and cart sit at the top level. Laravel encodes the locale in the directory name instead.
A quick way to spot this: if the converted PHP file has exactly one top-level key and it happens to be a two-letter language code, you have a Rails root key.
Symfony encodes its equivalent information in the filename instead. A file called messages.fr.yaml declares both the domain and the locale, so when you convert it the domain becomes the Laravel filename and the locale becomes the directory: lang/fr/messages.php. Files named for other domains, such as validators.fr.yaml, map to their own Laravel file rather than being merged in.
Rewriting placeholders
The converter leaves values byte for byte intact, which is the right default — a converter that rewrites message bodies on your behalf is a converter that will eventually mangle one. That means placeholder syntax arrives in its source form and must be translated to Laravel's.
| Origin | Placeholder | Laravel |
|---|---|---|
| Symfony (legacy) | %name% |
:name |
| Symfony (ICU) | {name} |
:name |
| Rails | %{name} |
:name |
| Rails (interpolated) | #{name} |
:name |
Rewriting Rails placeholders across a converted file
<?php
$rewrite = function (array $messages) use (&$rewrite): array {
return array_map(function ($value) use ($rewrite) {
return is_array($value)
? $rewrite($value)
: preg_replace('/%\{(\w+)\}/', ':$1', $value);
}, $messages);
};
$messages = $rewrite(require lang_path('en/messages.php'));
Run the rewrite before you commit, then grep the result for any surviving % or { characters. A leftover %name% does not throw — Laravel simply prints it verbatim, so the bug reaches users as literal punctuation in the middle of a sentence.
Plurals are a rewrite, not a conversion
Every framework encodes plural forms differently, and none of the encodings are mechanically interchangeable. This is the part of a YAML migration that needs a human.
From Rails. Rails uses named sub-keys under the message key. A nested mapping with one and other children has to collapse into a single Laravel string with pipe-separated forms in the order Laravel expects.
Rails plural becomes a Laravel pipe string
# Rails
cart:
items:
one: '%{count} item'
other: '%{count} items'
# Laravel
'items' => ':count item|:count items',
From Symfony. Older Symfony catalogues use interval notation such as {0} none|{1} one|]1,Inf[ many. Laravel understands a similar explicit-range syntax, so these often survive with light editing — but Symfony has moved to ICU MessageFormat, and an ICU plural block has no Laravel equivalent at all. Those need rewriting by hand.
Critically, the number of forms is a property of the target language, not of the file you are converting. A Spanish file needs two forms; Polish needs three; Arabic needs six. If the source catalogue was incomplete, converting it faithfully preserves the incompleteness. Check each locale's real requirement with the pluralization tester rather than assuming the source got it right.
YAML quirks that survive into your PHP
-
Coerced types. An unquoted
no,yes,onoroffparses as a boolean, and arrives in PHP astrueorfalserather than text. Scan the converted array for non-string values before shipping. -
Dates and numbers.
2026-08-16becomes a date,1.10becomes1.1, and a version string quietly loses its trailing zero. - Duplicate keys. YAML permits them; most parsers keep the last silently. If two identical keys existed, one translation vanished during conversion and nothing reported it.
-
Anchors and aliases.
&anchorand*aliasget expanded at parse time, so shared fragments are duplicated into the output rather than kept as references. -
Multiple documents. A file containing
---separators holds several documents; only the first is usually read.
After converting, assert that every leaf is a string. It is a three-line check and it catches the entire type-coercion family at once, which is otherwise the kind of bug that reaches production because the affected string is on a page nobody in the team reads.
Catching coerced values after conversion
<?php
$suspect = [];
array_walk_recursive(
require lang_path('en/messages.php'),
function ($value, $key) use (&$suspect) {
if (! is_string($value)) {
$suspect[$key] = var_export($value, true);
}
}
);
// ['norwegian' => 'false', 'version' => '1.1']
Splitting one catalogue into Laravel files
Symfony and Rails both tend toward large catalogues — a single messages.en.yaml or en.yml holding everything the application says. Laravel expects several smaller files, each forming its own namespace, and the conversion is a good moment to impose that structure rather than inheriting one enormous array.
The split is not cosmetic. Laravel resolves short keys per file, so messages.title and invoices.title never collide, and each file is an independent unit for merge conflicts. Ten translators editing ten files produce far fewer conflicts than ten editing one.
| Source catalogue | Laravel file | Why separate |
|---|---|---|
| validation messages | lang/en/validation.php |
Framework-provided; already translated by Laravel-Lang |
| auth and password text | lang/en/auth.php |
Framework convention; rarely edited |
| pagination labels | lang/en/pagination.php |
Framework convention |
| everything you wrote | lang/en/messages.php |
Your copy, split further by domain as it grows |
Before importing validation and auth strings, check whether you need them at all. The open-source Laravel-Lang project maintains professionally translated framework strings for dozens of locales, so migrating your own versions across usually means carrying worse translations than the ones you would get for free.
Finally, convert one locale first and get a page rendering end to end before doing the remaining ones. Placeholder rewrites, plural restructuring and the Rails root key all fail in ways that look fine in the file and wrong in the browser, and discovering that after converting twelve locales means fixing it twelve times.
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 YAML 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.
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.