Skip to main content

3 posts tagged with "globalize"

View All Tags

jQuery Validation Globalize hits 1.0

This is just a quick post - the tl;dr is this: jQuery Validation Globalize has been ported to Globalize 1.x. Yay! In one of those twists of fate I'm not actually using this plugin in my day job anymore but I thought it might be useful to other people. So here you go. You can read more about this plugin in an older post and you can see a demo of it in action here.

The code did not change drastically - essentially it was just a question of swapping parseFloat for parseNumber and parseDate for a slightly different parseDate. So, we went from this:

(function ($, Globalize) {
// Clone original methods we want to call into
var originalMethods = {
min: $.validator.methods.min,
max: $.validator.methods.max,
range: $.validator.methods.range
};
// Tell the validator that we want numbers parsed using Globalize
$.validator.methods.number = function (value, element) {
var val = Globalize.parseFloat(value);
return this.optional(element) || ($.isNumeric(val));
};
// Tell the validator that we want dates parsed using Globalize
$.validator.methods.date = function (value, element) {
var val = Globalize.parseDate(value);
return this.optional(element) || (val instanceof Date);
};
// Tell the validator that we want numbers parsed using Globalize,
// then call into original implementation with parsed value
$.validator.methods.min = function (value, element, param) {
var val = Globalize.parseFloat(value);
return originalMethods.min.call(this, val, element, param);
};
$.validator.methods.max = function (value, element, param) {
var val = Globalize.parseFloat(value);
return originalMethods.max.call(this, val, element, param);
};
$.validator.methods.range = function (value, element, param) {
var val = Globalize.parseFloat(value);
return originalMethods.range.call(this, val, element, param);
};
}(jQuery, Globalize));

To this:

(function ($, Globalize) {
// Clone original methods we want to call into
var originalMethods = {
min: $.validator.methods.min,
max: $.validator.methods.max,
range: $.validator.methods.range
};
// Globalize options - initially just the date format used for parsing
// Users can customise this to suit them
$.validator.methods.dateGlobalizeOptions = { dateParseFormat: { skeleton: "yMd" } };
// Tell the validator that we want numbers parsed using Globalize
$.validator.methods.number = function (value, element) {
var val = Globalize.parseNumber(value);
return this.optional(element) || ($.isNumeric(val));
};
// Tell the validator that we want dates parsed using Globalize
$.validator.methods.date = function (value, element) {
var val = Globalize.parseDate(value, $.validator.methods.dateGlobalizeOptions.dateParseFormat);
return this.optional(element) || (val instanceof Date);
};
// Tell the validator that we want numbers parsed using Globalize,
// then call into original implementation with parsed value
$.validator.methods.min = function (value, element, param) {
var val = Globalize.parseNumber(value);
return originalMethods.min.call(this, val, element, param);
};
$.validator.methods.max = function (value, element, param) {
var val = Globalize.parseNumber(value);
return originalMethods.max.call(this, val, element, param);
};
$.validator.methods.range = function (value, element, param) {
var val = Globalize.parseNumber(value);
return originalMethods.range.call(this, val, element, param);
};
}(jQuery, Globalize));

All of which is pretty self-explanatory. The only thing I'd like to draw out is that Globalize 0.1.x didn't force you to specify a date parsing format and, as I recall, would attempt various methods of parsing. For that reason jQuery Validation Globalize 1.0 exposes a $.validator.methods.dateGlobalizeOptions which allows you to specify the data parsing format you want to use. This means, should you be using a different format than the out of the box one then you can tweak it like so:

$.validator.methods.dateGlobalizeOptions.dateParseFormat = // your data parsing format goes here...

Theoretically, this functionality could be tweaked to allow the user to specify multiple possible date parsing formats to attempt. I'm not certain if that's a good idea though, so it remains unimplemented for now.

Upgrading to Globalize 1.x for Dummies

Globalize has hit 1.0. Anyone who reads my blog will likely be aware that I'm a long time user of Globalize 0.1.x. I've been a little daunted by the leap that the move from 0.1.x to 1.x represents. It appears to be the very definition of "breaking changes". :-) But hey, this is Semantic Versioning being used correctly so how could I complain? Either way, I've decided to write up the migration here as I'm not expecting this to be easy.

