Tatoeba/tatoeba2 — pull request #517 by jiru (2014-11-29 10:18:27+00:00)
Title: Better i18n

I haven’t yet tackled down #152 but this branch is getting rather heavy. Since it breaks many many translatable strings, I though it may be a good time to merge it on dev so that people have a fair amount of time to retranslate the UI, up to the next release and using dev.tatoeba.org.

This pull request solves #404, #434, add gettext’s msgctxt support, and does a big spring cleaning on translatable strings in order to ease translators’life.

## New string extraction tool: contextualized strings, translators comments
I removed the tatoi18n shell and created an xgettext-based equivalent bash script, with some more features, such as contextualized strings. So far, I added only one contextualized string: “Name” for lists and “Name” for users. Note that xgettext produces file references that are often one line off to the string, that’s because it sticks to the line in which the function name ( `__()`) appears, not the string itself. Not at big deal. Note that contextualized strings only work with .mo files, not .po (thank you CakePHP).

Another cool feature is comments that are extracted from the source code and attached to a string in the pot file. This way, you can tell specific things to translators about a particular string. To get your comment into the pot file, put it right before the call to __() and include @translators into it.

## Pluralized strings (#434)
Pluralized strings are rather simple to use, but you want to be careful when translating. I happened to write things like __n('One sentence', '%s sentences', $count). This has the following result:
* In English
* $count == 0 → %s sentences
* $count == 1 → One sentence
* $count > 1 → %s sentence

If you translate this to “Une phrase” and “%s phrases” in French, you end up with:
* In French
* $count == 0 → Une phrase
* $count == 1 → Une phrase
* $count > 1 → %s phrases

So you really need to translate it to "%s phrase" and "%s phrases" instead. This is because French has a different plural formula than English. English is plural if $count != 1, but French is plural when $count > 1. Brazilian and French are the only languages with such “weird” plural formula. If you’re programatically sure that $count can never be zero, you can theorically translate it to "une phrase"/"%s phrases" but I think it’s too dangerous. This would break if the code changes.

## Declensions (#404)
The new printf-like function `format()`, inspired by its Python equivalent, is a key feature that allows declensions and prettier strings for translators. Have a look at its tests in `app/tests/cases/bootstrap.test.php` for detailed example usages. Its key features are:
* self-explanatory parameters in the format string using {0}, {1} or {anyKeyword} instead of printf’s %s or %1$s
* sublisted parameters using the {p.subkey} syntax. Sublisted parameters expect a string formatted as `; subkey: value; subkey2: value2; ...` for the `p` parameter (read on). A parameter is interpreted as a sublist if it starts with `;`.

The sublist thing allows translators to freely create declension patterns when a string includes another. For instance, "Sentences in %s not translated in %s" is problematic because the %s are themselves translated strings. Let’s say we want to translate that sentence into the top-natural French "Phrases françaises non traduites en japonais". Here is how to accomplish that:

0. Regenerate the pot file and merge it with the French po. Remove the french default.mo so that CakePHP uses the po file instead.
1. Edit the translation of the string "French" in the French po file.
2. Instead of the current "français", you put a format()-compliant sublist, in which subkeys represent names of the declensions, which can be anything you want: `; masc-sing: français; fém-plur: françaises`
3. Similarily, you edit the translation for "Japanese" to: `; masc-sing: japonais; fém-plur: japonaises`
4. Then, edit the string that contains these two strings, which actually happens to be `Sentences in {sentLanguage} not translated into {targLanguage}`. Translate it to `Phrases {sentLanguage.fém-plur} non traduites en {targLanguage.masc-sing}`, and you’re done.
5. Now it works all right, and you should even get “français” on language selection lists. This is because `format()` fallbacks on the first item of the sublist (masc-sing here) when it doesn’t find the key you specified (if you use `{sentLanguage.blahblahblah}`) or you didn’t specified any (just `{sentLanguage}`).
6. Now I want “Français” title-cased in language lists!
7. Since language selection lists are not strings that gets inserted into other strings, a little trick is necessary. I added a special string "{language}" that translators can use to decline language names that appear alone on the UI. First, you need to add a new declension to "French" and "Japanese", let’s call it "seul". You end up with `; masc-sing: français; fém-plur: françaises; seul: Français`).
8. Edit the translation of the special string to "{language.seul}"
9. You should get it on language selection list, flag mouseover and stats page.
10. Of course, you could also had put the "seul: Français" declension at the beginning of the list and it would had worked without even modifying the translation of the "{language}" string.

## ~~Spring~~ Autumn cleaning
I merged sentences that where splitted into many separate echo’s so that translators can better translate them. I switched all the printf’ed strings to format() for strings that included two or more parameters, and added meaningful names for the parameters, such as "Translate {language} sentences that belong to {user}". I also switched to format() some strings that I thought a bit difficult to understand. I reduced as much as possible the HTML markup inside translatable strings (it now only has some ＜a href="%s"＞). I factorized some code around, fixed various inconsistencies or other bugs into the strings. I added some __() around strings that are automatically translated by CakePHP, so that they end up into the pot file (“Name” for list was one of them).