Update:Remora Localization

Localizing 101

Note: Expressions are converted to tags according to the standards mentioned at the end of the page (error_blah and such) so that multiple occurances of the same expression only have to be localized once.

Note 2: I assume you have the GNU gettext tools installed, otherwise the scripts won't work and, more importantly, the wrath of the merciless babelfish[TM] will come upon you.

L10n standards

  • If the string is a "widget" as defined in the shavictionary: (labels, common navigational elements like "Home" or "Top" or "Next")
    • element_name_additional
  • If the string is prose for explanations, error messages, instructions
    • type_name_additional
  • If the string is structural text like headers, titles, breadcrumbs, etc.
    • If the string is not in a form:
      • namespace_pagename_name_additional
    • If the string is inside of a form:
      • namespace_pagename_formname_element_name_additional

Where:

  • namespace is the location in cake of the view, so if it's under /views/developers/ the namespace is "developers"
  • pagename is the name of the file, with underscores taken out
  • formname is just what you think the form should be called with "form" appended, since cake is actually naming them... (should we make this more specific?)
  • name is a unique name for the element (preferably its id)
  • element is the closest tag or parameter, so for an images alt tag it would be "alt". For a label it would be "label"
  • additional is anything else needed to make a string unique
  • type is a global category, like "error"


Static Strings (PHP and gettext)

Use php's gettext functions to make localized strings, for example:

echo _('error_empty_glass');

To do string replacement, use sprintf like this:

echo sprintf(_('refill_something'), $glass, $beer);

Localizers can then translate it similar to:

The waiter pours some more %2$s into your %1$s.

and PHP will put in the value of $glass for %1$s and $beer for %2$s.

Note that we use ordinal parameters (%1$s) rather than simply %s which allows localizers to use a different order of the parameters (or drop some altogether).

Dynamic Strings

We need strings from the database to be localizable as well. This includes all english content in the remora code (Categories, etc.) but also the addons themselves (title, description, etc.)

The translations table is the hub of our localization. It was born out of the Pear::Translation2 specs, but we decided that the pear class wouldn't fit our needs. The translations table has 3 primary columns, and then all the additional comments are supported languages. The 3 main columns are:

  • `foreign_id` - The unique id of the row in the foreign table (that you are getting the translation for)
  • `foreign_table` - The name of the foreign table (as seen by cake!)
  • `foreign_column` - The name of the column in the foreign table that you want the translation for.

For example, if I wanted the name of second category in french, I could run:

 SELECT `fr` FROM `translations` WHERE `foreign_id`=2 AND `foreign_table`="Addontypes" AND `foreign_column`="name";

There are two methods in the Translation model (again, pulled from PEAR's Translation2): getOne() and getPage(). getOne() will retrieve a single row, and getPage() will retrieve an entire "page" - technically, it's actually retrieving all rows with a matching table name (foeign_table).

The table looks like this, for reference:

+----------------+------------------+------+-----+---------+-------+
| Field          | Type             | Null | Key | Default | Extra | 
+----------------+------------------+------+-----+---------+-------+
| foreign_id     | int(11) unsigned |      | PRI | 0       |       |       
| foreign_table  | varchar(50)      |      | PRI |         |       |       
| foreign_column | varchar(50)      |      | PRI |         |       |       
| en-US          | text             | YES  |     | NULL    |       |       
| en-GB          | text             | YES  |     | NULL    |       |       
| de             | text             | YES  |     | NULL    |       |       
...
...
| zh-CN          | text             | YES  |     | NULL    |       |
| zh-TW          | text             | YES  |     | NULL    |       |       
+----------------+------------------+------+-----+---------+-------+

Updating locales

extracting

After l10n strings have been added to the code files/views, they have to be extracted. There's a shell script that goes through the controllers, models and views (.php and .thtml files) and searches for gettext strings. The extracted strings are stored into ./messages.po .

Execute from the app dir:
./locale/extract-po.sh

merging

To bring the respective .po files of the individual locales up to date, execute from the app directory:

./locale/merge-po.sh messages.po ./locale
where messages.po is the file created by the extraction step and ./locale is the directory in which all the locales lie. The merge script will merge the new strings from messages.po into every *.po file underneath ./locale, then.

"compiling"

After translation, plain text .po files (PO = portable object) have to be translated into binary .mo files (MO = machine object). There's a third script you can run for that:

./locale/compile-mo.sh ./locale

This will make a .mo file in the same directory of every .po file.