{"id":656,"date":"2010-01-08T11:02:16","date_gmt":"2010-01-08T10:02:16","guid":{"rendered":"http:\/\/meier-online.com\/?p=656"},"modified":"2015-02-18T22:37:53","modified_gmt":"2015-02-18T21:37:53","slug":"mehrsprachige-webseiten-mit-ruby-on-rails-3","status":"publish","type":"post","link":"https:\/\/meier-online.com\/en\/2010\/01\/mehrsprachige-webseiten-mit-ruby-on-rails-3\/","title":{"rendered":"Multilingual Websites with Ruby on Rails (3)"},"content":{"rendered":"<p>Part 3 &#8211; Quality Assurence<\/p>\n<p>Texte geh\u00f6ren zu den Teilen einer Anwendung, die h\u00e4ufig ge\u00e4ndert werden. Wenn dann noch externe \u00dcbersetzer im Spiel sind, kann es leicht zu fehlenden \u00dcbersetzungen kommen<\/p>\n<p>Wir m\u00f6chten deshalb automatisch testen, dass es zu allen Texten in Sprache A auch \u00dcbersetzungen in Sprache B vorhanden sind. Ein solcher Test allein hilft schon sehr, und sollte ja nicht so schwierig sein.<\/p>\n<p>Leider gibt es dabei eine \u00dcberraschung: in der Internationalisierung-API findet sich zur Zeit (Rails 2.3.5) keine M\u00f6glichkeit, alle Schl\u00fcssel aufzuz\u00e4hlen (sprich iterieren). Man muss also offenbar die Yaml-Dateien selbst einlesen, um an alle Schl\u00fcssel zu kommen. Unserer Test ist dabei leider abh\u00e4ngig von der konkreten Art der Datenhaltung.<\/p>\n<pre lang=\"ruby\">\r\ntexts_hash = YAML::load(IO.read('config\/locales\/de.yml'))<\/pre>\n<p>Wie haben nun einen rekursiven Hash. Um alle Schl\u00fcssel zu \u00fcberpr\u00fcfen, extrahieren wie sie mit einer gesonderten Funktion. Diese Funktion ist einfach rekursiv aufgebaut.<\/p>\n<pre lang=\"ruby\">\r\ndef deep_hash_key(hash, prefix)\r\n  hash.keys.sort.collect{|key2|\r\n    val = hash[key2]\r\n    newprefix = prefix.nil? ? key2 : \"#{prefix}.#{key2}\"\r\n    if val.is_a? Hash\r\n     deep_hash_key(val, newprefix)\r\n    else\r\n      newprefix\r\n    end\r\n  }\r\nend<\/pre>\n<p>Wollen wir jetzt die \u00dcbersetzungen f\u00fcr alle Schl\u00fcssel pr\u00fcfen lauert die Zweite Falle: Wenn die \u00dcbersetzung eine Variableninterpolation enth\u00e4lt, erhalten wir einen Fehler beim Aufrufen der translate() Methode ohne den passenden Hash. Offenbar liefert uns die API auch keinen Zugriff auf den String ohne Interpolierung. Da uns in diesen Test nur die Existenz einer \u00dcbersetzung interessiert, und nicht ihr konkreter Wert, fangen wir die Exception &#8220;MissingInterpolationArgument&#8221; einfach auf.<\/p>\n<p>F\u00fcr eine deutsche Anwendung mit \u00dcbersetzungen in Englisch und Franz\u00f6sisch sieht dann der Test so aus:<\/p>\n<pre lang=\"ruby\">\r\ndef test_all_texts_should_exists\r\n  texts_hash = YAML::load(IO.read('config\/locales\/de.yml'))\r\n  de_keys = deep_hash_key(texts_hash['de'], nil).flatten\r\n  for lang in ['de', 'fr', 'en']\r\n    I18n::locale = lang\r\n    for key in de_keys\r\n      begin\r\n        val = I18n.translate(key, :default =&gt; nil, :raise =&gt; true)\r\n      rescue I18n::MissingTranslationData\r\n        val = nil\r\n      rescue I18n::MissingInterpolationArgument\r\n        val = true  # there was a value, only an argument is missing\r\n      end\r\n      assert_not_nil(val, \"key missing for locale #{lang}: #{key}\")\r\n    end\r\n  end\r\nend<\/pre>\n<p>Wenn jetzt eine \u00dcbersetzung fehlt, liefert der Test eine Fehlermeldung der Art:<\/p>\n<pre>\r\ntest_all_texts_should_exists(LocalisationTest): key missing for locale en: users.edit.title<\/pre>\n<p>Die fehlende \u00dcbersetzung k\u00f6nnen wir dann erg\u00e4nzen, bevor wir eine neue Version der Anwendung ver\u00f6ffentlichen.<!--more--><\/p>","protected":false},"excerpt":{"rendered":"<p>Sorry, this entry is only available in Deutsch.<\/p>\n","protected":false},"author":4,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[104],"tags":[126,114],"class_list":["post-656","post","type-post","status-publish","format-standard","hentry","category-develop","tag-mehrsprachigkeit","tag-ruby","entry"],"_links":{"self":[{"href":"https:\/\/meier-online.com\/en\/wp-json\/wp\/v2\/posts\/656"}],"collection":[{"href":"https:\/\/meier-online.com\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/meier-online.com\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/meier-online.com\/en\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/meier-online.com\/en\/wp-json\/wp\/v2\/comments?post=656"}],"version-history":[{"count":14,"href":"https:\/\/meier-online.com\/en\/wp-json\/wp\/v2\/posts\/656\/revisions"}],"predecessor-version":[{"id":2026,"href":"https:\/\/meier-online.com\/en\/wp-json\/wp\/v2\/posts\/656\/revisions\/2026"}],"wp:attachment":[{"href":"https:\/\/meier-online.com\/en\/wp-json\/wp\/v2\/media?parent=656"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/meier-online.com\/en\/wp-json\/wp\/v2\/categories?post=656"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/meier-online.com\/en\/wp-json\/wp\/v2\/tags?post=656"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}