Build A i18n Filter Using Vue.js & Native Web Specs
Raymond Camden | March 12th, 2018 | 5 min read
I'm still learning Vue and one of the things I was a bit slow to discover were filters. (To be fair, they are towards the end of the docs!) Filters, as you can probably guess, allow you to define simple textual transformations to values. So for example, the docs demonstrate a Capitalization filter that will convert the first letter in a string to uppercase. Vue doesn't ship with any filters baked in, but it's incredibly easy to write them. A great use case for this is in internationalization and globalization, broadly defined as formatting values in the user's preferred language.
This helps prevent problems like figuring out exactly what this date is: 4/8/73. For Americans, this is April 8th, 1973. For pretty much the rest of the planet, that date is August 4th, 1973. It would certainly be nice if we could simply format those values appropriately, and automatically, for our users.
This is not a new problem, of course, and multiple libraries exist to help with this. The incredible Moment.js has great support for this right now. But did you know that there's actually a web standards way of doing this? The Intl spec has been around for nearly six years, but browser support hasn't been terribly great till most recently. CanIUse.com currently reports support at a pretty high 85%.
Given this level of support, it would be pretty safe to make use of it within our applications. Checking for Intl support can be a simple if(window.Intl)
and fallback could be... well, simply punting and displaying the value as is. You would have to decide for yourself if that strategy makes sense.
At a high level, the Intl spec allows you to format dates, numbers, and currencies. It also has support for handling locale-specific sorting and pluralization. Please remember that you can't just "convert" American dollars into Euros. Yes, I've made that mistake before. So given that we've got a nice baked in way of internationalizing values and we've got Vue, how can we build some filters to support this?
The Initial Version
Let's begin by building a simple demo with no formatting at all. I began with a simple view that presented three values: a date, number, and currency. I also added some simple form fields for quick editing by the user.
<div id="app">
<p>The date is {{ date }}.</p>
<p>We've had {{ accidentFree }} accident free days!</p>
<p>You're current debt is ${{ debt }}.</p>
<hr/>
<p>
Use to change values above:
</p>
<p>
Date: <input v-model="date"><br/>
Accident Free Days: <input v-model="accidentFree"><br/>
Debt: <input v-model="debt"><br/>
</p>
</div>
The Vue code simply sets up initial values:
const app = new Vue({
el:'#app',
data:{
date:new Date(),
debt:999999999,
accidentFree:3232
}
})
You can play with this code right now using the embedded CodePen below:
See the Pen Vue Filter Post - P1 by Raymond Camden (@cfjedimaster) on CodePen.
Note that even if we don't necessarily care about the rest of the world, even just for myself, the values really aren't that nice. The debt, for example, is pretty hard to read. Is that 9 billion or trillion? The date is certainly readable but not very pretty. Let's kick it up a notch and begin adding our filters!
The Cool Version
Vue lets you define filters either globally or on a per-component basis. To keep things simple, we'll define the filters globally using this format:
Vue.filter('nameOfAwesomeFilter', s => {
//logic here, return the result
});
The example above would then let us use the filter like so: {{ boringValue | nameOfAwesomeFilter }}
. You could also use it in a bind: v-bind:label="boringValue | nameOfAwesomeFilter"
. Also note I'm using a fancy (i.e. hipster) arrow function here and that's totally not something you need to do as well.
For the first filter, we'll define one called date:
Vue.filter('date', s => {
if(!window.Intl) return s;
// convert to date
if(!(s instanceof Date)) {
let orig = s;
s = new Date(s);
if(s == 'Invalid Date') return orig;
}
return new Intl.DateTimeFormat().format(s);
});
At the top, we've got some simple feature support checking for the Intl API. As I said before, our fallback will simply return the initial value, and while that may not be optimal, it's a quick and dirty solution for the minority of folks running older browsers. (One idea you may consider is adding some text to the end that specifies the date is in an American format.)
Now comes a slightly tricky part. Our demo allows the user to enter new input for the dates which means the value may be a string, not a date as originally specified in the Vue component definition. It's easy enough to convert a string to a date, but JavaScript lets us turn anything into a date. The kinda lame check for "Invalid Date" handles that. Note I also keep the original value around so it can be returned instead of the 'bad' date object.
Finally - we simply create an instance of DateTimeFormat and run format on it. By default, the DateTimeFormat object will use the current locale and format dates in a short form like so: 2/20/2018. You can specify other formats and the docs over at MDN would be your best guide. Note that specifying options for the date requires specifying a locale, an issue we'll run into with currencies so make note of the solution there.
Using the filter becomes a simple matter of adding the pipe and filter name: <p>The date is {{ date | date }}.</p>
I'm not happy with "date" being used twice there but I'd assume in a real app the Vue data value would probably have a nicer name. Next up is the number, which is a lot simpler:
Vue.filter('number', s => {
if(!window.Intl) return s;
return new Intl.NumberFormat().format(s);
});
We've made use of the NumberFormat object with all its defaults. As with the DateTimeFormat object, you have options here too. In our demo, this returns (again, for an American): "We've had 3,232 accident free days!". In other locales you may see: "We've had 3 232 accident free days!" As before, making use of the filter is simple: <p>We've had {{ accidentFree | number }} accident free days!</p>
Finally, let's look at currency formatting. Now, to be clear, this is currency formatting, not currency conversion. Please reread that sentence a few times. In order to do this, we use the NumberFormat but will specify the option to format it as a currency. Doing this requires two changes.
In order to pass options to NumberFormat, we must pass the first argument which is the locale to use. Unfortunately, there is no built-in "getCurrentLocale" type function. I made use of this StackOverflow solution as it was pretty simple:
//https://stackoverflow.com/a/31135571/52160
function getLang() {
if (navigator.languages != undefined) return navigator.languages[0];
else return navigator.language;
}
With this function, we can now use it to pass the first argument, and then the options for currency. You must specify what currency you are using, and for the purpose of the demo, I select the Euro. Here is the filter.
Vue.filter('currency', s => {
let result = new Intl.NumberFormat(getLang(), {style:'currency',currency:'EUR'}).format(s);
return result;
});
Note how the options are passed as a simple object in the second argument. You can read about other possible options at the docs on MDN. The result? "Your current debt is €999,999,999.00."
You can play with this version here:
See the Pen Vue Filter Post - P2 by Raymond Camden (@cfjedimaster) on CodePen.
As a quick note, keep in mind that the filters in the demo are running on every single input in the form fields. While it won't matter for the demo, in a real app that could cause performance issues and a computed property may make more sense. You could also cache the creation of the DateTimeFormatter and NumberFormatter so that you only need to use the format function on those global objects. Please feel free to fork my CodePens and share an example of that!
I hope these examples gave you a small idea of just how easy filters are to use with Vue. I'd love to read your comments about any unique filters you may have built.
Click to load comments...