Skip to content

Good Practices for Developers

Staś Małolepszy edited this page Oct 25, 2017 · 9 revisions

This document outlines the good practices for making the code localizable with Fluent.

Prefer declarative over imperative

Fluent implementations usually offer a declarative API. In fluent-dom avoid the imperative formatValue method:

let value = await l10n.formatValue("message-id");
element.textContent = value;

Instead prefer the declarative setAttributes API:

l10n.setAttributes(element, "message-id");

fluent-dom will make sure that the element is re-translated if the user's current language changes and will also properly handle any attributes defined in the translation. A safe subset of HTML markup can also be supported.

Usually it is convenient to make the code operate on l10n identifiers at all times and set them on UI elements rathen than retrieve a translation manually.

(There are valid use-cases for using formatValue: modal dialogs, push notifications, alert() etc. In these cases the translation is displayed as a one-off and doesn't persist in the UI so there's usually no need to re-translate it on language change.)

Prefer redundancy over being smart

It's tempting to abstract common parts of translations via parametrization. Resist the temptation: it obfuscates the translations and makes it harder for localizers to see the full context of their work. Instead prefer some redundancy by repeating the common parts of translations in multiple messages.

This is wrong:

reaction-thumbs-up = a thumbs up
reaction-smiley-face = a smiley face
reacted-with = { $user_name } has reacted with { $reaction_type }.
let reaction_type = notification.type === "THUMBS_UP"
    ? l10n.formatValue("reaction-thumbs-up")
    : l10n.formatValue("reaction-smiley-face");
l10n.setAttributes(element, "reacted-with", {reaction_type});

This is better because it avoids the imperative call to formatValue:

reacted-with-thumbs-up = { $user_name } has reacted with a thumbs up.
reacted-with-smiley-face = { $user_name } has reacted with a smiley face.
l10n.setAttributes(element, `reacted-with-${notification.type}`);

This is best because it improves the grep-ability of the code:

let messages = {
    THUMBS_UP: "reacted-with-thumbs-up",
    SMILEY_FACE: "reacted-with-smiley-face",
};
l10n.setAttributes(element, messages[notification.type]);

Prefer separate messages over select expressions for app logic

Only use select expressions when the language requires them; do not use select expressions if the app logic requires them. Select expressions and their variants are optional and private; some languages might choose to not implement variants at all.

This is OK because his, her and their are required by the English grammar:

shared-schedule =
    { $other_user_name } has shared { $other_user_gender ->
        [male] his
        [female] her
       *[other] their
    } schedule with you.

This is wrong because THUMBS_UP and SMILEY_FACE are required by the app and choosing one of them as the default variant doesn't make much sense:

reacted-with =
    { $user_name } has reacted with { $reaction_type ->
       *[THUMBS_UP] a thumbs up
        [SMILEY_FACE] a smiley face
    }.

This is also wrong because add and remove are required by the app:

item-action =
    Are you sure you want to { $action ->
       *[add] add
        [del] remove
    } this item?

The default variant (here: *[add]) should make at least some sense for all possible values of the selector ($action). In the snippet above showing the default variant in case of a problem with $action can lead to data loss.

Use separate messages to make sure all localizations have translations for all the cases required by the app. This is the fix:

item-add = Are you sure you want to add this item?
item-del = Are you sure you want to remove this item?

One more example. This is OK:

new-notifications =
    { $num ->
        [0] No new notifications.
        [one] New notification.
       *[other] { $num } new notifications.
    }

This is wrong because the 0 case serves a different purpose than the two other variants.

items-selected =
    { $num ->
        [0] Select items.
        [one] One item selected.
       *[other] { $num } items selected.
    }

Some locales might not even define the 0 case and by Fluent's design there's no way of detecting it because variants are private. This is the fix:

items-select = Select items
items-selected =
    { $num ->
        [one] One item selected.
       *[other] { $num } items selected.
    }