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.
Il peut arriver qu’une classe de Redmine ne se comporte pas exactement comme vous le voudriez, ou que vous souhaitiez lui ajouter des propriétés.
C’est décrit en anglais sur la page Plugin Internals / Extending the Redmine Core du wiki officiel, qui renvoie vers la lecture de certains plugins d’Eric Davis pour des exemples.
Petit apparté, je partage assez l’analyse selon laquelle il est quasi inutile de maintenir une API pour surcharger les modèles / controlleurs. Cela dit, parfois les méthodes sont extrêmement longues et/ou sujettes à de fréquents changements. Toute surcharge dans un plugin induit donc un risque pour les futures versions du core…
Retour à nos moutons : admettons qu’on veuille ajouter au modèle Issue une méthode d’instance whoami qui retournerait “Je suis le ticket #XXX”. Exemple bidon, c’est pour la science.
Si on applique ce que préconise Eric, ça donne quelque chose de ce genre :
#init.rb require_dependency 'issue_patch' Dispatcher.to_prepare do Issue.send(:include, IssuePatch) unless Issue.included_modules.include? IssuePatch end #lib/issue_patch.rb require_dependency 'issue' module IssuePatch def self.included(base) base.extend(ClassMethods) base.send(:include, InstanceMethods) base.class_eval do unloadable #permet de décharger la classe en mode dev end end #ici nos méthodes de classe module ClassMethods end #ici nos méthodes d'instance module InstanceMethods def whoami "Je suis le ticket ##{self.id}" end end end
Classique, mais comme diraient certains amis “on voit pas trop ce que ça fait”.
Personnellement je préfère réouvrir la classe Issue, et ça a l’air de marcher tout aussi bien (en dev et en prod) :
#init.rb config.to_prepare do require_dependency 'issue_patch' end #lib/issue_patch.rb require_dependency 'issue' class Issue def whoami "Je suis le ticket ##{self.id}" end end
Différences :
- utilisation de “config” au lieu de “Dispatcher” ; sans importance à mon avis. C’est discuté un peu ici.
- ré-ouverture de la classe plutôt qu’inclusion d’un module ; je trouve ça plus lisible pour ce coup-ci
Attention, je ne dis pas que ce que fait Eric fonctionne moins bien. Au contraire, c’est peut-être plus “propre”, mais n’étant pas un développeur confirmé, si je ne comprends pas au premier coup d’oeil ce que j’ai fait, j’ai plus de mal à maintenir mon code.
Au passage, c’est une mauvaise idée d’appeler son patch “lib/issue_patch.rb”. Si tout le monde fait ça, on ne pourra pas faire fonctionner 2 plugins qui patchent la même classe en même temps. Beurk. D’ailleurs, c’était le cas pour des plugins à moi, donc autant utiliser des noms a priori uniques : commit redmine_drafts/ec06b8
Un billet en forme de petite note pour moi-même, relatif à mes découvertes de la soirée.
J’ai besoin d’exécuter des commandes avec Chef. Pour cela, il y a la ressource Execute :
execute "ma commande"Mais la documentation prévient bien :
By their nature, Execute resources are not idempotent, as they are completely up to the user’s imagination. Use the not_if or only_if meta parameters to guard the resource for idempotence.
OK, allons-y :
execute "ma commande" do not_if "ma condition shell" end
Si la commande doit être exécutée avec un user particulier :
execute "ma commande" do user "tom" not_if "ma condition shell" end
Mais la condition, elle, sera exécutée dans un contexte root (puisqu’il vaut mieux lancer chef-solo ou chef-client en root si l’on veut que la plupart des ressources fonctionnent). En général la condition serait à exécuter avec le même user. D’où :
execute "ma commande" do user "tom" not_if "ma condition shell", :user => "tom" end
Je trouve pas ça très joli. A réfléchir.
PS: si on veut se convaincre que ça se passe bien comme je dis :
execute "whoami > /tmp/whoami.execute" do user "tom" only_if "whoami > /tmp/whoami.only_if" end