Ruby dispose d’une large palette de bibliothèques. Parmi ce qu’il y a de plus indispensable on compte l’internationalisation, ou i18n. La référence est l’implémentation de Sven Fuchs. C’est cette gem qui est utilisée par Rails pour traiter tout ce qui concerne l’i18n.
Dans cet article je vais étendre les fonctionnalités d’i18n. L’objectif est de pouvoir choisir une langue de référence puis d’être en mesure de lister les clés manquantes ainsi que les clés non utilisées. La première situation où un tel système serait utile c’est lors d’une suite de tests.
Rails utilise par défaut I18n::Backend::Simple pour les traductions.
Ce backend repose sur I18n::Backend::Base.
C’est la méthode translate
de ce dernier module qui va nous intéresser…
En effet, nous allons étendre le backend pour qu’il intercepte et enregistre tout les appels à translate et si oui ou non il ont échoués. Pour parvenir à ce résultat je vais m’inspirer de i18n-missing_translation.
L’idée est d’arriver à surcharger la methode translate originale avec :
def translate(locale, key, options = {})
result = catch(:exception) { super }
if result.kind_of? I18n::MissingTranslation
# La clé est manquante
throw :exception, result
else
# La clé est utilisée par l'application
result
end
end
Dans un premier temps on va créer une classe Store
qui va permette de stocker les clés de traduction.
Voici le contenu de cette classe avec, juste en dessous du code, quelques explications.
class Store < Hash
def use(l, k, o) ; add_key(l, k , o, :used) end
def miss(l, k, o) ; add_key(l, k, o, :missing) end
def used?(keys)
keys.inject(self){|h, k| h.kind_of?(Hash) ? h[k] : (break h[k])} == :used
end
protected
def add_key(l, k, o, value)
keys = I18n.normalize_keys(l, k, o[:scope]).dup
l = keys.pop.to_s
h = keys.inject(self){|h, k| h.key?(k.to_s) ? h[k.to_s] : (h[k.to_s] = {})}
h[l] = value
end
end
La méthode translate
utilise les paramètres : locale
, key
et options
.
La methode I18n.normalize_keys
permet de transformer ces options en un Array
de Symbol
.
Par exemple I18n.normalize_keys(:en, 'foo', 'bar.bar')
donnera ce resultat : [:en, :bar, :bar, :foo]
.
Notre classe définit deux méthodes utilisant les paramètres de translate : use
et miss
.
Elles remplaceront les commentaires dans notre surcharge de la méthode translate
par la suite…
Les deux one-liners avec inject sont plus complexes, ils réalisent tout deux une exploration en profondeur pour :
used?
) ouStore
(add_key
).À présent, on peut mettre nos deux fichiers dans un scope : ici j’ai choisi I18n::ExtraTranslations
.
Il faut également penser à inclure notre nouvelle méthode translate
dans I18n::Backend::Simple
.
Voici l’arborescence des fichiers une fois cette organisation effectuée :
lib └── i18n ├── extra_translations │ ├── simple_extension.rb │ └── store.rb └── extra_translations.rb
Les contenus des fichiers simple_extension.rb
et store.rb
ont été modifiés mais sont disponibles sur notre dépot Github.
Et voici le contenu du fichier extra_translations.rb
:
require 'i18n'
require 'i18n/exceptions'
dirname = File.dirname(__FILE__)
module I18n
class ExtraTranslations
autoload :Store, "#{dirname}/extra_translations/store.rb"
autoload :SimpleExtension, "#{dirname}/extra_translations/simple_extension.rb"
class << self
# An ExtraTranslations::Store will be availaible everywhere
attr_writer :extra_translations
def extra_translations
@extra_translations ||= ExtraTranslations::Store.new
end
# Allow the user to focus its search on a specific locale
attr_writer :locale
def locale
@locale ||= I18n.default_locale
end
# Returns all the unused translation of some files and
# groups the results by filename.
def unused_translations(filenames=nil)
filenames ||= ["./config/locales/#{locale}.yml"]
filenames.inject({}) do |memo, filename|
data = YAML.load_file(filename)
keys = all_keys_from(data, [])
memo[filename] = keys.select{|keys| !extra_translations.used?(keys)}
memo[filename].map!{|keys| keys.join('.')}
memo
end
end
# Returns all the translate keys that throw an error
def missing_translations
keys = all_keys_from(extra_translations, [], []) do |keys, _|
!extra_translations.used?(keys)
end
keys.map{ |keys| keys.join('.') }
end
protected
# Build a list of translation paths
def all_keys_from(data, memo, stack=[], &filter)
if data.kind_of? Hash
keys = data.inject([]) do |m, (k, v)|
m + all_keys_from(v, [], stack.dup << k, &filter)
end
memo + keys
elsif filter.nil? || filter.call(stack, data)
memo << stack
else
memo
end
end
end
end
Backend::Simple.include ExtraTranslations::SimpleExtension
end
Le plus important, pour obtenir un rapport à la fin des tests vous n’avez qu’a ajouter les lignes suivantes à votre spec_helper.rb
:
require './lib/i18n/extra_translations'
at_exit do
require 'awesome_print'
puts "Missing translations:"
ap I18n::ExtraTranslations.missing_translations
puts "Unused translations:"
ap I18n::ExtraTranslations.unused_translations
end
Dans cet article, tout est dans le répertoire ./lib
de l’application Rails.
J’ai toutefois pris le temps de créer une gem et d’y ajouter des tests.
L’i18n est un des aspects importants d’une application. Elle représente une forte valeur ajoutée pour les clients sans pour autant être attirante pour les développeurs. C’est pourquoi il est nécessaire d’utiliser des outils pour nous simplifier cet aspect des projets.
J’espère que cet article vous aura montré à quel point on peut créer ces outils rapidement.
L’équipe Synbioz.
Libres d’être ensemble.