vue-i18n locale files: JSON, YAML, pipe plurals, and {named} placeholders
vue-i18n is the standard i18n library for Vue 3 (and Nuxt via @nuxtjs/i18n). It supports JSON and YAML locale files, pipe-separated plurals, named placeholders, and linked messages. Here is how each feature works — and the mistakes that trip people up.
Folder structure
The most common layout is a locales/ directory with one file per locale. Each file contains every translation key for that language:
my-vue-app/
├── src/
│ ├── i18n.ts ← createI18n() setup
│ └── ...
├── locales/
│ ├── en.json ← English (source)
│ ├── de.json ← German
│ ├── fr.json ← French
│ └── ja.json ← Japanese
└── package.jsonIn your i18n.ts setup file, import each locale and pass it to createI18n():
import { createI18n } from 'vue-i18n'
import en from '../locales/en.json'
import de from '../locales/de.json'
const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
messages: { en, de },
})
export default i18n@nuxtjs/i18n auto-discover locale files from a configured directory — you typically only set the path in nuxt.config.ts.Basic JSON format
Locale files can use flat keys or nested objects. vue-i18n resolves nested keys with dot notation:
{
"hello": "Hello!",
"nav": {
"home": "Home",
"about": "About us",
"contact": "Contact"
},
"footer": {
"copyright": "© 2026 My App"
}
}In a Vue template, use $t() to access any key:
<template>
<h1>{{ $t('hello') }}</h1>
<nav>
<a href="/">{{ $t('nav.home') }}</a>
<a href="/about">{{ $t('nav.about') }}</a>
</nav>
<footer>{{ $t('footer.copyright') }}</footer>
</template>{named} placeholders
vue-i18n uses single curly braces for named placeholders — not double braces like some other libraries:
{
"welcome": "Welcome, {name}!",
"itemsInCart": "You have {count} items in your cart.",
"orderConfirm": "Order #{orderId} confirmed for {email}."
}Pass the values as the second argument to $t():
<template>
<p>{{ $t('welcome', { name: 'Alice' }) }}</p>
<!-- Output: "Welcome, Alice!" -->
<p>{{ $t('itemsInCart', { count: 3 }) }}</p>
<!-- Output: "You have 3 items in your cart." -->
</template>{{name}} (double braces) will not work — vue-i18n expects {name}. Double braces are Vue template syntax, not vue-i18n syntax.Pipe-separated plurals
vue-i18n handles pluralization by splitting a single string on the pipe character |. The segments map to plural forms based on the count:
{
"car": "car | cars",
"apple": "no apples | one apple | {count} apples",
"item": "no items | one item | {n} items"
}How the segments are resolved:
- •Two segments (
"car | cars"): first for count 1, second for everything else. - •Three segments (
"no apples | one apple | {count} apples"): first for 0, second for 1, third for 2+.
Use $t() with a count parameter or the legacy $tc() helper:
<template>
<!-- Vue I18n v9+ (Composition API or Legacy) -->
<p>{{ $t('apple', { count: 0 }) }}</p>
<!-- Output: "no apples" -->
<p>{{ $t('apple', { count: 1 }) }}</p>
<!-- Output: "one apple" -->
<p>{{ $t('apple', { count: 5 }) }}</p>
<!-- Output: "5 apples" -->
<!-- Legacy $tc() — still works but $t() with plural is preferred -->
<p>{{ $tc('car', 2) }}</p>
<!-- Output: "cars" -->
</template>{n} is automatically injected with the count value. You can also use any named placeholder like {count} and pass it explicitly.YAML support
vue-i18n supports YAML locale files alongside JSON. The structure is identical — only the syntax differs. YAML is popular for its readability and lack of trailing-comma issues:
# locales/en.yaml
hello: Hello!
nav:
home: Home
about: About us
contact: Contact
welcome: "Welcome, {name}!"
car: car | cars
apple: "no apples | one apple | {count} apples"To use YAML, install @intlify/unplugin-vue-i18n (or the Vite/Webpack plugin) which handles YAML parsing at build time. Nuxt’s @nuxtjs/i18n supports YAML out of the box.
car: car | cars works, but welcome: Welcome, {name}! will fail because of the braces. Always wrap complex values in double quotes.Linked locale messages
The @:key syntax lets one message reference another, reducing duplication:
{
"the_world": "the world",
"dio": "DIO",
"greeting": "hello, @:the_world!",
"villainGreeting": "@:dio says @:greeting"
}$t('greeting') resolves to "hello, the world!". You can also apply modifiers to change the case of linked text:
{
"homeAddress": "Home address",
"missing498": "Please provide @.lower:homeAddress",
"heading": "@.upper:homeAddress",
"label": "@.capitalize:homeAddress"
}- •
@.upper:key— converts the linked value to uppercase. - •
@.lower:key— converts the linked value to lowercase. - •
@.capitalize:key— capitalizes the first letter.
SFC <i18n> blocks
Vue Single File Components can embed translations directly using a custom <i18n> block. This keeps component-specific translations co-located with the component:
<template>
<h1>{{ $t('title') }}</h1>
<p>{{ $t('description') }}</p>
</template>
<script setup>
// component logic
</script>
<i18n>
{
"en": {
"title": "My Component",
"description": "This component does something useful."
},
"de": {
"title": "Meine Komponente",
"description": "Diese Komponente macht etwas Nützliches."
}
}
</i18n>The <i18n> block also supports YAML by adding lang="yaml":
<i18n lang="yaml">
en:
title: My Component
description: This component does something useful.
de:
title: Meine Komponente
description: Diese Komponente macht etwas Nützliches.
</i18n><i18n> blocks require the @intlify/unplugin-vue-i18n Vite or Webpack plugin. Without it the block is silently ignored.Common mistakes
Forgetting spaces around pipes
Writing "car|cars" instead of "car | cars". vue-i18n requires a space before and after each pipe for plural splitting. Without spaces the entire string is treated as a single form.
Using double braces instead of single
Writing {{name}} in the locale file. vue-i18n uses {name} (single braces). Double braces are Vue template interpolation, not vue-i18n syntax.
Mixing $t and $tc incorrectly
In vue-i18n v9+, $t() handles pluralization when you pass a count parameter. The legacy $tc() still works but mixing the two in the same project leads to confusion. Pick one and be consistent.
YAML indentation errors
YAML is whitespace-sensitive. Using tabs instead of spaces, or inconsistent indentation, silently breaks nested keys. Always use 2-space indentation and validate with a YAML linter.
Unquoted YAML values with special characters
YAML values containing {, :, or # must be quoted. Without quotes, welcome: Welcome, {name}! is a YAML parse error.
vue-i18n format at a glance
| Feature | vue-i18n |
|---|---|
| File path | locales/en.json or locales/en.yaml |
| Interpolation | {named} single-brace placeholders |
| Plurals | Pipe-separated: "car | cars" |
| Linked messages | @:key and @.modifier:key |
| SFC support | <i18n> blocks in .vue files |
| YAML support | Yes, via @intlify/unplugin-vue-i18n |
| Nested keys | Yes, dot-notation access |
Translate your vue-i18n locale files with LocalePack
LocalePack preserves pipe plurals (car | cars), keeps every {named} placeholder intact, and supports both JSON and YAML. Upload your source locale, select target languages, and download ready-to-use translations. Pay once, no subscription.
Related guides
i18next JSON format: namespaces, plurals, and interpolation
The i18next locale file reference — namespace-based splitting, _plural suffixes, and {{interpolation}} syntax compared to vue-i18n.
Next.js i18n: next-intl vs react-i18next
A side-by-side comparison of the two leading Next.js i18n libraries — useful if your Vue project also has a Next.js counterpart.