Redmine Plugins #3 : supporter plusieurs langues

Redmine supporte à ce jour 37 langues. Si vous souhaitez diffuser votre plugin, c’est une bonne idée de respecter les mêmes conventions que le core, pour en faciliter les traductions, voire proposer plusieurs traductions de votre plugin directement. C’est ce que je fais par exemple pour mon plugin “Datacenter” que je livre en anglais et en français (voir la page de wiki française).

Pour cela, Redmine utilise l’internationalisation de Rails. Chaque mot ou groupe de mot qui doit être traduit est associé à une clé unique. Chaque langue a son fichier YAML dans le dossier config/locales/, et dans ce fichier on indique que telle clé correspond à telle chaine de caractères. Par exemple, plutôt que d’écrire “Mon super plugin” directement dans vos vus et helpers, vous allez lui associer une clé de votre choix, mettons text_my_super_plugin.

Dans la vue, vous pourrez utiliser le helper l() (un L minuscule) de cette façon :

<%= l(:text_my_super_plugin) %>

Ensuite vous devrez associer cette clé à sa valeur pour chaque langue. Pour le français, le fichier config/locales/fr.yml de votre plugin ressemblera à ça :

fr:
  text_my_super_plugin: Mon super plugin

Et vous pouvez traduire votre appli en anglais, en ajoutant un fichier config/locales/en.yml contenant :

en:
  text_my_super_plugin: My great plugin

Pour un texte accentué ou comportant des caractères spéciaux, il suffira de mettre la chaine entre quotes pour éviter toute confusion lors de l’analyse du fichier. Attention à ce que votre fichier reste bien en UTF8 tout de même.

Si la traduction n’existe pas (fichier de langue manquant ou clé inexistante dans la langue de l’utilisateur), Redmine affichera une erreur. C’est la que le helper l_or_humanize peut être utile :

<%= l_or_humanize(:super_plugin) %>

Si la clé existe, elle sera remplacée par sa traduction. Si non, Rails tentera d’en faire une chaine pour humain (remplacement des underscores par des espaces, majuscule à la première lettre, etc.). En l’occurrence Super plugin.

Pour les affichages de dates, heures, temps ou intervalles de temps, il existe des helpers beaucoup plus évolués que ceux présentés ci-dessus. Ils sont définis dans lib/redmine/i18n.rb. En voici une liste, ainsi que comment les tester dans une console Rails :

% ruby script/console
Loading production environment (Rails 2.3.5)
>> include Redmine::I18n
=> Object
>> set_language_if_valid('fr')
=> :fr
>> l_hours(5)
=> "5.00 heures"
>> format_date(Time.now)
=> "26/04/2010"
>> format_time(Time.now)
=> "26/04/2010 19:55"
>> day_name(1)
=> "lundi"
>> month_name(3)
=> "mars"

A des fins de test, le helper ll() permet de préciser d’abord la locale avant la clé et ainsi de tester une clé dans une locale particulière :

>> ll("fi", :field_mail)
=> "Sähköposti"

Dernière chose, il est possible d’utiliser des variables dans vos fichiers de langue. Ils seront interpolés lors du rendu de la vue. Si vous n’avez qu’une variable à mettre, vous pouvez utiliser le nom “value” et passer la valeur dans votre vue directement en 2e argument de votre l(). Si vous avez 2 variables ou plus, il faut leur donner un nom et passer un hash en 2e argument de l() dans votre vue. Evidemment ces valeurs peuvent elles-même faire appel à vos traductions pour éviter de dupliquer des traductions.

Un exemple vaut mieux qu’un long discours. Avec ce fichier de langue :

fr:
  label_draft_saved_time: "Brouillon sauvegardé à {{value}}"
  label_draft_pending: "Brouillon en attente, sauvegardé il y a {{time}} : {{restore}} ou {{delete}}"
  label_draft_restore: "restaurer"
  label_draft_delete: "supprimer"

Je peux faire appel à ceci dans mes vues (les valeurs de temps sont bidon) :

<%= l(:label_draft_saved_time, format_time(Time.now)) %>
<%= l(:label_draft_pending, {:time => format_time(Time.now),
                             :restore => l(:label_draft_restore),
                             :delete => l(:label_draft_delete)}) %>

J’essaierai de documenter tout ça en anglais dans le wiki Redmine un de ces 4.