I'm working on a quite big jQuery UI plugin right now. I think it will be a great open source project, so I thought using a translation system. The only plugin I found using such system is the ui.datepicker.js and it does the job, I think it's somewhat primitive.
Here's how it works. When a translation file is included it extends the plugin's regional property with a object containg translation key/value pairs and sets itself to the default language.
Here an example of a (stripped) translation file taken from ui.datepicker.js:
// French initialisation for the jQuery UI date picker plugin.
// Written by Keith Wood (kbwood@virginbroadband.com.au) and Stéphane Nahmani (sholby@sholby.net).
jQuery(function($){
$.datepicker.regional['fr'] = {
clearText: 'Effacer',
closeText: 'Fermer',
prevText: '<Préc',
prevBigText: '<<',
nextText: 'Suiv>',
nextBigText: '>>',
currentText: 'Courant',
dateFormat: 'dd/mm/yy',
firstDay: 0,
initStatus: 'Choisir la date',
isRTL: false};
$.datepicker.setDefaults($.datepicker.regional['fr']);
});
By now it's quite simple, but when we look at the plugin code it gets a bit uglier. For the plugin to work without translation file, the author is forced to create a "default" fall back language object within his plugin:
this.regional[''] = { // Default regional settings
clearText: 'Clear', // Display text for clear link
closeText: 'Close', // Display text for close link
prevText: '<Prev', // Display text for previous month link
prevBigText: '<<', // Display text for previous year link
nextText: 'Next>', // Display text for next month link
nextBigText: '>>', // Display text for next year link
currentText: 'Today', // Display text for current month link
firstDay: 0,
dateFormat: 'mm/dd/yy', // See format options on parseDate
initStatus: 'Select a date' // Initial Status text on opening
isRTL: false
}
Now while this approach work fine, it's far from optimal for many reasons.
So here I am with my i18n plugin proposal, it's really my first working draft and I hope I'll get some feedbacks or improvement ideas on it.
The first thing I designed was the translation processe, just because I though it would be the easy part.. fool I was. But after some trials and errors I found what I think is a better solution.
$.i18n('fr.datepicker', {
'Clear': 'Effacer',
'Close': 'Fermer',
'<Prev': '<Préc',
'<<' '<<',
'Next>': 'Suiv>',
'>>': '>>',
'Today': 'Courant',
'{m:d}/{d:d}/{y:d}':
'{d:d}/{m:d}/{y:d}',
'Select a date':
'Choisir la date'
});
The first argument is the language code, it can be namespaced with a dot. By default "jQuery" namespace is used (so you can only give the language code).
Secondly, the original string is in the translation file, like with gettext. I think it's a must for several reasons. Among them, it helps the translator and you don't have to create a meaningful key.
The only downside I can see is the key length, I don't know what's the maximum string length you can use as object key or if there is performances costs, but on the other and I wouldn't recommend JavaScript for storing long texts. I see this plugin more to translate short texts for UI components, like the datepicker.
Another advantage is more legible code, take this simple example taken from the datepicker plugin:
var controls = '<div class="ui-datepicker-control">' + (isRTL ? '' : clear) +
'<div class="ui-datepicker-close"><a onclick="jQuery.datepicker._hideDatepicker();">' +
this._get(inst, 'closeText') + '</a></div>' + (isRTL ? clear : '') + '</div>';
Now with my proposed system:
var controls = '<div class="ui-datepicker-control">' + (isRTL ? '' : clear) +
'<div class="ui-datepicker-close"><a onclick="jQuery.datepicker._hideDatepicker();">' +
$.i18n('datepicker', 'Close') + '</a></div>' + (isRTL ? clear : '') + '</div>';
Not bad, but not a really great improvement neither. Here's the sugar:
// I enclose the plugin
(function($){
// defined a "_" method like gettext, which will specify
// the plugin and make the call
function _(str, args) {
return $.i18n('imgTools', str, args);
}
var controls = '<div class="ui-datepicker-control">' + (isRTL ? '' : clear) +
'<div class="ui-datepicker-close"><a onclick="jQuery.datepicker._hideDatepicker();">' +
_('Close') + '</a></div>' + (isRTL ? clear : '') + '</div>';
Bonus: Now js files can be scanned for _(*) and base translation files generated automatically.
But where to specify the language ? Simple, it's global. You set it once and all your plugins will look for the translated string or use the original if no translation is available.
// switching language
$.i18n('fr');
Here's the complete code, my jquery.strings.js plugin is required for the string formating.
(function($){
$._i18n = { trans: {}, default: 'en', language: 'en' };
$.i18n = function() {
var getTrans = function(ns, str) {
var trans = false;
// check if string exists in translation
if ($._i18n.trans[$._i18n.language]
&& $._i18n.trans[$._i18n.language][ns]
&& $._i18n.trans[$._i18n.language][ns][str]) {
trans = $._i18n.trans[$._i18n.language][ns][str];
}
// or exists in default
else if ($._i18n.trans[$._i18n.default]
&& $._i18n.trans[$._i18n.default][ns]
&& $._i18n.trans[$._i18n.default][ns][str]) {
trans = $._i18n.trans[$._i18n.default][ns][str];
}
// return trans or original string
return trans || str;
};
// Set language
if (arguments.length < 2 && arguments[0].length == 2) {
return $._i18n.language = arguments[0];
}
else {
// get translation
if (typeof(arguments[1]) == 'string') {
var trans = getTrans(arguments[0], arguments[1]);
// has variables for string formating
if (arguments[2] && typeof(arguments[2]) == 'object') {
return $.format(trans, arguments[2]);
}
else {
return trans;
}
}
// set translation
else {
var tmp = arguments[0].split('.');
var lang = tmp[0];
var ns = tmp[1] || 'jQuery';
if (!$._i18n.trans[lang]) {
$._i18n.trans[lang] = {};
$._i18n.trans[lang][ns] = arguments[1];
}
else {
$.extend($._i18n.trans[lang][ns], arguments[1]);
}
}
}
};
})(jQuery);
Recently my coworker (Gnuvince) gave me the nice idea of using a path based approach like xPath to get properties of JSON objects.
My first thought was to replicate xPath's syntax, but it quickly came apparent that beside of being way to heavy, it didn't fit at all.
On the other hand, jQuery's selector approach turned out to be the perfect syntax. I wrote a working and extensible prototype, which I <sarcasm>creatively</sarcasm> decided to name jPath (source code).
var employee: {
name: 'John Doe',
phone: '(555) 555-1234',
bookmarks: ['google.com', ...],
prefs: {
color: 'blue',
page: 'http://www.google.com/'
}
}
};
$(employee).jpath('name'); // John Doe
$(employee).jpath('bookmarks'); // ['google.com', 'reddit.com', 'xkcd.com']
$(employee).jpath('prefs.color'); // blue
I find it syntactically sexy. But more importantly, it takes cares of checking if the property is defined before returning it. Which means that when requesting an property of an undefined property it will consistently return false:
// plain javascript
alert(enployee.rights); // 'undefined'
alert(employee.rights.constitutional); // reference error exception
// leads to ugly and redundant code like this:
if (employee && employee.rights.constitutional && employee.rights.constitutional) { ... }
// jPath
$(employee).jpath('rights'); // false
$(employee).jpath('rights.constitutional'); // false
Until now it's all cute, but I though I could spice it up and add expressions to it, again like jQuery' selectors.
$(employee).jpath('name:match(John)'); // return true
$(employee).jpath('bookmarks:eq(2)'); // return 'xkcd.com'
$(employee).jpath('color:is(blue)'); // return true
| expr | description |
|---|---|
| :contains(value) | Returns true if the object's contains value |
| :first | Returns the first element of an object |
| :last | Returns the last element of an object |
| :eq(N) | Returns the Nth element of an array or the N property of an array |
| :is(str) | Returns true if the object's value == str |
| :match(str) | Returns true if the object's value match str |
It's quite easy to add custom expressions, here's two examples:
(function($){
$.extend(jpath.expr, { // s: scope, k: key, p: param
'eq': function(s, k, p) {
return s[k] && s[k][p] || false;
},
'first': function(s, k) {
return s[k] && s[k][0] || false;
}
});
})(jQuery);
Axis too are extensible. Until now I only considered the "." (dot) to access child property, but there's probably more I could do. So I let room for expansion here too.
(function($){
axis: { // scope, property
'.': function(s, k) {
return $.jpath.getObj(s, k);
}
}
})(jQuery);
It's an experimental plugin, I still haven't tested really it so use it on a production site at your own risks.
Feedbacks are more than welcome. If you have any improvements idea or suggestions, please don't hesitate.
VLC is without a doubt my favorite video player, in my opinion it's simply the best. It's really cross platform, intuitive, fast and light. Oh, and the keyboard shortcuts are great too.
But there is one oddly head banging feature which drives me crazy in term of usability: fullscreen mode on a multi display setup.
I don't know if Windows users are afflicted by this (I use only Ubuntu), but here's what I have to do just in order to choose the monitor in which I want fullscreen mode:
That's freaking insane. The normal and expected behavior for a end user is simply to:
The Screen for fullscreen mode option could simply be changed to Force screen for fullscreen mode. -1 could mean auto and higher than -1, the screen number.
But then again VLC beat most other players, because it's an Open Source project. Unfortunately, I have no C programming skill so I can't implement this improvement myself, but at least I can suggest it.
I really hope this improvement idea will makes its way into a future release and kudos to the dev team for their great work.