To kick things off I've set up a very simple repo that consists of a single page that depends upon Globalize 0.1.x to render a number and a date in German. It looks like this:

<html>
<head>
<title>Globalize demo</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container-fluid">
<h4>Globalize demo for the <em id="locale"></em> culture / locale</h4>
<p>This is a the number <strong id="number"></strong> formatted by Globalize:
<strong id="numberFormatted"></strong></p>
<p>This is a the number <strong id="date"></strong> formatted by Globalize:
<strong id="dateFormatted"></strong></p>
</div>
<script src="bower_components/globalize/lib/globalize.js"></script>
<script src="bower_components/globalize/lib/cultures/globalize.culture.de-DE.js"></script>
<script>
var locale = 'de-DE';
var number = 12345.67;
var date = new Date(2012, 5, 15);
Globalize.culture( locale );
document.querySelector('#locale').innerText = locale;
document.querySelector('#number').innerText = number;
document.querySelector('#date').innerText = date;
document.querySelector('#numberFormatted').innerText = Globalize.format(number, 'n2');
document.querySelector('#dateFormatted').innerText = Globalize.format(date, 'd');
</script>
</body>
</html>

When it's run it looks like this:

Let's see how we go about migrating this super simple example.

Update our Bower dependencies#

First things first, we want to move Globalize from 0.1.x to 1.x using Bower. To do that we update our bower.json:

"dependencies": {
"globalize": "^1.0.0"
}

Now we enter: bower update. And we're off!

bower globalize#^1.0.0 cached git://github.com/jquery/globalize.git#1.0.0
bower globalize#^1.0.0 validate 1.0.0 against git://github.com/jquery/globalize.git#^1.0.0
bower cldr-data#>=25 cached git://github.com/rxaviers/cldr-data-bower.git#27.0.3
bower cldr-data#>=25 validate 27.0.3 against git://github.com/rxaviers/cldr-data-bower.git#>=25
bower cldrjs#0.4.1 cached git://github.com/rxaviers/cldrjs.git#0.4.1
bower cldrjs#0.4.1 validate 0.4.1 against git://github.com/rxaviers/cldrjs.git#0.4.1
bower globalize#^1.0.0 install globalize#1.0.0
bower cldr-data#>=25 install cldr-data#27.0.3
bower cldrjs#0.4.1 install cldrjs#0.4.1
globalize#1.0.0 bower_components\globalize
鈹溾攢鈹 cldr-data#27.0.3
鈹斺攢鈹 cldrjs#0.4.1
cldr-data#27.0.3 bower_components\cldr-data
cldrjs#0.4.1 bower_components\cldrjs
鈹斺攢鈹 cldr-data#27.0.3

This all looks happy enough. Except it's actually not.

We need fuel#

Or as I like to call it cldr-data. We just pulled down Globalize 1.x but we didn't pull down the data that Globalize 1.x relies upon. This is one of the differences between Globalize 0.1.x and 1.x. Globalize 1.x does not include the "culture" data. By which I mean all the globalize.culture.de-DE.js type files. Instead Globalize 1.x relies upon CLDR - Unicode Common Locale Data Repository. It does this in the form of cldr-json.

Now before you start to worry, you shouldn't actually need to go and get this by yourself, the lovely Rafael Xavier de Souza has saved you a job by putting together Bower and npm modules to do the hard work for you.

I'm using Bower for my client side package management and so I'll use that. Looking at the Bower dependencies downloaded when I upgraded my package I can see there is a cldr-data package. Yay! However it appears to be missing the associated json files. Boo!

To the documentation Batman. It says you need a .bowerrc file in your repo which contains this:

{
"scripts": {
"preinstall": "npm install [email protected]",
"postinstall": "node ./node_modules/cldr-data-downloader/bin/download.js -i bower_components/cldr-data/index.json -o bower_components/cldr-data/"
}
}

Unfortunately, because I've already upgraded to v1 adding this file alone doesn't do anything for me. To get round that I delete my bower_components folder and enter bower install. Boom!

