Skip to content

Good Practices for Developers

Staś Małolepszy edited this page Apr 15, 2020 · 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:

// ❌ Avoid the imperative APIs.
let value = await l10n.formatValue("message-id");
element.textContent = value;

Instead prefer the declarative setAttributes API:

// ✅ Declarative APIs are 👌.
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 WET over DRY

WET stands for Write Everything Twice. It's the opposite of Don't Repeat Yourself.

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:

# ❌ Avoid splitting messages.
reaction-thumbs-up = a thumbs up
reaction-smiley-face = a smiley face
reacted-with = { $user_name } has reacted with { $reaction_type }.
// ❌ Retrieving a translation in order to pass it into another
// translation as an argument is a code smell.
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:

# ✅ Redundancy helps localizers understand each message in full.
reacted-with-thumbs-up = { $user_name } has reacted with a thumbs up.
reacted-with-smiley-face = { $user_name } has reacted with a smiley face.
// ✅ Let `l10n` handle setting the value of `element`. This also ensures
// that the element is re-translated when the user's language changes.
l10n.setAttributes(element, `reacted-with-${notification.type}`);

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

// ✅ You can now run `grep` to find references to these messages.
// This is super useful when the identifier needs to change or when
// you want to remove this message altogether.
let messages = {
    THUMBS_UP: "reacted-with-thumbs-up",
    SMILEY_FACE: "reacted-with-smiley-face",
};
l10n.setAttributes(element, messages[notification.type]);

Prefer separate messages over variants for UI logic

If the UI requires branching, use separate messages and choose the right one in the code. Only use select expressions and variants when the language grammar or style requires them. Do not use variants for the UI logic. Variants are private to localizations; some languages might choose to have different variants or to not implement them at all.

The rule of thumb is:

Use variants only if the default variant makes sense for all possible values of the selector.

Or, another way to phrase this could be:

When using variants, keep in mind that other languages may collapse them into a single variant due to their grammar.

The following example is OK because his, her and their are required by the English grammar. If there's a problem with the value of $other_user_gender, the default variant will result in an acceptable translation:

# ✅ The `his`, `her`, `their` variants are language-specific.
# The default variant works for all values of the selector.
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 UI and choosing one of them as the default variant doesn't make sense:

# ❌ The variants here are UI-specific.
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:

# ❌ The variants are UI-specific.
# Displaying the default variant in case of a problem with the selector
# may lead to data loss! 
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:

# ✅ Separate message mean that tools can easily verify that all locales
# have translated all possible cases.
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:

# ✅ All variants are language-specific. The [0] variant adds
# flavor to the translation. The default variant still works 
# for all values of the selector, even if "0 new notifications"
# sounds a bit off.
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.

# ❌ The [0] variant should be a separate message. It serves
# a different purpose in the UI than the 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:

# ✅ Separate messages which serve different purposes.
items-select = Select items
# ✅ The default variant works for all values of the selector.
items-selected =
    { $num ->
        [one] One item selected.
       *[other] { $num } items selected.
    }