Sunday, July 20, 2008

Rails + Gettext Crash Course, Part 2

This is the second part of the first rails+gettext article. In this part we will take a look at some advanced points of using gettext and its bind to ruby/rails. Generally there is nothing "advanced", just say we move a little bit beyond elementary _('some text') calls.

Interface

Except the _(String) method, there are several more standard methods to define messages for gettext. Generally there are two parts of them, for singular and plural forms. The first ones take as the main argument your usual message string, the second ones two more arguments, singular form of the message, plural form of the message and an optional number which helps to determinate when it is a singular form and when it is a plural form.

Singular methods
  • String _(String) - the standard method as we now it. The method takes a string and returns its translated version.

  • String s_(String message[, String div="|"]) - this one for the cases when you need to define a scope for the message, say you have just the same message in two different context which might have different translation. So you write a string like s_('context|phrase'), your message will be splat by the divider ('|' by default) and if there is no translation the last part of it will be returned. Simple example s_('gender|sex') and s_('activity|sex'), by default both of them will return 'sex', but you may have two different translations of the word in different languages depend on the context.

  • String N_(String) - this one might look a little bit strange. It tells gettext to define in its base the given string as a message, but does not translate anything and returns the given string as is. Generally it does just nothing, it is a dummy method and just a marker for the gettext parser. You will be given some examples of its usage below.

Plural methods
  • n_(String msg, String msg_plural, Integer number) - this one is same as _(String) but for the plural cases. The first argument is a singular version of the message the second is the plural version of the message. And the last argument the current lets call it plurality marker by which gettext will define which message use. NOTE: despite that you pass here only two messages, when you translate your .po files you may define several (more than two) translations of the messages which will depend on the plurality marker.

  • ns_(String msg, String msg_plural, Integer number[, String div='|']) - same as s_(String[, div]) but for plural forms

  • Nn_(String msg, String msg_plural) - same as the N_(String) method but for plural forms

That's all about the message definition methods.

Models Internationalization

GetText by default has all the standard error messages translations for most of the languages inside the package. And the parser will automatically define translations for your model fields which are in the database (meant field-names translation). So you don't need to worry about that.

But sure you have got your special cases and you can follow the next instructions. Say the usual case, a user model where you have got some virtual fields

class User < ActiveRecord::Base
has_one :profile
has_many :comments

attr_accessor :password, :password_confirmation

# we can define the virtual fields translation like that
N_("User|Profile")
N_("User|Comments")
N_("User|Password")
N_("User|Password confirmation")

# we can create custom error-messages in this way
validates_uniqueness_of :login, :message => N_('The login-name is already taken')

def validate
errors.add("password", _("%{fn} is wrong!")) unless password == password_confirmation
end
end

As you see we have used the method N_(String) for class initialisation calls and the method _(String) inside the custom validation method. The point is simple, in the first case you don't really need the translation result, you just need to define some messages and the second case works in runtime so you need an already translated version of the message.

And another moment, we have used the 'User|' prefix in the field name definitions, this is to determinate in the translation which model the field name belongs, so you could have different translations of the field names for different models. By default it will be splat by '|' and the last token will be taken.

If you don't want to translate some particular field names, you may define to skip them by calling the untranslate(*names) method like that

class User < ActiveRecord::Base
untranslate :login, :email
# or
untranslate_all
end


Templates Internationalization

In most cases of templates internationalization you just use the standard gettext methods, like <%= _('Some label text') %>, but there is another way.

Sometime templates contain lots of text with big descriptions, so it is more comfortable to have another template file special for a particular language rather than put big pieces of text in the _(String) function. It is simple, just create another file next to existing one but with a name which have a suffix named after the locale you want. For example

app/views/pages/
about.html.erb
about_de_DE.html.erb
about_ru.html.erb

If the current locale is 'ru', then the about_ru.html.erb file will be used, if there is no matching file, the default about.html.erb file will be used.

Language Switching Tactics

And at the end some points about the language switching tactic. If you are experienced developer you probably do not need this part, but I would like to spell some words about it.

The best way here I think is to follow the general ideology, separate parameter variables and the urls formatting, just as you do with say same ID parameter. A good solution would be define a parameter name which will point to the desired language, say params[:lang], then as I showed in the previous article, you can use it in your application controller to switch the locale by a before filter.

class ApplicationController < ActionController::Base
init_gettext 'my_site'

before_filter :define_language

def define_language
case params[:lang]
when 'fr': set_locale 'fr_FR'
when 'ru': set_locale 'ru_RU'
else set_locale 'en_EN'
end
end
end

and then you left some options for youself how present it at your urls. You may use a simplest way like and left the gings as is

/articles/show/1 <- English
/articles/show/1?lang=fr <- French

or you can handle the language by routes so your pages were more caching friendly

map.connect ':controller/:action/:id'

map.connect ':lang/:controller/:action/:id',
:requirements => {
:lang => /(ru|de|fr)/
}, :lang => nil

----
/articles/show/1 <- English
/fr/articles/show/1 <- French

or later you may handle them by subdomains or something like that, whatever you choose your application code will not need changes.

--

Okay, think that is all what I had to say about using gettext with rails. Good luck!

1 comment:

valley said...

Hello Nikolay,
good post. But what i still do not understand is, how i will be able to show translations to the user with

validates_uniqueness_of :login, :message => N_('user.login.alreadytaken')

With that i just see the string
'user.login.alreadytaken' on the page when this error occurs, but not any translated sentence, which i defined in my .po files.
You said N(string) actually does nothing. Ok, but i need a way to show the custom translated strings (set with :message) when validations fail.

Is there a way to do so with gettext, or is there some more work to do ? i have gettext 1.93.0 and rails 2.1.1
Thank you!
Regards
valley