bower globalize#^1.0.0 cached git://github.com/jquery/globalize.git#1.0.0
bower globalize#^1.0.0 validate 1.0.0 against git://github.com/jquery/globalize.git#^1.0.0
bower cldrjs#0.4.1 cached git://github.com/rxaviers/cldrjs.git#0.4.1
bower cldrjs#0.4.1 validate 0.4.1 against git://github.com/rxaviers/cldrjs.git#0.4.1
bower cldr-data#>=25 cached git://github.com/rxaviers/cldr-data-bower.git#27.0.3
bower cldr-data#>=25 validate 27.0.3 against git://github.com/rxaviers/cldr-data-bower.git#>=25
bower preinstall npm install [email protected]
bower preinstall [email protected] node_modules\cldr-data-downloader
bower preinstall 鈹溾攢鈹 [email protected]
bower preinstall 鈹溾攢鈹 [email protected]
bower preinstall 鈹溾攢鈹 [email protected] ([email protected])
bower preinstall 鈹溾攢鈹 [email protected] ([email protected])
bower preinstall 鈹溾攢鈹 [email protected] ([email protected])
bower preinstall 鈹溾攢鈹 [email protected]
bower globalize#^1.0.0 install globalize#1.0.0
bower cldrjs#0.4.1 install cldrjs#0.4.1
bower cldr-data#>=25 install cldr-data#27.0.3
bower postinstall node ./node_modules/cldr-data-downloader/bin/download.js -i bower_components/cldr-data/index.json -o bower_components/cldr-data/
bower postinstall GET `https://github.com/unicode-cldr/cldr-core/archive/27.0.3.zip`
bower postinstall GET `https://github.com/unicode-cldr/cldr-dates-modern/archive/27.0.3.zip`
bower postinstall GET `https://github.com/unicode-cldr/cldr-cal-buddhist-modern/archive/27.0.3.zip`
bower postinstall GET `https://github.com/unicode-cldr/cldr-cal-chinese-modern/archive/27.0.3.zip`
bower postinstall GET `https://github.com/unicode-cldr/cldr-cal-coptic-modern/archive/27.0.3.zip`
bower postinstall GET `https://github.com/unicode-cldr/cldr-cal-dangi-modern/archive/27.0.3.zip`
bower postinstall GET `https://github.com/unicode-cldr/cldr-cal-ethiopic-modern/archive/27.0.3.zip`
bower postinstall GET `https://github.com/unicode-cldr/cldr-cal-hebrew-modern/archive/27.0.3.zip`
bower postinstall GET `https://github.com/unicode-cldr/cldr-cal-indian-modern/archive/27.0.3.zip`
bower postinstall GET `https://github.com/unicode-cldr/cldr-cal-islamic-modern/archive/27.0.3.zip`
bower postinstall GET `https://github.com/unicode-cldr/cldr-cal-japanese-modern/archive/27.0.3.zip`
bower postinstall GET `https://github.com/unicode-cldr/cldr-cal-persian-modern/archive/27.0.3.zip`
bower postinstall GET `https://github.com/unicode-cldr/cldr-cal-roc-modern/archive/27.0.3.zip`
bower postinstall GET `https://github.com/unicode-cldr/cldr-localenames-modern/archive/27.0.3.zip`
bower postinstall GET `https://github.com/unicode-cldr/cldr-misc-modern/archive/27.0.3.zip`
bower postinstall GET `https://github.com/unicode-cldr/cldr-numbers-modern/archive/27.0.3.zip`
bower postinstall GET `https://github.com/unicode-cldr/cldr-segments-modern/archive/27.0.3.zip`
bower postinstall GET `https://github.com/unicode-cldr/cldr-units-modern/archive/27.0.3.zip`
bower postinstall Received 28728K total.
bower postinstall Received 28753K total.
bower postinstall Unpacking it into `./bower_components\cldr-data`
globalize#1.0.0 bower_components\globalize
鈹溾攢鈹 cldr-data#27.0.3
鈹斺攢鈹 cldrjs#0.4.1
cldrjs#0.4.1 bower_components\cldrjs
鈹斺攢鈹 cldr-data#27.0.3
cldr-data#27.0.3 bower_components\cldr-data

