Ce billet est nue traduction du très intéressant Javascript Internationalisation initialement écrit par Matthew Sommerville pour le site 24 ways to impress your friends. Il m’a semblé intéressant de le traduire car l’internationalisation (i18n) et la localisation (l10n) sont des sujets complexes qui posent encore beaucoup de problèmes.

Dunder était assis, regardant tristement son écran.

– Quoi de neuf Dunder ? demanda Rudolph en secouant la neige de ses bois à l’entrée de l’étable.
– Je viens de terminer de développer le nouvel intranet des rennes demandé par le Père Noël. Tu sais comme il aime avoir l’air à la page, et comme il nous rabâche constamment les oreilles avec le web 2.0, l’AJAX, les bords arrondis ; il nous a même parlé de Comet en nous le présentant comme le nouveau serveur web à la mode.
– Et alors ?
– Jusqu’ici, ce que j’ai fait lui plaît bien, et c’est en plus utilisable, accessible et joli. Cependant, comme les elfes seront les principaux utilisateurs du site, et qu’ils viennent de partout, le site doit fonctionner en plusieurs langues. C’est très bien, sauf pour la preview en javascript que j’ai écrite pour le formulaire de commandes des rennes. Regarde…

Tout en m’expliquant cela, il afficha le formulaire de commande en français.

– Ça a l’air bien, dit Rudolph.
– Oui, mais si j’ajoute des éléments, la preview est en anglais, et c’est en dur dans le Javascript. Je ne veux pas faire du code différent pour chaque langage, c’est stupide. J’ai bien pensé à mettre tout ça dans des conditions, mais ça ne va pas non plus.
– Et regarde, tu n’affiches pas non plus les grands nombres correctement sur la version française, ajouta Rudolph qui jouait depuis un moment avec le code source.

function update_text() {
  var hay = getValue('hay');
  var carrots = getValue('carrots');
  var bells = getValue('bells');
  var total = 50 * bells + 30 * hay + 10 * carrots;
  var out = 'You are ordering '
    + pretty_num(hay) + ' bushel' + pluralise(hay) + ' of hay, '
    + pretty_num(carrots) + ' carrot' + pluralise(carrots)
    + ', and ' + pretty_num(bells) + ' shiny bell' + pluralise(bells)
    + ', at a total cost of <strong>' + pretty_num(total)
    + '</strong> gold pieces. Thank you.';
    
    document.getElementById('preview').innerHTML = out;
}

function pretty_num(n) {
  n += '';
  var o = '';

  for (i = n.length; i > 3; i-= 3) {
    o = ',' + n.slice(i-3, i) + o;
  }
  
  o = n.slice(0, i) + o;
  return o;
}
 
function pluralise(n) {
  if (n != 1) 
    return 's';
  return '';
}

– Eh merde ! s’écria Dunder. C’est vraiment trop compliqué.
– Ça n’est pas si compliqué que ça. Tu dois juste changer ta manière de voir le problème. Comme nous avons besoin de faire quelque-chose de simple, nous n’avons pas à couvrir toutes les possibilités. Pour commencer, nous devons fournir au script les informations relatives à la langue utilisée. On va donc créer un objet global qu’on va appeler, au hasard i18n, auquel on va passer l’ensemble des informations linguistiques nécessaires. Notre première variable sera d’ailleurs le séparateur numérique pour les milliers, et nous allons donc modifier notre fonction pretty_num :

function pretty_num(n) {
  n += '';
  var o = '';
  
  for (i = n.length; i > 3; i-= 3) {
    o = i18n.thousands_sep + n.slice(i - 3, i) + o;
  }

  o = n.slice(0, i) + o;
  return o;
}

– Notre objet i18n contiendra également toutes les chaînes traduites, auxquelles nous accéderons via une fonction nommée _(). La majorité des langages utilisent une fonction portant le même nom pour traiter les traductions (à commencer par le très répandu gettext NDT). C’est très simple :

function _(s) {
  if (typeof(i18n) != 'undefined' && i18n[s]) {
    return i18n[s];
  }
  return s;
}

