L20n/Toolbox

From MozillaWiki
Jump to: navigation, search

Introduction

L20n is a localization format and an API that is aiming at remodeling the way developers internationalize their code and how localization is being done. This document runs down the items of the toolbox that l20n offers to localizers and programmers. Simple examples that build on each other make up the first part, and the second part will show how those tools are put to use to implement some patterns that are hard in current l10n infrastructures.

The examples will be composed by the localized files, and an example API pseudo code that shows how much of the l20n logic the programmer sees.

You can see the annotated grammar of the localized files for reference (warning, WIP).

Toolbox

Starting off with the simplest "just strings" example, let's build our toolbox one by one.

Simple example

In its simplest form, L20n code/format may look like this:

<newFile "New File">
<close "Close">
// Get a new context
let ctx = l20n.getContext();
// add a resource to the context
ctx.addReference("resource://path")

// pool for entities
let msg = ctx.get("newFile")
let msg2 = ctx.get("close")

Parameters

L20n accepts two levels of parameter passing:

  • per call
  • per context

Per call parameter takes precedence.

<luckyNum "Your lucky number is: {{num}}">
<signedIn "You're signed in as {{login}}">
// Get a new context
let ctx = l20n.getContext();
// add a resource to the context
ctx.addReference("resource://path")

// add a per-context parameter
ctx.data = {'login': 'Gandalf'}

// pool for entities
let msg = ctx.get("luckyNum", {'num': 7})
let msg2 = ctx.get("signedIn")

Veriable referencing

L20n allows for variable referencing from inside of an entity:

<extName "AdBlock">
<installExt "Install {{extName}}">
// Get a new context
let ctx = l20n.getContext();
// add a resource to the context
ctx.addReference("resource://path")

// pool for entities
let msg2 = ctx.get("installExt")

Entity values

L20n entities may have one of a three types of values:

  • a string
  • a list
  • a dict
<extName "AdBlock">
<drinks ["Coca Cola", "Gatorade", "Water"]>
<cookie {'one': "Cookie", 'many': "Cookies"}>