That's right - I'm golden. And if I didn't want to do that I could have gone straight to the command line and entered this: (look familiar?)

npm install [email protected]
node ./node_modules/cldr-data-downloader/bin/download.js -i bower_components/cldr-data/index.json -o bower_components/cldr-data/

Some bitching and moaning.#

If, like me, you were a regular user of Globalize 0.1.x then you know that you needed very little to get going. As you can see from our example you just serve up Globalize.js and the culture files you are interested in (eg globalize.culture.de-DE.js). That's it - you have all you need; job's a good'un. This is all very convenient and entirely lovely.

Globalize 1.x has a different approach and one that (I have to be honest) I'm not entirely on board with. The thing that you need to know about the new Globalize is that nothing comes for free. It's been completely modularised and you have to include extra libraries depending on the functionality you require. On top of that you then have to work out the portions of the cldr data that you require for those modules and supply them. This means that getting up and running with Globalize 1.x is much harder. Frankly I think it's a little painful.

I realise this is a little "Who moved my cheese". I'll get over it. I do actually see the logic of this. It is certainly good that the culture date is not frozen in aspic but will evolve as the world does. But it's undeniable that in our brave new world Globalize is no longer a doddle to pick up. Or at least right now.

Take the modules and run#

So. What do we actually need? Well I've consulted the documentation and I think I'm clear. Our simple demo cares about dates and numbers. So I'm going to guess that means I need:

  • <a href="https://github.com/jquery/globalize#core-module">globalize.js</a>
  • <a href="https://github.com/jquery/globalize#date-module">globalize/date.js</a>
  • <a href="https://github.com/jquery/globalize#number-module">globalize/number.js</a>

On top of that I'm also going to need the various cldr dependencies too. That's not all. Given that I've decided which modules I will use I now need to acquire the associated cldr data. According to the docs here we're going to need:

  • cldr/supplemental/likelySubtags.json
  • cldr/main/<i>locale</i>/ca-gregorian.json
  • cldr/main/<i>locale</i>/timeZoneNames.json
  • cldr/supplemental/timeData.json
  • cldr/supplemental/weekData.json
  • cldr/main/locale/numbers.json
  • cldr/supplemental/numberingSystems.json

Figuring that all out felt like really hard work. But I think that now we're ready to do the actual migration.

Update 30/08/2015: Globalize 路 So What'cha Want#

To make working out what you need when using Globalize I've built Globalize 路 So What'cha Want. You're so very welcome.

The Actual Migration#

To do this I'm going to lean heavily upon an example put together by Rafael. The migrated code looks like this:

<html>
<head>
<title>Globalize demo</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container-fluid">
<h4>Globalize demo for the <em id="locale"></em> culture / locale</h4>
<p>This is a the number <strong id="number"></strong> formatted by Globalize:
<strong id="numberFormatted"></strong></p>
<p>This is a the number <strong id="date"></strong> formatted by Globalize:
<strong id="dateFormatted"></strong></p>
</div>
<!-- First, we load Globalize's dependencies (`cldrjs` and its supplemental module). -->
<script src="bower_components/cldrjs/dist/cldr.js"></script>
<script src="bower_components/cldrjs/dist/cldr/event.js"></script>
<script src="bower_components/cldrjs/dist/cldr/supplemental.js"></script>
<!-- Next, we load Globalize and its modules. -->
<script src="bower_components/globalize/dist/globalize.js"></script>
<script src="bower_components/globalize/dist/globalize/number.js"></script>
<!-- Load after globalize/number.js -->
<script src="bower_components/globalize/dist/globalize/date.js"></script>
<script>
var locale = 'de';
Promise.all([
// Core
fetch('bower_components/cldr-data/supplemental/likelySubtags.json'),
// Date
fetch('bower_components/cldr-data/main/' + locale + '/ca-gregorian.json'),
fetch('bower_components/cldr-data/main/' + locale + '/timeZoneNames.json'),
fetch('bower_components/cldr-data/supplemental/timeData.json'),
fetch('bower_components/cldr-data/supplemental/weekData.json'),
// Number
fetch('bower_components/cldr-data/main/' + locale + '/numbers.json'),
fetch('bower_components/cldr-data/supplemental/numberingSystems.json'),
])
.then(function(responses) {
return Promise.all(responses.map(function(response) {
return response.json();
}));
})
.then(Globalize.load)
.then(function() {
var number = 12345.67;
var date = new Date(2012, 5, 15);
var globalize = Globalize( locale );
document.querySelector('#locale').innerText = locale;
document.querySelector('#number').innerText = number;
document.querySelector('#date').innerText = date;
document.querySelector('#numberFormatted').innerText = globalize.formatNumber(number, {
minimumFractionDigits: 2,
useGrouping: true
});
document.querySelector('#dateFormatted').innerText = globalize.formatDate(date, {
date: 'medium'
});
})
</script>
</body>
</html>

