Convert CSV to Laravel PHP array

When a translator returns a finished spreadsheet, this converts it straight back into a lang/ PHP file with the original nesting restored from dot notation.

Turning a finished spreadsheet back into a language file

This is the return leg of the translation handoff. A translator has filled in a two-column sheet, and you need it back as lang/{locale}/messages.php with the original nesting intact. The converter reads dot notation in the key column and rebuilds the tree, so cart.empty becomes a nested array entry rather than a literal key containing a full stop.

Input — messages_es.csv

key,value
welcome,"¡Bienvenido de nuevo, :name!"
cart.empty,"Tu carrito está vacío"
cart.items,":count artículo|:count artículos"

Output — lang/es/messages.php

<?php

return [
    'welcome' => '¡Bienvenido de nuevo, :name!',
    'cart' => [
        'empty' => 'Tu carrito está vacío',
        'items' => ':count artículo|:count artículos',
    ],
];

Rebuilding the nesting matters because Laravel resolves short keys by splitting on dots. A flat array whose keys literally contain dots will not answer __('messages.cart.empty') — the translator returns the key itself, and your page renders messages.cart.empty to a real user.

The first column is treated as the key and the second as the value. Extra columns such as source, context or notes are working material for the translator, not part of the language file, so trim the sheet down to key and value before converting.

Validate before you overwrite anything

A returned spreadsheet is untrusted input. It has passed through at least one spreadsheet application and at least one human, and both are capable of quietly damaging it. Run these checks before the output touches your repository — ideally as a script, because doing it by eye in a language you do not read is not a real check.

Key parity against the source. Compare the returned key column with the file you sent. Added keys usually mean someone typed into the wrong row; missing keys mean a row was deleted; reordered keys are harmless on their own but often accompany a sort operation that misaligned values.

Placeholder integrity. Every :token in the source must appear in the target. Translators localise enthusiastically and :name occasionally becomes :nombre, which Laravel will not substitute — it prints the literal text. Extract the token set from both sides and compare.

Placeholder check

<?php

preg_match_all('/:(\w+)/', $source, $expected);
preg_match_all('/:(\w+)/', $translated, $actual);

$drift = array_diff($expected[1], $actual[1]);

if ($drift !== []) {
    logger()->warning("Placeholder drift in {$key}", $drift);
}

Plural form counts. Laravel splits plural strings on the pipe character. If English supplied two forms and the target locale needs three — Polish, Russian, Czech and Arabic all do — a two-form translation will select the wrong text for entire ranges of numbers. Count pipes per row and check them against the locale's requirement using the pluralization tester.

Encoding. Open the returned file and look for é, ñ or literal question marks where accented characters belong. That is a UTF-8 file that was read as Latin-1 somewhere along the way. It is recoverable, but only if you catch it before the values are committed.

Smart quotes are the check people skip. Spreadsheets convert straight apostrophes to typographic ones automatically. The string still looks right, but if any code or test compares against the original it will no longer match.

Merge, do not replace

The instinct is to overwrite lang/es/messages.php with the converted output. Resist it. Between sending the spreadsheet and receiving it, your team almost certainly shipped features that added new keys, and those keys exist in your current Spanish file but not in the translator's sheet. A straight overwrite deletes them.

Merge instead, letting the returned translations win for keys they cover while preserving everything they do not mention:

A merge that cannot lose keys

<?php

$current = require lang_path('es/messages.php');
$returned = require storage_path('imports/messages_es.php');

// array_replace_recursive keeps keys absent from the returned file
$merged = array_replace_recursive($current, $returned);

file_put_contents(
    lang_path('es/messages.php'),
    "<?php\n\nreturn " . var_export($merged, true) . ";\n"
);

Commit the merge as its own change, separate from any code. A translation-only commit is reviewable by a native speaker and revertible without touching application logic, which is exactly what you want the first time a locale regresses.

Where this fits in a real workflow

  • Agency deliverables. The standard output of a paid translation job is a spreadsheet. This is how it becomes code.
  • Community translations. A shared Google Sheet is the lowest-friction way to let volunteers contribute a locale without teaching them Git.
  • Bulk copy edits. When someone rewrites two hundred strings for tone, editing the spreadsheet and re-importing beats two hundred individual edits.
  • Seeding a new locale. Machine-translate the source column, have a native speaker correct it in the sheet, then import the result.
  • Recovering from a bad export. When a translation platform mangles its own output, CSV is the neutral ground both sides can read.

The pattern works well for batch translation with a clear start and end. It scales badly when translation is continuous: each round trip needs an export, a wait, a validation pass and a merge, and if you are shipping weekly the spreadsheet is out of date before it comes back. That is the point at which teams move to translating in place rather than in transit.

Reading the CSV the way the spreadsheet wrote it

Before any of the content checks matter, the file has to parse into the rows you expect. CSV has no single authoritative dialect, and the spreadsheet that produced your file made several decisions on its own.

Symptom Cause Fix
Everything lands in one column File is semicolon-delimited Re-export choosing comma, or set the delimiter when parsing
First key looks like welcome UTF-8 byte order mark on line one Strip the BOM before reading the header row
A row splits into two Unescaped line break inside a value Ensure the value is quoted; use a real CSV parser, never explode()
Values show #NAME? Leading =, +, - or @ read as a formula Ask for the sheet formatted as Text and re-enter those rows
Trailing empty rows appear Spreadsheet exported blank cells below the data Skip rows with an empty key

Use a real parser rather than splitting strings. PHP's fgetcsv() and Laravel's League\Csv both handle quoting, embedded delimiters and multi-line values correctly, and every hand-rolled explode(',', $line) eventually meets a translated sentence that contains a comma.

One structural case is worth checking explicitly: a key that is both a leaf and a branch. If the sheet contains both cart and cart.empty, the tree cannot hold both a string and an array at the same position. One of them will win and the other will vanish silently. It is rare, but it happens whenever someone adds a parent-level label to an existing group, and the resulting bug looks like a translation that simply refuses to appear. Detect it by grouping the keys and flagging any key that is also a prefix of another, then rename one of the two before importing.

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 keys are flattened to dot notation in CSV (cart.empty) and re-expanded into nested structures when converting back.

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