Convert YAML to JSON
Convert Symfony or Rails YAML catalogs into JSON for Laravel JSON files or JavaScript i18n libraries.
Getting a YAML catalogue into something a bundler can read
YAML is pleasant to edit and awkward to ship. Browsers cannot parse it, bundlers need a loader for it, and shipping a YAML parser to the client to read translation data is a needless dependency. Converting to JSON produces something every runtime already understands, at roughly a tenth of the parsing cost.
Input — messages.yaml
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"
}
}
Indentation becomes nesting, and the resulting object drops straight into vue-i18n, i18next or a Laravel JSON language file. The output is UTF-8 without ASCII escaping, so accented characters and non-Latin scripts stay legible rather than becoming \u00e9 sequences.
There is a performance argument alongside the compatibility one. Browsers parse JSON with a native, heavily optimised routine, while YAML requires a JavaScript parser measured in tens of kilobytes that then runs far slower. For a translation bundle loaded on every page view, shipping JSON instead is one of the cheapest wins available.
Where types get lost on the way in
This conversion has one significant asymmetry, and it runs in the direction people rarely check. YAML infers types from unquoted values, so by the time the parser hands you a tree, some of your translation strings may no longer be strings at all. JSON then faithfully records whatever the YAML parser decided.
What unquoted YAML does to a language list
# Input
languages:
norwegian: no
version: 1.10
released: 2026-08-16
# Output — none of these are strings any more
{
"languages": {
"norwegian": false,
"version": 1.1,
"released": "2026-08-16T00:00:00Z"
}
}
The Norwegian language code becoming false is the canonical example, and it has broken real production systems. The version string losing its trailing zero is subtler and arguably worse, because 1.1 looks plausible everywhere it appears. Neither produces an error at any point in the pipeline.
The asymmetry is worth stating plainly: converting JSON to YAML is lossless, because JSON has already committed to a type for every value. Converting YAML to JSON is not, because the YAML parser makes those decisions for you and the JSON output records the result as though it had always been intended.
Guard against it by asserting that every leaf in the converted JSON is a string. A translation bundle should contain nothing else; numbers and booleans belong in configuration, not in copy. The check is a few lines and it catches the whole family at once.
Asserting every value is a string
const nonStrings = []
const check = (node, path = '') => {
for (const [key, value] of Object.entries(node)) {
const at = path ? `${path}.${key}` : key
if (value !== null && typeof value === 'object') check(value, at)
else if (typeof value !== 'string') nonStrings.push([at, value])
}
}
check(messages)
if (nonStrings.length) throw new Error(JSON.stringify(nonStrings))
YAML features that flatten out during conversion
YAML has capabilities JSON simply does not, and they resolve at parse time rather than surviving into the output. None of these break the conversion, but each changes what the file looks like afterwards.
-
Comments disappear. Every
#annotation explaining a tricky string to translators is gone, permanently. If those notes matter, move them into a translator-notes field before converting. -
Anchors and aliases expand.
&sharedand*sharedare resolved, so a fragment referenced in ten places is duplicated ten times in the JSON. Editing it afterwards means editing all ten. -
Merge keys are applied.
<<:inheritance is flattened into the resulting object, which is usually what you want but makes the output considerably larger than the source. -
Only the first document survives. A file with
---separators holds several documents; JSON has no equivalent, so the rest are typically dropped. - Duplicate keys collapse. YAML permits them and most parsers keep the last silently, so a translation can vanish with no warning.
Because the conversion is deterministic, the sensible pattern is to treat YAML as the source you edit and JSON as a build artefact you generate. Commit the YAML, gitignore the JSON, and regenerate it in CI. That way the comments and anchors keep their value for the humans while the machines get the format they actually want.
Fitting the output to its consumer
Nested JSON is the common shape, but each runtime expects something slightly different around it, and the mismatches fail quietly rather than loudly.
| Consumer | Shape | Placeholder |
|---|---|---|
| vue-i18n | Nested object per locale | {name} |
| i18next | Nested, optionally namespaced | {{name}} |
| React Intl | Flat keys, ICU syntax | {name} |
| Laravel JSON | Flat, source sentence as key | :name |
If the YAML came from Rails, strip the top-level locale key first — a file wrapped in en: converts into an object with a single en property, and every lookup path gains a segment that should not be there. Symfony catalogues have no such wrapper, but they do use %name% placeholders that need rewriting for a JavaScript runtime.
Plural forms need the most attention. Rails encodes them as one and other sub-keys, Laravel as pipe-separated forms in one string, and ICU as a structured message block. None of these convert mechanically into another, and how many forms a locale needs is a property of the language rather than the file — check each target with the pluralization tester before rewriting.
Making it a build step
Because the conversion is deterministic, the durable arrangement is to convert automatically rather than by hand. The YAML is the file people edit and review; the JSON is an artefact regenerated whenever the YAML changes.
- 1 Commit the YAML, ignore the JSON. One source of truth, no possibility of the two drifting apart, and no merge conflicts in a generated file.
- 2 Regenerate in CI. Convert during the build so a malformed YAML file fails the pipeline rather than the browser.
- 3 Validate after conversion. Assert that every leaf is a string and that all locales expose the same key set. Both checks are cheap and catch the failures that otherwise reach users.
-
4
Rewrite placeholders in the same pass. If the front end needs
{{name}}and the catalogue holds:name, do it here rather than asking translators to think about syntax. - 5 Fail loudly on missing keys. A locale that silently falls back looks fine in testing and looks broken to the users who speak that language.
Keep the generated file out of code review as well. A regenerated bundle produces a large mechanical diff that buries the two lines a reviewer actually needs to see, and reviewers who learn to skim generated diffs will eventually skim a real change.
The one thing worth preserving manually is the comments, since they do not survive. Before adopting this pipeline, move any translator guidance out of YAML comments and into a field that travels with the string, or it will quietly be lost the first time someone regenerates the bundle.
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 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.