<install "Install {{extName}}">
<buy "Buy {{drinks[0]}}">
<iWant "I want a {{cookie['one']}}">
let msg = ctx.get("install") // Install AdBlock
let msg2 = ctx.get("buy") // Buy Coca cola
let msg3 = ctx.get("iWant'); // I want a Cookie

Entity arguments

Entity can also take other entities or parameters passed into the context or directly in the call as arguments in order to resolve complex entity and pick the right value:

<drinks[num] ["Coca Cola", "Gatorade", "Water"]>
<cookie[form] {one: "Cookie", many: "Cookies"}>
let msg2 = ctx.get("drinks", {num: 0}) // Coca cola
let msg3 = ctx.get("cookie", {form: 'many'}); // Cookies

One can also chain the arguments:

<drinks[type, num] {
    cup: {one: "Cup", many: "Cups"},
    pot: {one: "Pot", many: "Pots"}
}>
let msg2 = ctx.get("drinks", {type: "pot", num: "many"}) // Pots

Entity properties

In L20n context, an entity is an object that may contain a value and a list of key-value pairs of properties.

This allows developers to bundle all localization messages related to a single UI unit and it allows developers and localizers to provide additional information on the entity.

<extName "AdBlock"
 gender: "male"
 declension: "nominative">

<buttonClick "Click me"
 info: "{{buttonClick.title}}"
 title: "In order to press the button use ctrl+{{buttonClick.accesskey}}"
 accesskey: "c">

JS:

let msg = ctx.getAttributes("buttonClick").info


XML:

<button l10n_id="buttonClick"/>

An entity may reference properties of other entities:

<buttonClick "Click me"
 tooltip: "If you click me, I'll give you a kiss"
 accesskey: "c"
 label: "Press this button:">

<buttonHelp: "In order to press the button use ctrl+{{buttonClick.accesskey}}">

Per-locale customizations

L20n allows localizers to customize their entities and add new ones in the scope of their own locale. The scope of customizations is as follows:

  • Locale may have its own entities that are referred from "global" entities
  • Locale may have custom entity properties like "gender" on an entity.

Local entities and properties must not be used outside of the context and should not be called from by the API.

en-US:

<brandName "GMail">

<loadedInfo "Your connection to {{brandName}} has been dropped">

pl:

<brandName {
  nominative: "GMail",
  genitive: GMaila",
  dative: "Gmailowi",
  accusative: "GMaila",
  instrumental: "GMailem",
  vocative: "GMailu"
}>

<loadedInfo "Rozłączyłeś się z {{brandName['instrumental']}}">

Expressions

L20n grammar allows for expressions that represent a subset of common programming language expressions. What's possible is:

  • or (||)
  • and (&&)
  • equality (==, !=)
  • relational (<, >, <=, >=)
  • additive (+, -)
  • multiplicative (*, /, %)
  • conditional (x?y:z)
  • unary (+, -, !)

This allows for limited logic inside localization files that may be required for picking the right element depending on the variables:

<drinks[num==1?'one':'many'] {one: "Cup", many: "Cups"}>
let msg = ctx.get("drinks", {num: 5}) // Cups

This may be preliminary used for plural formulas, but it may also help localizers picking the right entity basing on any conditions.

Macros

Most popular expressions may be wrapped into a separate type of object in L20n format - macros and used in the whole context for which they're available.

warning: Marcos differ from entities in the way they use attributes - instead of selecting a particular value from hash or list values, it becomes a local variable used by the macro.

en-US:

<plural(n) {n==1?'one':'many'}>

<drinks[plural(num)] {one: "Cup", many: "Cups"}>
let msg = ctx.get("drinks", {num: 5}) // Cups

Use cases

Now that our toolbox is complete, let's use them to attack some real life examples. This section goes by linguistic use cases rather than the concrete technical tools used to attack them.

Plurals and Genders

Below is an example of a macro that may be used by many slavic locales where the number and type of gender forms is different depending on the plural form.

en-US:

/* plural form - one, few, many */
<plural(n) {n==1?'one':'many'}>

<someone[who] {
  masculine: "He",
  femine: "She",
  neuter: "It,
}>

<someoneWalks[plural(num)] {
  one: "{{someone}} walks",
  many: "They walk"
}>
let msg = ctx.get("someoneWalks", {num: 5, who: "femine"}) // They walk

pl:

/* plural form - one, few, many */
<plural(n) {n%10==1&&n%100!=11?'one':n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?'few':'many'}>

/* gender plural form - one or many */
<gplural(n)->{n==1?'one':'many'}>

/*
 * input:
 * n - number
 * g - gender:
 *   masculine
 *   femine
 *   neuter
 */
<gender(n, g) {gplural(n)=='one' ? g :
              (g=='masculine'?'masculine':'non_masculine')}>

<someoneWalks[gplural(num), gender(num, who)] {
  one: {
    masculine: "On idzie",
    femine: "Ona idzie",
    neuter: "Ono idzie"
  },
  many: {
    masculine: "Oni idą",
    non_masculine: "One idą"
  }
}>
let msg = ctx.get("someoneWalks", {num: 5, who: "femine"}) // One idą

Gender probablisitics

This is an extreme case, but let's assume that your app is building a sentence that uses a name provided into the box.

en-US:

<called "{{name}} called">
let msg = ctx.get("called", {name: "Anna"})

That's simple in english, and actually quite popular, but it's impossible in other languages where the form of the sentance depends on the gender of name1.

If the developer has access to the gender of the person, he may provide it to the localizer who can then use it to make the sentence sound right. But if he does not have the access, then, well, the current way localizers approach this is something like:

pl:

/* User Anna called */
<likes "Dzwonił użytkownik {{name}}">

This way it becomes possible to adjust the word "user", and it sounds less wrong.

But in L20n, it's becoming possible to be smarter about it:

pl:

/* most popular names */
<nameList {
  'Anna': 'female',
  'Patrick': 'male',
  'Maradona': 'male'
}>

<likes[nameList[name].gender||"unknown"] {
    male:"Dzwonił {{name}}",
    female:"Dzwoniła {{name}}",
    unknown: "Dzwonił użytkownik {{name}}"
}>

This doesn't solve all cases, but it may solve majority of cases with a fallback which means that in the worst case, it will be similar to what localizers can do with today technologies. But for the most popular names, cities, countries, holidays, colors it becomes possible to make it look right.

Another example would be a name of the city.

en-US:

<called "You will fly to {{city}}">

Well, in many languages slavic languages we decline the name of the city if you localize it and it is culture dependent.

pl:


<cities {
  "New York": {
    nominative: "Nowy Jork",
    genitive": "Nowego Jorku"
  },
  "Berlin": {
    genitive": "Berlina"
  },
  "Puerto Rico": "Portoryko"
}>

<declenseCity(name, form) cities[name][form]||cities[name]||name>

<called "Polecisz do {{declenseCity(city, 'genitive')}}">

Exceptions

In an email to ICU-design Doug Felt proposed an extension to ICU message formatting to solve a case where you'd want to add exceptions to plural forms.

Here comes a translation of this to L20n:

<plural(n) {n==1?'one':'other'}>

<wroteFiles[n<3?n:plural(n)-2] {
  1: "Wrote {{file_1}}.",
  2: "Wrote {{file_1}} and {{file_2}}.",
  one: "Wrote {{file_1}}, {{file_2}} and one other file.",
  other: "Wrote {{file_1}}, {{file_2}}, and {{n-2}} other files."
}>
let msg = ctx.get("wroteFiles", {n: 5, file_1: "path", file_2: "path"})

Spark campaign example

A nice, complex, entity we found while working on a social mobile game "Spark".

The string should look like this:

 FoxyBarbarian shared to 5 people in 23 different countries around the world.
 And they unlocked 10 cool badges in the process.

 How far will your Spark go?

In gettext it looks more or less like this:

<p class="section sans">
  <span>{{ _('{username}')|f(username='FoxyBarbarian') }}</span>
    
  {{ ngettext('shared to <span>{n} person</span>',
              'shared to <span>{n} people</span>', num_people)|fe(n=num_people) }}
  {{ ngettext('in <span>{n} country</span> around the world.',
              'in <span>{n} different countries</span> around the world.',
              num_countries)|fe(n=num_countries) }}
  {{ ngettext('And, they unlocked <span>{n} cool badge</span> in the process.',
              'And, they unlocked <span>{n} cool badges</span> in the process.',
              num_badges)|fe(n=num_badges) }}

  {{ _('How far will your Spark go?') }}
</p>

In L20n it could like this:

lol:

<people[plural(num_people)] {
  'one': "one person",
  'many': "{{num_people}} people"
}>

<countries[plural(num_countries)] {
  'one': "one country",
  'many': "{{num_countries}} different countries"
}>

<badges[plural(num_badges)] {
  'one': "one cool badge",
  'many': "{{num_badges}} cool badges"
}>

<userShared "<span>{{username}}</span> shared to <span>{{people}}</span> in <span
{{countries}}</span> around the world. And, they unlocked <span>{{badges}}</span> in
the process. How far will your Spark go?">

template:

{{ get_value('userShared', {'num_people': num_people, 'num_countries': num_countries,
'num_badges': num_badges}) }}

Reference links

Next: L20n examples