i18n and l10n for Mustache (PHP) and Angular (Javascript)

English in fact uses just two forms of plural:

  • 0 children
  • 1 child
  • 2 children
  • 3 children

In Angular terminology it means ONE or OTHER1:

/**
 * Plural select rules for en locale
 *
 * @param {number} n  The count of items.
 * @return {goog.i18n.pluralRules.Keyword} Locale-specific plural value.
 * @private
 */
goog.i18n.pluralRules.enSelect_ = function(n) {
  if (n == 1) {
   return goog.i18n.pluralRules.Keyword.ONE;
  }
  return goog.i18n.pluralRules.Keyword.OTHER;
};

Use in templates

Mustache has no filter not tag to offer i18n or l10n. As it is solely a templating language. In the previous post I gave few thoughts about transcripting PHP / Mustache templates to Angular. I am about to continue here.

As I’ve used gettext in a custom project and in WordPress templates I have few observation comming from these areas:

  • WordPress uses plain English for the templates and only 2 forms of translations:
    • simple string: __()
    • of pluralised versions: _n()
  • Missing string translations appears as set in template (in English as it was written that way)
  • Defining pluralisation rules is cryptic

My first proposition for translations in Mustache was using opening ((( and closing ))). In most editors ( is auto-closed ) right away making it neat to write.

Translating a simple string, label for example could look like:

<label for="name">((( Type your name )))</label>

Pluralisation version is a little harder to write:

<footer>((( {{likes}} ? One like : {{likes}} likes : No likes )))</footer>

Of course this is not a valid mustache template and with a help of a custom loader this can get transcripted to:

<label for="name">{{__}}Type your name{{/__}}</label>

and

<footer>{{_n}} {"var": "likes", "singular": "One like", "plural": "{{likes}} likes", "zero": "No likes"} {{/_n}}

The {{__}} and {{_n}} must be registered as mustache helpers.

Mix in the Angular

In the template the use varies:

<ng-pluralize count="personCount"
               when="{'0': 'Nobody is viewing.',
                    'one': '1 person is viewing.',
                  'other': '{} people are viewing.'}">
</ng-pluralize>

As you can see, even the official documentation uses ZERO keyword. In other words, there might be scenarios where even English would use more than ONE or OTHER keywords for better user experience:

<span ng-pluralize count="personCount" offset=2
              when="{'0': 'Nobody is viewing.',
                     '1': '{{person1}} is viewing.',
                     '2': '{{person1}} and {{person2}} are viewing.',
                   'one': '{{person1}}, {{person2}} and one other person are viewing.',
                 'other': '{{person1}}, {{person2}} and {} other people are viewing.'
              }"
>
    <!-- Prerendered value gets replaced when Angular kicks in: -->
    John and Travolta and 7 other people are viewing
</span>

WordPress keeps it simple, as there are less edge cases and benefits of writing more readable templates working out-of-the-box for English are obvious. Since WordPress uses PHP for templates, there’s always an option to jump out and define special cases.

Since Angular sees it differently, any of the plural pattern keywords can be used within app:

/**
 * Plural pattern keyword
 * @enum {string}
 */
goog.i18n.pluralRules.Keyword = {
  ZERO: 'zero',
  ONE: 'one',
  TWO: 'two',
  FEW: 'few',
  MANY: 'many',
  OTHER: 'other'
};

Since my ((( translate me ))) proposal did not stick to mustache anyway, let’s think about merging the Angular and Mustache strategies.

Angular meets Mustache

Translating simple strings as labels and placeholder can be done using filters (Angular) and helpers (Mustache). They are not alike though:

Mustache helpers:

