Translate: New Rails I18n Plugin with a Nice Web UI

Here at Mynewsdesk we are in the midst of internationalizing a fairly big Ruby on Rails application. Sven Fuchs’s I18n Textmate bundle has been a great help in extracting thousands of texts away from our source code and into YAML files. As we were getting ready to start the actual translation work we figured that a nice web interface would be helpful. Since we couldn’t find a web UI out there we decided to roll our own and thus the translate plugin was born.

The plugin requires that you run Rails 2.2 or later. It lets you mount a web UI at /translate where you can view and translate all your I18n texts. You can select which locales you are translating from and to, search for keys and texts, and choose to view only untranslated texts. The plugin reads and writes all translations to YAML files under config/locales. For more details, please refer to the README file.

Screenshot of the User Interface

Newsdesk translate I18n screenshot

Newsdesk translate I18n screenshot

41 thoughts on “Translate: New Rails I18n Plugin with a Nice Web UI

  1. How do you enable support fro utf-8 in yaml? Installing ya2yaml doesn’t seem to finish the job because non-latin characters are written as binary (?!). I am working on Windows, haven’t tried on my linux machine yet.

    Other from the problem, really useful plugin.

  2. Peco,
    I don’t know what the issue with ya2yaml is. We use it successfully on Linux and Mac OSX to write swedish chars in UTF8. I don’t know how ya2yaml works so I can’t really offer much help there. Sorry.

    Peter

  3. Hi Peter,

    the problem was the usual suspect called Windows, I tried it on my Ubuntu and it works fine. It seems that ya2yaml doesn’t recognize utf-8 characters on windows (?!) for some reason.

    Thanks

  4. Looks nice, i have one problem with it though, i have an app with several keys as strings with spaces. The get all messed up once i save:

    “Location / gérance”: “Location / Gérance”

    becomes

    ? “Location / gérance”
    : “Location / Gérance”

  5. Peter, thx for nice plugin !

    Is there a chance for small addition?
    I think it would be very useful to group keys by key.pattern. For example i use txt.meetings.incoming as a key and it would be nice if all txt.meetings keys would be grouped under link like those “all | untranslated | translated”. In this case link would be f.e. “txt.meetings”
    Seem like not too difficult to implement through some magical ruby hackery…

  6. Wow. Awesome and really promising. At this very moment i find it really useful in a new project of mine. Good job and thank you.

  7. Very nice work! Hope you don’t mind one tweak: If you would like to add a simple Ajax google translate link below each form Element, you can add this:

    In the plugin’s layout file, insert this on line #8:
    —-

    google.load(“language”, “1″);

    function getGoogleTranslation(id, text, from_language, to_language) {
    google.language.translate(text, from_language, to_language, function(result) {
    if (!result.error) { $(id).writeAttribute({ value : result.translation }); }
    });

    }

    —-
    In the index.html view file: Add this on line #90
    —-
    ‘padding: 0; margin: 0;’ %>

    —-

    This passes the english text to the Google translation’s engine and fills the return into the form field so you can clean it up further before saving. Bug: if the string has a single quote, this will not work yet.

  8. @tim Thanks for your contribution, it certainly looks useful. The easiest way to pull it in to translate i probably for you to fork the project and send us a pull request.
    Regards, Marten

  9. Awesome plugin! Exactly what we need right now.

    One question: The plugin writes changes directly to the yaml files. Do you commit these changes manually or what is your best practice?

  10. After install translate, I got a problem with Rails 2.3.2

    Showing vendor/plugins/translate/views/translate/index.rhtml where line # raised:
    private method `gsub' called for ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]:Array
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_view/helpers/javascript_helper.rb:143:in `escape_javascript'
    /home/simoniong/app/test_goldberg/vendor/plugins/translate/views/translate/index.rhtml:95:in `_run_rhtml_vendor47plugins47translate47views47translate47index46rhtml'
    /home/simoniong/app/test_goldberg/vendor/plugins/translate/views/translate/index.rhtml:73:in `each'
    /home/simoniong/app/test_goldberg/vendor/plugins/translate/views/translate/index.rhtml:73:in `_run_rhtml_vendor47plugins47translate47views47translate47index46rhtml'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_view/helpers/capture_helper.rb:36:in `call'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_view/helpers/capture_helper.rb:36:in `capture'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_view/helpers/capture_helper.rb:129:in `with_output_buffer'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_view/helpers/capture_helper.rb:36:in `capture'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_view/helpers/form_tag_helper.rb:460:in `form_tag_in_block'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_view/helpers/form_tag_helper.rb:39:in `form_tag'
    /home/simoniong/app/test_goldberg/vendor/plugins/translate/views/translate/index.rhtml:59:in `_run_rhtml_vendor47plugins47translate47views47translate47index46rhtml'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_view/renderable.rb:34:in `send'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_view/renderable.rb:34:in `render'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_view/base.rb:301:in `with_template'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_view/renderable.rb:30:in `render'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_view/template.rb:194:in `render_template'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_view/base.rb:260:in `render'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_view/base.rb:343:in `_render_with_layout'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_view/base.rb:257:in `render'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/base.rb:1241:in `render_for_file'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/base.rb:937:in `render_without_benchmark'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/benchmarking.rb:51:in `render'
    /home/simoniong/.gem/ruby/1.8/gems/activesupport-2.3.2/lib/active_support/core_ext/benchmark.rb:17:in `ms'
    /home/simoniong/.gem/ruby/1.8/gems/activesupport-2.3.2/lib/active_support/core_ext/benchmark.rb:10:in `realtime'
    /home/simoniong/.gem/ruby/1.8/gems/activesupport-2.3.2/lib/active_support/core_ext/benchmark.rb:17:in `ms'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/benchmarking.rb:51:in `render'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/base.rb:1317:in `default_render'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/base.rb:1323:in `perform_action_without_filters'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/filters.rb:617:in `call_filters'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/filters.rb:610:in `perform_action_without_benchmark'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/benchmarking.rb:68:in `perform_action_without_rescue'
    /home/simoniong/.gem/ruby/1.8/gems/activesupport-2.3.2/lib/active_support/core_ext/benchmark.rb:17:in `ms'
    /home/simoniong/.gem/ruby/1.8/gems/activesupport-2.3.2/lib/active_support/core_ext/benchmark.rb:10:in `realtime'
    /home/simoniong/.gem/ruby/1.8/gems/activesupport-2.3.2/lib/active_support/core_ext/benchmark.rb:17:in `ms'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/benchmarking.rb:68:in `perform_action_without_rescue'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/rescue.rb:160:in `perform_action_without_flash'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/flash.rb:141:in `perform_action'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/base.rb:523:in `send'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/base.rb:523:in `process_without_filters'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/filters.rb:606:in `process'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/base.rb:391:in `process'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/base.rb:386:in `call'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/routing/route_set.rb:433:in `call'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/dispatcher.rb:88:in `dispatch'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/dispatcher.rb:111:in `_call'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/dispatcher.rb:82:in `initialize'
    /home/simoniong/.gem/ruby/1.8/gems/activerecord-2.3.2/lib/active_record/query_cache.rb:29:in `call'
    /home/simoniong/.gem/ruby/1.8/gems/activerecord-2.3.2/lib/active_record/query_cache.rb:29:in `call'
    /home/simoniong/.gem/ruby/1.8/gems/activerecord-2.3.2/lib/active_record/connection_adapters/abstract/query_cache.rb:34:in `cache'
    /home/simoniong/.gem/ruby/1.8/gems/activerecord-2.3.2/lib/active_record/query_cache.rb:9:in `cache'
    /home/simoniong/.gem/ruby/1.8/gems/activerecord-2.3.2/lib/active_record/query_cache.rb:28:in `call'
    /home/simoniong/.gem/ruby/1.8/gems/activerecord-2.3.2/lib/active_record/connection_adapters/abstract/connection_pool.rb:361:in `call'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/vendor/rack-1.0/rack/head.rb:9:in `call'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/vendor/rack-1.0/rack/methodoverride.rb:24:in `call'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/params_parser.rb:15:in `call'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/rewindable_input.rb:25:in `call'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/session/cookie_store.rb:93:in `call'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/reloader.rb:9:in `call'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/failsafe.rb:11:in `call'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/vendor/rack-1.0/rack/lock.rb:11:in `call'
    /usr/lib/ruby/1.8/thread.rb:135:in `synchronize'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/vendor/rack-1.0/rack/lock.rb:11:in `call'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/dispatcher.rb:106:in `call'
    /usr/lib/ruby/gems/1.8/gems/rails-2.3.2/lib/rails/rack/static.rb:31:in `call'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/vendor/rack-1.0/rack/urlmap.rb:46:in `call'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/vendor/rack-1.0/rack/urlmap.rb:40:in `each'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/vendor/rack-1.0/rack/urlmap.rb:40:in `call'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/vendor/rack-1.0/rack/content_length.rb:13:in `call'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/vendor/rack-1.0/rack/handler/webrick.rb:46:in `service'
    /usr/lib/ruby/1.8/webrick/httpserver.rb:104:in `service'
    /usr/lib/ruby/1.8/webrick/httpserver.rb:65:in `run'
    /usr/lib/ruby/1.8/webrick/server.rb:173:in `start_thread'
    /usr/lib/ruby/1.8/webrick/server.rb:162:in `start'
    /usr/lib/ruby/1.8/webrick/server.rb:162:in `start_thread'
    /usr/lib/ruby/1.8/webrick/server.rb:95:in `start'
    /usr/lib/ruby/1.8/webrick/server.rb:92:in `each'
    /usr/lib/ruby/1.8/webrick/server.rb:92:in `start'
    /usr/lib/ruby/1.8/webrick/server.rb:23:in `start'
    /usr/lib/ruby/1.8/webrick/server.rb:82:in `start'
    /usr/lib/ruby/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/vendor/rack-1.0/rack/handler/webrick.rb:13:in `run'
    /usr/lib/ruby/gems/1.8/gems/rails-2.3.2/lib/commands/server.rb:111
    /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require'
    /usr/local/lib/site_ruby/1.8/rubygems/custom_require.rb:31:in `require'
    script/server:3

    Any Hint ?

  11. Simon,
    thanks for reporting the bug related to non-string translations. It’s been fixed and committed to Github now.

    Cheers

    Peter

  12. Mirko,
    Yes we manually commit the translated files. If you translated your texts with Translate in the first place, we write a translation log that allows you to track changes that you pull from your colleagues. (“Changed” link in Translate). Might come in handy for you.

  13. Hi,

    thanks for this nice plugin, but I get an error if I visit the /translate section:

    Showing vendor/plugins/translate/views/translate/index.rhtml where line #59 raised:

    No :secret given to the #protect_from_forgery call. Set that or use a session store capable of generating its own keys (Cookie Session Store).

    My protect_from_forgery setting in the environment.rb is (in my opinion) correct (I use the active_record_store) and all forms works fine on my site. Has anyone else this problem?

    Regards, Sascha

  14. I was also getting binary data in the translation file but solve this with require ya2yaml gem in development environment.

    require ‘ya2yaml’

    Translate plug-in calls the ya2yaml method if it exists, but previously doesn’t try to require ya2yaml gem in its init and then falls back to to_yaml method.

    keys.respond_to?(:ya2yaml) ? keys.ya2yaml(:escape_as_utf8 => true) : keys.to_yaml

  15. There is a problem with UTF-8 encoding in ruby 1.9. ya2yaml gem is incompatible. Do you know any workaround to write yaml file in human format with that ruby version?

  16. This is a very neat plugin, but we’ve noticed that when new translations are added, it seems to do some reordering of the .yml files. This is causing some painful git merges when multiple people update the translation files in separate branches.

    Is there an intentional reordering of the keys being done, or is this a side effect of something else? If maintaining the key ordering would be desirable, I’d be willing to take a crack at a patch.

  17. Hi, I’m wondering how this would work for multiple app-servers on different machines. For instance, if I have three app-servers, does each server just write to it’s own local copy of config/locales/fr.yml? This would mean that the same text would be translated 3 times right? Unless I synchronised the yaml files periodically. Or can I configure the i18n loadpath to point at a shared fr.yml on a static asset server?

  18. This plugin is great!! Thanks for building it!

    I have a question. Is there any extension for your plugin that would allow us to combine it with the model-driven translation provided by the Globalize2 plugin (http://github.com/joshmh/globalize2), so that we could use the Translate plugin´s UI, to store information on the database tables created by Globalize2?

    Thanks again!

  19. Hi, I would love to use the rake task for automated google translation. I have the translate plugin installed and the web-interface including single google translations do work. For the rake task I did install the latest httparty gem (I have version 0.4.5 ob my system).

    When I run the rake task to translate from en to fr:

    rake translate:google FROM=en TO=fr

    I do get the following error:

    Constant HTTParty from htt_party.rb not found

    The include “HTTParty” is described in the httparty readmes so I don’t understand the error here. Do I miss something obvious?

    Any help would be appreciated.

  20. Could this be used with I18N DB to give it a database backend? I deploy on heroku, which is a read only filesystem.

  21. Hi, thanks for this great plugin…and sorry for my english…

    I have a really weird problem, suddenly the plugin stop working right. Now when i press “save translations” button, and the plugin save the translations in the yml but skip the quotes (“).
    So my file looks like this.

    en:

    site:
    hello: Hello World

    instead of:

    en:

    site:
    hello: “Hello World”

    Any hint??

    Regards..

  22. Thanx fo that great tool! Just one warning for all using passenger or any another software managing multiple rails processes …

    You might encounter loosing translation entries when more than one application instance is running in parallel. Then the force_init_translations method in the translate_controller.rb is unsufficient, because it reloads only the translation entries of the instance you are currently working on. All other instances keep the translations they loaded on startup. When you you are later on dispatched by one of these, the previously entered translations are “lost” and missing forever (unless you added some backup functionality)

    To avoid such serious kind of problem, you have to either make sure to run your translation instance only with one process or to use something like

    FileUtils.touch(File.join(Rails.root, “tmp”, “restart.txt”))

    to force the update of all instances instead of using force_init_translations.

Comments are closed.