Staś Małolepszy

Media queries for L20n with globals

With globals in L20n, localizers will be able to adjust the translations to the runtime conditions, such as the sreen width and the current time.

In my last post I talked about changes to the l20n syntax and the way we reference entities (foo) as opposed to context variables ($foo). We decided to use these changes as an opportunity to introduce another data type system-defined globals (@foo).

Globals will provide information about the runtime environment to the localizers and will allow them to tailor the translations to the conditions under which the translation is shown to the user.

While we haven't settled on the final list of globals yet, the list of candidates includes the following variables:

  • @os -- the current operating system; the values might be win, mac and linux; useful in case of messages that refer to the keyboard (Control key vs. Command key) or when using OS-specific vocabulary (Preferences vs. Settings vs. Options),
  • @screenWidth -- the width, in pixels, of the screen; we might want to dive deeper with device width, viewport width and resolution (in DPI); helpful when you want to have two versions of the interface and be smart about limited space in the UI,
  • @time or @hour -- the local time for cases when you want to address the user according to the time of the day.

Let's have a look at these globals in action, starting with the last one: @hour.

Current time: @hour

With @hour the localizers will know what time it is when the translation is shown to the user. It will be interesting to see in what ways the localizers will decide to use this global. Here's one possibility:

    <timeOfDay() { @hour <= 12 ? "morning" : @hour <= 18 ? "afternoon" : "evening" }>

    <greeting[timeOfDay()] {
      morning: "Good morning, {{ $user.firstname }}!",
      afternoon: "Good afternoon, {{ $user.firstname }}!",
      evening: "Good evening, {{ $user.firstname }}!"

Operating system: @os

The @os global will store the information about the user's operating system or, more generally speaking, the platform. For many locales, different platforms have different translation guidelines, and knowing which platform the user is on proves very useful.

    <syncConnect[@os] {
        win: "Select “Pair a device” in your desktop Firefox options.",
        mac: "Select “Pair a device” in your desktop Firefox preferences.",
        linux: "Select “Pair a device” in your desktop Firefox preferences."

The mac and linux keys are redundant in this example. In order to improve this code we may choose to use a macro.

    <areWeOnWindows() { @os == "win" ? "win" : "other" }>
    <syncConnect[areWeOnWindows()] {
        win: "Select “Pair a device” in your desktop Firefox options.",
        other: "Select “Pair a device” in your desktop Firefox preferences."

Another solution would be to define a macro which selects the right word for us, and insert that word into the final string. Note how in the snippet below, @os is passed to the macro from the syncConnect entity. It is then available in the macro as its first argument, $os.

    <settings($os) { $os == "win" ? "options" : "preferences" }>
    <syncConnect "Select “Pair a device” in your desktop Firefox {{ settings(@os) }}.">

This way, if you really wanted to, you could force a value from the macro independently of the actual runtime, like so:

    <settings($os) { $os == "win" ? "options" : "preferences" }>
    <helpText "On Windows, open the {{ settings('win') }} dialog.">

Screen width: @screenWidth

The typical usecase for this global would be when the screen estate is scarce and the translation needs to be short. On the other hand, if the same application is running on a bigger screen, there is no need to sacrifice the length and the quality of the translation; there's more space available to fit the translation comfortably.

In the end, the localizer finds herself wanting to have two versions of the translation. Enter the @screenWidth global.

    <doNotTrack[@screenWidth <= 480 ? "short" : "long"] {
      short: "Tell sites not to track me",
      long: "Tell websites I do not want to be tracked."

For the sake of consistency and the ease of reuse, we can put the expression from the index into a macro, and then call the macro whenever we need to.

    <UISize() { @screenWidth <= 480 ? "short" : "long" }>

    <doNotTrack[UISize()] {
      short: "Tell sites not to track me",
      long: "Tell websites I do not want to be tracked."

    <topSites[UISize()] {
      short: "Top Sites",
      long: "Most Frequently Visited Websites"

Conditional blocks: next milestone?

Looking at the example above, it might be tempting to ask for the ability to change the grouping of entities. Instead of grouping them under the identifiers, why not group them under the conditional expression?

In other words, why not group by columns, instead of rows, in the table below?

short long
doNotTrack Tell sites not to track me Tell websites I do not want to be tracked
topSites Top Sites Most Frequently Visited Sites

Here's how conditional blocks could look like:

    // pseudocode; this code won't work in L20n 1.0

    if (@sreenWidth <= 480) {
      <doNotTrack "Tell sites not to track me">
      <topSites "Top Sites">
    } else {
      <doNotTrack "Tell websites I do not want to be tracked.">
      <topSites "Most Frequently Visited Websites">

We've spent a lot time considering this idea and brainstorming all the pros and cons. In the end we decided to leave this feature out for the 1.0 version of L20n. Here's why.

The good parts:

  • Reduced syntactic overhead related to defining indexes on entities and having hashes as entities' values.
  • The if-else construct opens up a lot of possibilities for other interesting conditional expressions, with entities logically grouped together under a common scenario.

Testing the value of @locale would allow having multilocale l20n files. Testing for @region would allow localizers to override translations for a particular region and group these region-specific messages in one place or one file. And so on.

Sounds pretty awesome? It sure does, but keep in mind that it's already possible to do all that by using indexes. And before we can introduce conditional blocks, we'd need to find satisfying answers to a couple of questions.

The challenges:

  • An important feature of L20n is that an entity is defined in a single place, with all of its variants. This makes it significantly easier to understand entities as objects, and makes l20n files easier to comprehend.

    If we allowed conditional blocks, some entites would end up being defined in multiple places. Combine that with file imports and in some extereme cases, an entity might be defined simultaneously in two or more different files! Each of these definitions would come into effect under different conditions, effectively creating a cascade of overrides. How does the localizer know where the entity is defined?
    What if none of the conditions is met? Which definition is the canonical one? Should we require the localizer to make one of the definitions the default one?

  • Related to the above point, a question about the localization toolchain rises. We want L20n to come with a well thought-through workflow, together with tools which let localizers see the status of their localization, diff between their version and the source locale, and update the localization files with new entities (as well as remove the obsolete ones).

    One of the most important features of the l20n toolchan is a so-called completeness check. How far am I in my localization work? Is it 10%? 50%? 95%? A completeness check verifies that the list of entities in the source locale (the one you're translating from) is the same as the list of entities in the target locale (the one you're translating into).

    You've probably spotted the problem here already. If an entity is defined in a condition block, what should the completeness check report about it? Should the update tool add a new entity to all conditional blocks and let the localizer decide which block it really belongs to? Same thing goes for removing obsolete entities.

While there exist feasible solutions to the above problems, we feel that they would overly complicate the syntax and the workflow for the initial release of L20n. The same functionality is possible with indexes (like in the @screenWidth example above) and we want to see how the localizers use it first. We can then have a better insight into the usefulness of conditional blocks and understand whether or not the localizers would really benefit from them.

While we are not ruling conditional block out of L20n, we feel strongly that they don't belong in the 1.0 release.


The discussion takes place in the l20n newsgroup.
Please post your thoughts and questions there.

Published on 30.04.2012

Staś Małolepszy

Thoughts about the Internet, the information society, Mozilla and human-computer interactions.

Latest notes