{{#registered_mustache_filter_lambda_helper}}
  string content passed as function argument
{{/registered_mustache_filter_lambda_helper}}

Angular filters (similar to Mustache pragmas):

{{ string content passed as function argument | angularFilter }}

But, Mustache templates can be rewritten using custom loader. Therefore the template can be written as {{ 'Translate me' | translate }} and rewritten as {{#translate}}Translate me{{/translate}} (or any other lambda like {{#__}}...{{/__}}).

Even though Angular has ng-bind directive and we could probably omit the curly braces at all in angular templates, there is however one exceptions: HTML element attribute values like:

<input type="text" placeholder="{{ 'Some value here...' | translate }}" />

I’ve found a gettext angular translations working close to my expectations with one exception: the ONE keyword being written inside of the tag like:

<div translate translate-n="countExpression" translate-plural="OTHERS">ONE</div>

Except you’ll end up with missing pre-rendered placeholder. I’am sure it can be modified to a more original Angular approach. But takeaway here is that it uses English as WordPress does, which is nice.

Translating as constants

(I’ve never done that but) have you ever tried to translate English to English?

It might not be as straightforward from the first time. Take for example CodeIgniter’s solution to translations:

$lang['language_key'] = "The actual message to be shown";

The language key is in most time something cryptic, resembling constants:

echo lang('language_key', 'form_item_id');
// becomes <label for="form_item_id">language_key</label>

You are therefore “forced” to define set also for English. Imagine defining other than ONE and OTHER rules. You could have created a totally different translations for TWO or FEW. By using plain english as the language key you can return itself on occasions when the translation is not explicitly defined (yet). The benefit: most situations require only ONE and OTHER versions, prototyping could be easier even with translations as there would be usually needed only the extra language dictionary.

Gettext has POEdit translations editor which has ability to can scan the PHP files, looking for __() or other defined functions to extract strings to translate. Angular way probably cannot be scanned automatically but, with a little help from caching to a PHP file it can be done (or using some Grunt task).

Final markup/anotation proposal

  1. Keep it simple: use two strings as WordPress does
  2. Other variations can be decided upon the pluralisation variable and looked up in the dictionary

Examples:

  • simple string tranlation: {{ 'Translate me' | translate }}
  • simple string translation in an attribute: <input placeholder="{{ 'Enter email' | translate }}" />
  • string pluralisation: {{ people ? 'One person likes it' : '{} people like it' | translate }}
  • string pluralisation in an attribute: <input name="override_num_of_downloads" placeholder="{{ downloads ? '1 download' : '{} downloads' | translate }}" />

This way we can overcome the ‘pluralisation cannot be used in attributes’ problem and use the same {{ count ? one : other | translate }} or {{ ‘string’ | translate }}` within element’s content.

Angular uses other arguments for pluralisation:

  • count – variable to watch for picking string version; defined before ? sign, e.g. as people: {{ people ? 'One person' : '{} people' | translate }} meaning: evaluate people, if people == 1, use 'One person', or else '{} people' where {} gets replaced with the current value
  • offset – attribute allows further customization of pluralized text, which can result in a better user experience. For example, instead of the message “4 people are viewing this document”, you might display “John, Kate and 2 others are viewing this document”
  • when – omitting this angular is done on purpose. When building app in English, by using th proposed translation form already defines ONE and OTHER which is what is needed in most of the cases. There is no rule that English must not be translated further. Passing translation:
{
 '0': 'Nobody is viewing.',
 'one': '1 person is viewing.',
 'other': '{} people are viewing.'
}

and evaluating people as 0 and with lookup for translation of {} people would result in returning ‘Nobody is viewing’.

Rewriting of proposal for Mustache

It is a sad fact, that Mustache cannot be extended easily to a developers needs (which is also a good thing as it forces developer to think outside of the box). As I mentioned earlier, there is an option to rewrite any ‘shorthand’ markup using custom Mustache loader class.

Wich effectively can turn

{{ people ? 'One person' : '{} people' | translate }}

into

{{#__}}{"var": "people", "one": "One person", "other": "{{people}} people"}{{/__}}

(I use JSON string to encode arguments as lamdas are invoked with ONE string argument.)

Rewriting for Mustache/Angular combo

The Mustache template loader could even be set to rewrite:

{{ people ? 'One person' : '{} people' | translate }}

to:

<ng-pluralize count="people" when="{ '0': 'One person', 'other': '{} people'}">
  {{people}} people <!-- gets pretendered by Mustache as a placeholder -->
</ng-pluralize>

and rendered as:

<ng-pluralize count="people" when="{ '0': 'One person', 'other': '{} people'}">
  3 people <!-- gets pretendered by Mustache as a placeholder -->
</ng-pluralize>

Note: Use of ng-pluralize could be possible only if it was rewriten for this behaviour. Maybe namespacing it to something else would make sense.

Or as a custom directive (no prerendered placeholders):

<ul ng-repeat-start="event in events">
    <li namespace-translate-template="
        {{ event.name }}: 
        {{ item.people ?
             'One person is attending'
           : '{} people are attending'
        }}"
    ></li>
</ul>

Or more natural tempting approach using filter:

<ul ng-repeat-start="event in events">
    <li>
        {{ event.name }}: 
        {{ item.people ?
             'One person is attending'
           : '{} people are attending'
        | translate }}
    </li>
</ul>

In conclusion with aswers to how to prefil template server-side:

<!-- Mustache templete to fill data serverside: -->
<ul ng-hide="true">
    {{#events}}
    <li>
        {{ event.name }}:
        {{#__}}
            { /* JSON data to pass to lambda helper */ }
        {{/__}}
    </li>
    {{/events}}
</ul>
<!--
Angular template to be used after load

Could be loaded using AJAX keeping the resulting code
more semantic and without this duplicity.
-->
<ul ng-cloak ng-repeat="event in events">
    <li>
        {{ event.name }}:
        {{ item.people ?
             'One person is attending'
           : '{} people are attending'
        | translate }}
    </li>
</ul>

Extra: Offsets:

Since we are not tied to any schema, offsets could be set like this:

{{ people + 2 ? 'One person' : '{} people' | translate }}

or more Angular filters way:

{{ people ? 'One person' : '{} people' | translate:2 }}

Where are the options for rendering 'John, Kate and 2 other people'? They must be defined as English translations of {} people when people evaluate as OTHER pattern keyword.

Next: Code it

Part 1: Transcription to be used with Mustache (done)

  1. Added expandTranslationMarkup() method in template loader class for Mustache
  2. Added predefined __ and _n lambdas in Mustache preprocessor class

Part 2: Directive / Filter in Angular (todo)

If you feel like ‘Hey I like the idea, I can help’ please do contact me:

I’ve already started coding the PHP server-side Mustache Loader with some atomic webdesign extras.

\#workinprogressahead


  • summ3r

    An reliable online alternative for translating software strings is https://poeditor.com/
    Collaborative, fast and accessible even for the tech-unsavvy.

  • Thanks, I know about the Poedit. Though, don’t get me wrong, but I know about better options than your suggested online service which is “unnecessary”.

    You can download the Poedit program for free. Sending the file over email is pretty simple.

    If you have a WordPress site, you should definitely check the CodeStylink Localization plugin which provides just the same online collaboration service. For free.

    Poedit is built around the gettext() and therefor is good to simple translations. Pluralisation works, but setting is cumbersome. Special cases need to be handled (like 1/4 of potato) even in English additionally.

    I’ve written my own little YAML database able to support advanced translations lookup as Angular.js. The traslations are added to the simple text file on the fly, collaborative editing can be done by sharing using Google Docs or Dropbox.

    But as always, different projects have different needs.