By the way, I'm using fetch and promises to load the cldr-data. This isn't mandatory - I use it because Chrome lets me. (I'm so bleeding edge.) Some standard jQuery ajax calls would do just as well. There's an example of that approach here.

Observations#

We've gone from not a lot of code to... well, more than a little. A medium amount. Almost all of that extra code relates to getting Globalize 1.x to spin up so it's ready to work. We've also gone from 2 HTTP requests to 13 which is unlucky for you. 6 of them took place via ajax after the page had loaded. It's worth noting that we're not even loading all of Globalize either. On top of that there's the old order-of-loading shenanigans to deal with. All of these can be mitigated by introducing a custom build step of your own to concatenate and minify the associated cldr / Globalize files.

Loading the data via ajax isn't mandatory by the way. If you wanted to you could create your own style of globalize.culture.de.js files which would allow you load the page without recourse to post-page load HTTP requests. Something like this Gulp task I've knocked up would do the trick:

gulp.task("make-globalize-culture-de-js", function() {
var locale = 'de';
var jsonWeNeed = [
require('./bower_components/cldr-data/supplemental/likelySubtags.json'),
require('./bower_components/cldr-data/main/' + locale + '/ca-gregorian.json'),
require('./bower_components/cldr-data/main/' + locale + '/timeZoneNames.json'),
require('./bower_components/cldr-data/supplemental/timeData.json'),
require('./bower_components/cldr-data/supplemental/weekData.json'),
require('./bower_components/cldr-data/main/' + locale + '/numbers.json'),
require('./bower_components/cldr-data/supplemental/numberingSystems.json')
];
var jsonStringWithLoad = 'Globalize.load(' +
jsonWeNeed.map(function(json){ return JSON.stringify(json); }).join(', ') +
');';
var fs = require('fs');
fs.writeFile('./globalize.culture.' + locale + '.js', jsonStringWithLoad, function(err) {
if(err) {
console.log(err);
} else {
console.log("The file was created!");
}
});
})

The above is standard node/io type code by the way; just take the contents of the function and run in node and you should be fine. If you do use this approach then you're very much back to the simplicity of Globalize 0.1.x's approach.

And here is the page in all its post migration glory:

It looks exactly the same except 'de-DE' has become simply 'de' (since that's how the cldr rolls).

The migrated code is there for the taking. Make sure you remember to bower install - and you'll need to host the demo on a simple server since it makes ajax calls.

Before I finish off I wanted to say "well done!" to all the people who have worked on Globalize. It's an important project and I do apologise for my being a little critical of it here. I should say that I think it's just the getting started that's hard. Once you get over that hurdle you'll be fine. Hopefully this post will help people do just that. Pip, pip!

Knockout + Globalize = valueNumber Binding Handler

I鈥檝e long used Globalize for my JavaScript number formatting / parsing needs. In a current project I鈥檓 using Knockout for the UI. When it came to data-binding numeric values none of the default binding handlers seemed appropriate. What I wanted was a binding handler that:

  1. Was specifically purposed for dealing with numeric values
  2. Handled the parsing / formatting for the current locale (and I naturally intended to use Globalize for this purpose)

Like so much development we start by standing on the shoulders of giants. In this case it鈥檚 the fantastic Ryan Niemeyer who put up a post on StackOverflow that got me on the right track.

Essentially his approach provides an 鈥渋nterceptor鈥 mechanism that allows you to validate numeric data entry on input and format numeric data going out as well. Very nice. Into this I plugged Globalize to handle the parsing and formatting. I ended up with the 鈥渧alueNumber鈥 binding handler:

ko.bindingHandlers.valueNumber = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
/**
* Adapted from the KO hasfocus handleElementFocusChange function
*/
function elementIsFocused() {
var isFocused = false,
ownerDoc = element.ownerDocument;
if ("activeElement" in ownerDoc) {
var active;
try {
active = ownerDoc.activeElement;
} catch(e) {
// IE9 throws if you access activeElement during page load
active = ownerDoc.body;
}
isFocused = (active === element);
}
return isFocused;
}
/**
* Adapted from the KO hasfocus handleElementFocusChange function
*
* @param {boolean} isFocused whether the current element has focus
*/
function handleElementFocusChange(isFocused) {
elementHasFocus(isFocused);
}
var observable = valueAccessor(),
properties = allBindingsAccessor(),
elementHasFocus = ko.observable(elementIsFocused()),
handleElementFocusIn = handleElementFocusChange.bind(null, true),
handleElementFocusOut = handleElementFocusChange.bind(null, false);
var interceptor = ko.computed({
read: function () {
var currentValue = ko.utils.unwrapObservable(observable);
if (elementHasFocus()) {
return (!isNaN(currentValue) && (currentValue !== null) && (currentValue !== undefined))
? currentValue.toString().replace(".", Globalize.findClosestCulture().numberFormat["."]) // Displays correct decimal separator for the current culture (so de-DE would format 1.234 as "1,234")
: null;
} else {
var format = properties.numberFormat || "n2",
formattedNumber = Globalize.format(currentValue, format);
return formattedNumber;
}
},
write: function (newValue) {
var currentValue = ko.utils.unwrapObservable(observable),
numberValue = Globalize.parseFloat(newValue);
if (!isNaN(numberValue)) {
if (numberValue !== currentValue) {
// The value has changed so update the observable
observable(numberValue);
}
} else if (newValue.length === 0) {
if (properties.isNullable) {
// If newValue is a blank string and the isNullable property has been set then nullify the observable
observable(null);
} else {
// If newValue is a blank string and the isNullable property has not been set then set the observable to 0
observable(0);
}
}
}
});
ko.utils.registerEventHandler(element, "focus", handleElementFocusIn);
ko.utils.registerEventHandler(element, "focusin", handleElementFocusIn); // For IE
ko.utils.registerEventHandler(element, "blur", handleElementFocusOut);
ko.utils.registerEventHandler(element, "focusout", handleElementFocusOut); // For IE
if (element.tagName.toLowerCase() === 'input') {
ko.applyBindingsToNode(element, { value: interceptor });
} else {
ko.applyBindingsToNode(element, { text: interceptor });
}
}
};

Using this binding handler you just need to drop in a valueNumber into your data-bind statement where you might previously have used a value binding. The binding also has a couple of nice hooks in place which you might find useful:

numberFormat (defaults to "n2")
allows you to specify a format to display your number with. Eg, "c2" would display your number as a currency to 2 decimal places, "p1" would display your number as a percentage to 1 decimal place etc
isNullable (defaults to false)
specifies whether your number should be treated as nullable. If it's not then clearing the elements value will set the underlying observable to 0.

Finally when the element gains focus / becomes active the full underlying value is displayed. (Kind of like Excel - like many an app, the one I'm working on started life as Excel and the users want to keep some of the nice aspects of Excel's UI.) To take a scenario, let's imagine we have an input element which is applying the "n1" format. The underlying value backing this is 1.234. The valueNumber binding displays this as "1.2" when the input does not have focus and when the element gains focus the full "1.234" is displayed. Credit where it鈥檚 due, this is thanks to Robert Westerlund who was kind enough to respond to a question of mine on StackOverflow.

Finally, here鈥檚 a demo using the "de-DE" locale:

PS Globalize is a-changing#

The version of Globalize used in the binding handler is Globalize v0.1.1. This has been available in various forms for quite some time but as I write this the Globalize plugin is in the process of being ported to the CLDR. As part of that work it looks like the Globalize API will change. When that gets finalized I鈥檒l try and come back and update this.