– Tu vois, si on a une traduction, on l’utilise, dans le cas contraire, on retourne la chaîne passée en argument. C’est d’ailleurs bien pratique quand la traduction du site prend du retard entre deux version, au moins on affiche quelque-chose.
– Je vois. _(“Salut Dunder”) affichera la traduction de cette phrase si elle existe, et dans le cas contraire, “Salut Dunder”.
– Exactement ! Mais continuons. Ta fonction plural ne fonctionne pas, même en français, pour tous les mots dont le pluriel ne se forme pas en rajoutant un simple “s”, comme dans “soupirail”.
– Tu as raison ! Comment ai-je pu ne pas y penser ?
– Pas de soucis. On va simplement fournir le singulier et le pluriel des mots à la fonction, et décider lequel des deux utiliser. Ça nous permettra de traiter n’importe quelle traduction :

function pluralise(s, p, n) {
  if (n != 1) 
    return _(p);
  return _(s);
}

– Évidemment, ça ne marche pas partout, et il nous faudra créer une fonction pour certaines langues à mesure que nous employons plus d’elfes. Par exemple, en polonais, le mot “fichier” devient au pluriel : 1 plik, 2-4 pliki, 5-21 plików, 22-24 pliki, 25-31 plików, et ainsi de suite.
– Eh ben…
– Ce n’est pas fini. La construction des phrases n’est pas la même en fonction de la langue utilisée. Nous ne pouvons donc plus utiliser ta fonction de concaténation, car elle ne fonctionnera jamais. Nous devons au contraire conserver des phrases cohérentes. Et cela nous donne alors :

function update_text() {
  var hay = getValue('hay');
  var carrots = getValue('carrots');
  var bells = getValue('bells');
  var total = 50 * bells + 30 * hay + 10 * carrots;

  hay = sprintf(pluralise('%s bushel of hay', '%s bushels of hay', hay), pretty_num(hay));
  carrots = sprintf(pluralise('%s carrot', '%s carrots', carrots), pretty_num(carrots));
  bells = sprintf(pluralise('%s shiny bell', '%s shiny bells', bells), pretty_num(bells));
  var list = sprintf(_('%s, %s, and %s'), hay, carrots, bells);
  var out = sprintf(_('You are ordering %s, at a total cost of <strong>%s</strong> gold pieces.'),
  list, pretty_num(total));
  out += ' ';
  out += _('Thank you.');
  document.getElementById('preview').innerHTML = out;
}

– Sprintf est une fonction courante, à laquelle on passe une chaîne de formats et quelques variables, et ces dernières prennent leur place dans la chaîne. Javascript ne dispose pas d’une telle fonction, il va donc nous falloir coder la nôtre. On va faire simple, et ne traiter que les nombres entiers et les chaînes de caractères. Je suis certain qu’il en existe de bien plus complètes sur le web.

function sprintf(s) {
  var bits = s.split('%');
  var out = bits[0];
  var re = /^([ds])(.*)$/;

  for (var i = 1; i < bits.length; i++) {
    p = re.exec(bits[i]);

    if (!p || arguments[i]==null) 
      continue;

    if (p[1] == 'd') {
      out += parseInt(arguments[i], 10);
    } 
    else if (p[1] == 's') {
      out += arguments[i];
    }
    out += p[2];
  }

  return out;
}

– Enfin, il nous faut créer un fichier pour chaque langue, dans laquelle nous allons déclarer notre objet i18n. Voilà ce à quoi un fichier de référence pourrait ressembler dans le cas de ton formulaire de commandes :

var i18n = {
  thousands_sep: ',',
  "%s bushel of hay": '',
  "%s bushels of hay": '',
  "%s carrot": '',
  "%s carrots": '',
  "%s shiny bell": '',
  "%s shiny bells": '',
  "%s, %s, and %s": '',
  "You are ordering %s, at a total cost of <strong>%s</strong> gold pieces.": '',
  "Thank you.": ''
};

– Si tu veux appliquer ça à tout notre intranet, tu devrais utiliser xgettext, qui te permet d’extraire automatiquement toutes les chaînes de caractères à traduire depuis un grand nombre de fichiers source. Ça te génère ensuite un fichier .po standard. Tu peux ensuite utiliser un script différent afin de générer un fichier de traduction type à partir d’un fichier .po. Et voilà, du javascript localisé en français, en anglais, et en allemand, utilisant du code commun.
– Merci beaucoup Rudolph !
– Ce n’est rien. Oh, un dernier truc : n’oublie pas de commenter ton fichier afin de bien expliquer le contexte d’utilisation de chaque phrase à ton traducteur. Ainsi, il évitera les contresens et les faux amis et perdra beaucoup moins de temps.

Perry the Platypus wants you to subscribe now! Even if you don't visit my site on a regular basis, you can get the latest posts delivered to you for free via Email: