Internationaliser

son JS dans la joie

Une présentation de Christophe Porteneuve à #FranceJS 2013

Salut

Moi c’est Christophe

JS Attitude, Git Attitude

Paris Web, toussa…

@porteneuve

Petit glossaire

Terme Définition
L10n Localization (régionalisation) : traduction et adaptation à la culture pour une certaine région, un certain pays
i18n Internationalization : adaptation technique ouvrant la voie à de multiples régionalisations
g11n Globalization : i18n + L10n

Le problème

Les développeurs s’en foutent…

…ou alors ils n’y pensent pas…

…ou alors ils ont la flemme…

…ou alors leurs outils puent.

Niveau −1

vos 1 invités sont prévenus

Aucune personnalisation (masculin pluriel pour tout)

Niveau 0

vos 1 invité(e)(s) est (sont) prévenu(e)(s)

« Je sais que c’est pourri mais ni le temps ni l’envie »
(on se cantonne en général au masculin ou à du partiel)

Niveau 1

votre invité(e) est prévenu(e)

Effort sur le nombre, mais pas sur le genre
La majorité des efforts s'arrête là :-(

Niveau 2

votre invitée est prévenue

La classe à Dallas : déclinaison genre + nombre
(on ne sait toutefois pas à quel degré de pluralisation)

Niveau 42

votre invitée a bien reçu les 1 042 messages renvoyés ce lundi

Achievement unlocked: g11n Wizard!

Déclinaison à contextes multiples + formatage

Le format c’est 2 points

(spéciale dédicace à @notabene et @ElieSl)

Les dates et les nombres

samedi, mai 18 2013 à 5:00, le thé…
Il y a 2,345 propositions à évaluer
Un T-shirt à seulement €7.95 !

Côté standards…

Le TC39 a compris qu’il faut quelque chose…

Le standard ECMA-402, à savoir la ECMAScript Internationalization API, comble un manque, mais attention !

Uniquement pour le formatage et la collation (comparaison/tri de textes, etc.)

C’est déjà pas mal !

Déterminer la langue

Adoptez une approche à priorité croissante

  1. L'en-tête HTTP Accept-Language (~ navigator.language)
  2. Un cookie (persiste un choix explicite)
  3. Un paramètre dans l’URL :
    • sous-domaine : fr.wikipedia.org
    • path info : apple.com/fr/…
    • query string : example.com/?locale=fr&…
  4. L’attribut lang= de <html> (que côté client, a posteriori)

Au niveau du HTML

La langue active, ce n’est pas anodin…

Quand tu l’oublies, ça peut trop foutre la honte

  • L’attribut noyau lang= (codes ISO-3166-2)
  • L’attribut noyau dir= (ltr, rtl)
  • L’élément bdo pour les cas foireux ou pré-orientés

Avec tes p’tits doigts

var TRANSLATIONS = {
  fr: {
    label: 'Changez la langue :',
    '#great': 'C’est génial !',
    // …
  },
  …
};

function applyLocale(loc) {
  loc = 'string' == typeof loc ? loc : document.documentElement.getAttribute('lang');

  var repo = TRANSLATIONS[loc];

  for (var sel in repo)
    document.querySelector(sel).innerText = repo[sel];
}

Un mot sur gettext et les *.po

Gettext est à peu près partout…

…mais il est limité à deux contextes : le nombre et un contexte libre

(en revanche il gère très bien les formes de pluriel)

Il est facile de convertir ses fichiers .po (voire .mo) vers d’autres formats, notamment JSON, pour migrer vers d’autres outils

Flexion… extension…

Le principal problème vient des pluriels irréguliers et des invariables, particulièrement fréquents en anglais

Principale solution JS : Lingo
Module Node, enrobable côté client (ex. avec browserify)

var lang = new Language('en', 'English');

lang.pluralize('data')      // => 'data'
lang.pluralize('sheep')     // => 'sheep'
lang.singularize('octopi')  // => 'octopus'
lang.singularize('axes')    // => 'axis'

Formater

pour de vrai

Les nombres (simples, monnaies, pourcentages…) et les dates/heures

numberformat.js

Portage JS du module Google Closure
(lui-même issu du JDK je crois)

var parisCash       = new NumberFormat('fr_FR', NumberFormat.Format.CURRENCY);
var phillyCash      = new NumberFormat('en_US', NumberFormat.Format.CURRENCY);
var montrealCash    = new NumberFormat('fr_CA', NumberFormat.Format.CURRENCY);
var unambiguousCash = new NumberFormat('fr_CA', NumberFormat.Format.CURRENCY,
  null, NumberFormat.CurrencyStyle.GLOBAL);

parisCash.format(Math.PI * 1000)       // => '3 141,59 €' — avec les insécables…
phillyCash.format(Math.PI * 1000)      // => '$3,141.59'
montrealCash.format(Math.PI * 1000)    // => '3 141,59 $'
unambiguousCash.format(Math.PI * 1000) // => '3 141,59 CAD $'

157 locales pris en charge !

numeral.js

Toutes sortes de formatages/analyses utiles…

numeral.language('fr');
var pim = numeral(Math.PI * 1000 * 1000 * 1000);
var pik = numeral(Math.PI * 1000);
var pi  = numeral(Math.PI * 1000);

pi.format('0.000')               // => '3,142'
pi.format('0.0%')                // => '314,2%'
pik.format('0,0.00 $')           // => '3141,59 €' -- presque…
pim.format('0.00b')              // => '2.93GB'
pik.format('0.00a')              // => '3.14k'
numeral(1).format('o')           // => '1er'
numeral(18394).format(':')       // => '5:06:34'
numeral().unformat('3 142,17 €') // => 3142.17

