#localization Region

Purpose

The #localization region holds the translations of the strings marked with @t("...") in #code and #view. The compiler checks that every @t has an entry and builds the translations into the program; at run time @t shows the translation for the active locale, chosen by the locale module.

Marking strings

tape
#code
import io from "io";

fn greet(name: []const u8, unread: i64) {
    io.println(@t("Hello, welcome back!"));
    io.println(@t("{name}, you have {unread} new messages"));
}

#view
component LoginForm {
    view {
        widgets.Label { text: @t("Username"); }
        widgets.Button { label: @t("Sign in"); }
    }
}

@t takes a string literal (or a key, see Keyed translations below). Its placeholders are names of variables, parameters or props in scope, written as {name}; their values are written the way string interpolation writes them (numbers in decimal, enums by their description, and so on). A placeholder can’t be an expression: bind {a + b} to a name first, so translators only ever see names. A string has at most 8 different placeholders.

@t(...) is a string, like an interpolated literal, so it works anywhere a string does. Strings without @t are never translated.

Translation entries

tape
#localization
"Hello, welcome back!" {
    fi: "Hei, tervetuloa takaisin!";
    sv: "Hej, välkommen tillbaka!";
    de: "Hallo, willkommen zurück!";
}

"{name}, you have {unread} new messages" {
    fi: "{name}, sinulla on {unread} uutta viestiä";
    de: "{unread} neue Nachrichten für {name}";
}

"Username" {
    fi: "Käyttäjänimi";
    de: "Benutzername";
}

"Sign in" {
    fi: "Kirjaudu sisään";
}

Each entry is the @t string followed by its translations, one per language: code: "text";. Language codes are identifiers such as fi, de, pt_BR; use whatever your project uses. A translation may put the placeholders in a different order and repeat them, but it must use exactly the placeholders of the source string. An entry doesn’t need every language.

The region belongs to its file: each module’s @t strings are translated by that module’s own #localization region.

Compile-time verification

ProblemResult
@t("...") with no entryerror
a translation that uses another placeholder, or leaves one outerror
the same source string twice, or two translations into one languageerror
a placeholder that isn’t a name, or more than 8error
an unknown name in a placeholdererror (as in interpolation)
an entry no @t useswarning
@t(key) of a key no region declareserror
@t(key, ...) with more or fewer arguments than its source text’s highest placeholder, or a non-integer for a %d or a counterror
a key’s text that uses other placeholders than its source text, or %d where the source has %serror
a key without a source-language text, a key given twice for one language (across all the files and entries), a language the region doesn’t declareerror
a missing {lang} file; a value that isn’t a string or an object; a malformed file (reported at its line and column)error
a key written in the region that no @t useswarning (an imported key is never one)

Keyed translations

When the texts are shared with other clients (an iOS and an Android app reading the same catalogue), name them by key instead of by their English text. The region declares its source language and the others, imports the catalogue’s JSON files at compile time and may add keys of its own; @t takes a key and the values of its placeholders:

tape
#view
component Continue {
    view {
        widgets.Button { label: @t(onboarding.continue); }
    }
}

#code
import io from "io";

fn show(name: []const u8, days: i64) {
    io.println(@t(chat.joined, name));                       // "%1$s joined"
    io.println(@t(backup_detail_disable_window_days, days)); // plural, by `days`
    io.println(@t(onboarding.title));
}

#localization

source en;                 // the language every key must have
languages fi, sv, de;      // the others

import "translations/{lang}.json";                // one file per language
import "translations/_platform_desktop.json";     // one file, top level by language
import plurals "translations/_plurals.json";      // key → language → plural category

onboarding.title {         // a key written here
    en: "Welcome to Sulle";
    fi: "Tervetuloa Sulleen";
}

backup.days {              // a plural key written here
    en: { one: "{1} day"; other: "{1} days"; };
    fi: { other: "{1} päivää"; };
}
  • source <code>; and languages <code>, ...; come before the entries. A text in a language the region doesn’t name is an error, so a typo in a file name or a code shows at compile time.
  • Paths are relative to the .tape file. {lang} reads one file per language (the source’s too), each an object of key → text; a missing file is an error. A file without {lang} has languages at its top level. A plurals file maps key → language → forms (zero, one, two, few, many, other; every language’s forms need other). The compiler reads the files at every build: they aren’t shipped.
  • A key is a dotted path of identifiers; keywords are fine as segments (call.continue, chat.for). In a file it is written whole ("onboarding.continue": "...") or as nested objects ({"onboarding": {"continue": "..."}}); both are the same key.
  • Placeholders are positional: {1}..{8} in the region, and the printf forms of the mobile platforms in the files: %s and %d (the next argument), %1$s and %2$d (argument n), %% for a %. %d takes an integer; %s and {n} take anything string interpolation writes.
  • A plural key’s first argument is the count, an integer; %d (or {1}) is the count. Its form is chosen by the active language’s CLDR plural rules (one, few, many, …), and a form a language doesn’t have falls back to its other.
  • For the active code, say fi_FI, @t shows the fi_FI text, else the fi text, else the source language’s; locale.set("") shows the source language’s.
  • A program usually keeps its catalogue in one module the others import: write the module’s alias first, @t(text.onboarding.continue) with import text from "./l10n.tape";. A key may not start with one of its module’s import aliases.
  • Only the keys a program names are compiled into it, so a catalogue of 2,000 keys shared with other clients costs only the texts you use.

Choosing the language

A program starts in the locale its environment names (LC_ALL, LC_MESSAGES or LANG, so LANG=fi_FI.UTF-8 is fi_FI). When none of them is set, as on Windows and for a macOS app started from Finder or the Dock, it starts in the user’s display language: the Windows display language (not the regional format), or the first of the macOS preferred languages. It can switch at any time:

tape
import locale from "locale";

locale.set("fi");   // Finnish from now on
locale.set("");     // the source strings

For the active code, say fi_FI, @t shows the fi_FI translation, else the fi translation, else the source string. See locale.

Limits

  • @t isn’t available in #test regions (test the functions that use it, after locale.set) or in t0.
  • Enum descriptions can’t be translated yet.
  • Translations are part of the program: JSON catalogues are read at compile time (Keyed translations above); exporting the @t strings for translators and loading translations from a file at run time are planned.

Last modified: