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, on or off parses as a boolean, and arrives in PHP as true or false rather than text. Scan the converted array for non-string values before shipping.
  • Dates and numbers. 2026-08-16 becomes a date, 1.10 becomes 1.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. &anchor and *alias get 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.

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