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 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.
Firstly, this is what I liked in vue-i18n:
Component interpolation allows using components inside translation messages. Nice way of reducing v-html
directive usages.
Keeping translations for the component in the same file as template and js code is really convenient.
Being the most used Vue.js internationalization library, it has a heap of useful packages and extensions.
And this is what I didn't like in vue-i18n or what didn't work for my project:
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.
The grammar of the 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 parameters:
$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 the translator tries to use this syntax and the 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.
Developers are forced to make choices that translators should make: should the translation message be pluralized, and which date and number format should be used?
Example (date format):
vue-i18n
has a fixed number of developer-predefined date formats, and the 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 the UI in some languages.
fluent-vue solution:
The fluent syntax allows translators to call custom functions in translation messages. There is the 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.
Even with the $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 it 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 ->
[1] An apple
*[other] {$appleCount} apples
} and {$bananaCount ->
[1] a banana
*[other] {$bananaCount} bananas
}
Also published on https://demivan.me/posts/2021-08-08-vue-i18n-in-real-world-application.