Difficulties you might encounter when using vue-i18n in real-world Vue.js applications

August 8, 2021 · 5min

After few months of frustration with trying to use the "de-facto" internationalization library for Vue.js - vue-i18n, I've decided it is time to replace it. And that is why I have created fluent-vue. I will write more about it and Fluent syntax it uses in my following blog posts.

In this blog post, I try to explain what problems I have encountered when trying to use the vue-i18n library in my app, and how fluent-vue and Fluent syntax solve them.

vue-i18n good parts:

Firstly, this is what I liked in vue-i18n:

1. Component interpolation

Component interpolation allows using components inside translation messages. Nice way of reducing v-html directive usages.

2. SFC custom blocks

Keeping translations for the component in the same file as template and js code is really convenient.

3. Tooling

Being the most used Vue.js internationalization library, it has a heap of useful packages and extensions.

vue-i18n's issues:

And this is what I didn't like in vue-i18n or what didn't work for my project:

1. Complicated API for developers

vue-i18n has 5 different methods: ($t, $tc, $te, $d, $n). It has separate methods for formatting simple text, pluralized text, date, and numbers.
fluent-vue has only 2 methods and one of them is rarely used.

2. "Leaky" localizations

Grammar of source language limits what features translators can use and leaks into app code and translations messages of other languages.

Example (pluralization):

If you want translators to be able to use pluralization, you need to use $tc method. Even if you don't need it for your source language. You cannot just write:

const messages = {
  en: {
    'copy-n-files': 'Copy {count} files'

$t('copy-n-files', { count: filesCount })

You need to use $tc method with additional parameter:

$tc('copy-n-files', filesCount, { count: filesCount })

And translators still have no way of knowing, without checking application code, whether translation that uses the following format would be pluralized.

const messages = {
  en: {
    'copy-n-files': 'Copy {count} file | Copy {count} files'

On top of that, if translator tries to use this syntax and developer did not use $tc method, it will not be pluralized and you will see both choice variants displayed in your app.

fluent-vue solution:

copy-n-files = { $count -> 
    [one] Copy file
   *[other] Copy {$count} files
$t('copy-n-files', { count: 5 })

This syntax can be used in any translation message to choose an option based on plural category, or even a concrete value.

3. Translators do not have control over translations

Developers are forced to make choices that translators should make: "should translation message be pluralized?", "what date and number format to use?".

Example (date format):

vue-i18n has a fixed number of developer-predefined date formats and developer decides what format to use in each case.

const dateTimeFormats = {
  'en': {
    short: {
      year: 'numeric',
      month: 'short',
      day: 'numeric'
    long: {

const messages = {
  'en': {
    'last-online': 'User was last online at {date}'

$t('last-online', { date: $d(new Date(), 'short') })

Translators cannot change date formatting for a particular translation, for example, if it does not fit into UI in some language.

fluent-vue solution:

Fluent syntax allows translators to call custom function in translation messages. There is built in DATETIME function:

last-online = User was last online at { DATETIME($date, year: "numeric", month: "short", month: "short") }
$t('last-online', { date: new Date() })

If you want to have predefined date formats it can easily be implemented using a custom function. But translators will still be able to choose what format to use in each case.

4. Syntax is not powerful enough

Even with $tc method there is no way to have pluralization that depends on counts of 2 or more objects:

$tc('apples-and-bananas', /* what should go here? */, { appleCount: 1, bananaCount: 5 })

const messages = {
  en: {
    'apples-and-bananas': '{appleCount} apples and {bananaCount} bananas'

One possible solution for this issue is splitting translation into three different ones. But is does not look particularly good:

$t('apples-and-bananas', {
  appleCountText: $tc('apples', 1, { appleCount: 1 }),
  bananaCountText: $tc('banana', 5, { bananaCount: 5 })

const messages = {
  en: {
    'apples-and-bananas': '{appleCountText} and {bananaCountText}',
    'apples': '{appleCount} apple | {appleCount} apples',
    'bananas': '{bananaCount} banana | {bananaCount} bananas'

fluent-vue solution:

Thanks to Fluent syntax you can write translation, without splitting it, like this:

$t('apples-and-bananas', { appleCount: 1, bananaCount: 5 })
apples-and-bananas = {$appleCount -> 
    [one] An apple
   *[other] {$appleCount} apples
} and {$bananaCount -> 
    [one] a banana
   *[other] {$bananaCount} bananas
CC BY-NC-SA 4.0 2021 © Ivan Demchuk