Internationalisation in the code

This page describes some technical aspects of internationalising the Alaveteli code. It's mostly aimed at devs who are working on the codebase — if you just want to translate Alaveteli into your own language, see translating Alaveteli instead.

Deployment notes

Deployed translations for the project live in locale/.

We encourage translations to be done on Transifex because translators can work through its web interface rather than needing to edit the .po and .pot files directly. Ultimately, Transifex just captures translators’ work and turns it into the files that Alaveteli needs (using gettext).

How to get the latest translations onto your site

For example, to deploy English and Spanish translations at once:

  • Ensure their .po files are at locale/en/app.po and locale/es/app.po (for example, by downloading them from Transifex)
  • Set AVAILABLE_LOCALES to en es

What to do if you don’t have complete translations for an older release of Alaveteli

Before a new release of Alaveteli is made, the translation files are pulled from Transifex and added to Alaveteli’s locale/ directory in github. These represent the most complete translations for the previous release. Then the files in Transifex are updated with any new strings that need to be translated for the upcoming release. At this point old strings that are no longer used in the new release are also removed. The latest release tag for a release in github should contain the most complete translations for that release from Transifex.

If you’re using an older release of Alaveteli and you want to add to or change the translations, you can edit the .po files directly using a local program such as PoEdit.

How to add new strings to the translations

You need to do this if you’ve added any new strings to the code that need translations (or if you change an existing one).

To update the .po or .pot files for each language, run:

bundle exec rake gettext:store_model_attributes

followed by:

bundle exec rake gettext:find

If gettext:find only creates the file locale/im-config.pot then you need to unset the TEXTDOMAIN environment variable and try again.

For more details about the translations, see the page about translating Alaveteli.

Technical implementation details

Getting the current locale

This is complicated by the fact that there are two competing ways to define a locale+territory combination. The POSIX (and gettext and Transifex) way is like en_GB; the Rails way is like en-US. Because we are using gettext and Transifex for translations, we must deal with both.

  • for the Rails version of the currently selected locale, use I18n.locale
  • for the POSIX version of the locale, use FastGettext.locale

I18n in templates

Before you add i18n strings to the source, you should read internationalisation guidelines that apply to all our projects.

Some hints for adding the strings into the Alaveteli code:

  • Simple strings: <% = _("String to translate") %>
  • Strings that include variables: give the translator a hand by inserting strings that can be interpolated, so the variable has meaning. For example, <%= "Nothing found for '" + h(@query) + "'" %> might become <%= _("Nothing found for ''", :search_terms => h(@query)) %>
  • Strings containing numbers: <%= n_('%d request', '%d requests', @quantity) % @quantity %>
  • We allow some inline HTML where it helps with meaningful context, for example:
_('<a href="">Browse all</a> or <a href="">ask us to add it</a>.',
   :browse_url => @browse_url, :add_url => @add_url)

Similar rules can apply to strings in the Ruby source code.

Programmatic access of translated PublicBodies

Apart from the templates, the only other area of i18n currently implemented is in the PublicBodies.

The implementation allows for getting different locales of a PublicBody like so:

    PublicBody.with_locale("es") do
      puts PublicBody.find(230).name
    end

Usually, that’s all the code you need to know about. There’s a method self.locale_from_params() available on all models which returns a locale specified as locale=xx in the query string, and which falls back to the default locale, that you can use in conjunction with the with_locale method above. All the joining on internal translation tables should usually be handled automagically – but there are some exceptions, that follow below.

Overriding model field setters

Internally, we use the Globalize plugin to localize model fields. Where column “foo” has been marked in the model as :translates, globalize overrides foo.baz = 12 to actually set the value in column baz of table foo_translations.

A side effect of the way it does this is that if you wish to override a specific attribute setter, you will need to explicitly call the Globalize machinery; something like:

    def name=(name)
        globalize.write(self.class.locale || I18n.locale, "name", name)
        self["name"] = short_name
        # your other stuff here
    end

Searching

The find_first_by_<attr> and find_all_by_<attr> magic methods should work. If you want to do a more programmatic search, you will need to join on the translation table. For example:

          query = "#{translated_attr_name(someattr) = ? AND #{translated_attr_name('locale')} IN (?)"
          locales = Globalize.fallbacks(locale || I18n.locale).map(&:to_s)
          find(
            :first,
            :joins => :translations,
            :conditions => [query, value, locales],
            :readonly => false
          )

You may also need to do some lower-level SQL joins or conditions. See PublicBodyController.list for an example of a query that has a condition that is explicitly locale-aware (look for the locale_condition variable)

Internationalised Sorting

Introduced in Alaveteli 0.23.0.0

Some support has been added to internationalize the sorting of records.

Currently this requires PostgreSQL >= 9.1.12.

The collations available depend on how your system is configured. You can view the available collations through the database console.

SELECT * FROM pg_collation;

If the locale you desire is not available you need to ensure the language pack is installed and add the collation to postgres.

# List the available language packs
$ sudo apt-cache search ^language-pack-

# Install the language pack
$ sudo apt-get install language-pack-nn

# The language should now appear in the available locales
$ locale -a

Once the language pack is installed you must add the collation to postgres.

# Create the "standard" named collations
# The value for LOCALE must be the same as the value listed in the
# locale -a command
CREATE COLLATION "nn_NO" (LOCALE = 'nn_NO.utf8');
CREATE COLLATION "nn_NO.utf8" (LOCALE = 'nn_NO.utf8');

# Create a collation that matches the locale name in Alaveteli's
# locale directory
CREATE COLLATION "nn" (LOCALE = 'nn_NO.utf8');

Now actions that support collation sorting will sort in the way expected by the current locale. If a collation isn’t available for the current locale, the sort will fall back to the default sorting order.

Translation and releases

The release manager will enforce a translation freeze just before a new release is cut. During such time, you must not introduce new strings to the code if your work is due for inclusion in this release. This is necessary to allow translators time to complete and check their translations against all the known strings. See more about translating Alaveteli.