Pas parfait, mais ça aide…

moment.js

Écoute-moi bien

Si ton JS manipule des dates et heures

…tu arrêtes les conneries

…et tu utilises Moment !

moment.js

Calculs + formatage et analyse… en multilingue

Chaînes de format détaillées (JDK, pas strftime)

moment.lang('fr');
var today = moment('19/05/2013', 'DD/MM/YYYY');

today.week()                                      // => 20
today.add(2, 'months').format('dddd D MMMM YYYY') // => 'jeudi 18 juillet 2013'
today.endOf('month').fromNow()                    // => 'dans 3 mois'
moment().add(1, 'day').calendar()                 // => 'Demain à 10:40'
moment().endOf('month').diff(moment(), 'days')    // => 12
// Et PLEIN d’autres trucs !

Traduire

avec du contexte

i18next

Beaucoup de fonctionnalités, dispo client (détection de langue) et Node, réutilisation de clé, pluralisation riche, deux niveaux de contexte, post-processing et notifs serveur traductions manquantes, etc.

// Dans locales/fr/translation.json ou JSON local chargé :
// { "greeting": "Salut __name__ !",
//   "greet_friends_male": "Salut copain !",
//   "greet_friends_male_plural": "Salut les copains !",
//   "greet_friends_female": "Salut copine !",
//   "greet_friends_female_plural": "Salut les copines !"}
i18n.init({ lng: 'fr' });
i18n.t('greeting', { name: 'John')                      // => 'Salut John !'
i18n.t('greet_friends', { count: 3, context: 'male' })  // => 'Salut les copains !'

jsperanto

Bon équilibre entre simplicité fonctionnelle et API.

Pluralisation personnalisable, réutilisation de clé, interpolation, chargement initial ou AJAX, fallback…

Dépendance jQuery, en revanche.

// locales/fr.json (ou datastore intégré au JS) :
// { "greeting": "Bonjour __name__",
//   "inbox": "Vous avez un e-mail",
//   "inbox_plural": "Vous avez __count__ e-mails" }

$.jsperanto.init(applyLocale, { lang: 'fr' });
$.t('greeting', { name: 'Robert' }) // => 'Bonjour Robert'
$.t('inbox', { count: 17 }) // => 'Vous avez 17 e-mails'

messageformat.js

Multi-contexte avec imbrication. Le top, mais syntaxe un peu lourde, forcément…

var mf = new MessageFormat('fr');

var f = mf.compile('Il \
  {RES, plural, =0{n’y a aucun} one{y a un} other{y a #}} \
  {RES, plural, one{produit trouvé} other{produits trouvés}} \
  dans {CAT, plural, one{une catégorie} other{# catégories}}');

f({ RES: 0, CAT: 0 }) // => 'Il n’y a aucun produit trouvé dans une catégorie'
f({ RES: 1, CAT: 1 }) // => 'Il y a un produit trouvé dans une catégorie'
f({ RES: 3, CAT: 1 }) // => 'Il y a 3 produits trouvés dans une catégorie'
f({ RES: 3, CAT: 2 }) // => 'Il y a 3 produits trouvés dans 2 catégories'

L20n

Par Mozilla, initialement pour Firefox OS.
Responsive. Mais bordel côté format & mise en œuvre…

<name "L20n">

<welcome "{{ name }} by Mozilla">

<position() { @screen.width.px < 1140 ?
             "default" : "right" }>

<step4[position()] {
  default: """
    4. Start localizing a resource file
    as you see below.
  """,
  right: """
    4. Start localizing a resource file
    as you see on the right.
  """
}>

Gérer la trad

Basé Rails ?

Approche engine : Tolk

Approche SaaS : Locale

Les 2 : YAML + sérialisation JSON dans la vue initiale

  • Capture d’écran de Tolk
  • Capture d’écran de Locale

Webtranslateit

Service en ligne multi-formats, type Locale mais avec une insistance sur les workflows de relecture/approbation.

  • Capture d’écran de Webtranslateit

messageform.at

Outil open-source pour simplifier les saisies MessageFormat avec des exemples concrets. Encore balbutiant mais très, très bonne contextualisation pour assister la traduction.

Petit tour de l'interface vite fait…

Intégration

avec l’écosystème

RequireJS

Plugin i18n

Archi intéressante avec fallbacks progressifs, etc. mais une limite à connaître : basé modules → langue déterminée une seule fois

Framework : Angular

Angular propose le formatage pour les dates/heures, nombres et monnaies, ainsi que la pluralisation, mais rien de spécial pour la traduction.

Framework : Ember

Extensions Handlebars intégrées basées sur CLDR.js pour la traduction et la pluralisation, qu’on charge comme on veut. Juste la pluralisation comme contexte, mais CLDR.

Côté Node : i18n-2

Interfaçage avancé Express, détection/persistence de langue, helpers de traduction et de pluralisation, compatibilité Webtranslateit…

Côté Node : i18next (aussi !)

Toutes les fonctions du côté client + sa propre UI de gestion et un bon interfaçage Express.

Côté Node : jus-i18n

Similaire à i18n-2 mais plus flexible sur les stockages de traductions. Déjà pas mal de choses mais (comme d’hab) juste 2 niveaux de contexte.

Côté Node : frameworks applicatifs

Prise en charge « façon Rails » CompoundJS, TowerJS, etc.

Merci !

@porteneuve

JS Attitude