#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
#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
#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
| Problem | Result |
|---|---|
@t("...") with no entry | error |
| a translation that uses another placeholder, or leaves one out | error |
| the same source string twice, or two translations into one language | error |
| a placeholder that isn’t a name, or more than 8 | error |
| an unknown name in a placeholder | error (as in interpolation) |
an entry no @t uses | warning |
@t(key) of a key no region declares | error |
@t(key, ...) with more or fewer arguments than its source text’s highest placeholder, or a non-integer for a %d or a count | error |
a key’s text that uses other placeholders than its source text, or %d where the source has %s | error |
| 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 declare | error |
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 uses | warning (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:
#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>;andlanguages <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
.tapefile.{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. Apluralsfile maps key → language → forms (zero,one,two,few,many,other; every language’s forms needother). 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:%sand%d(the next argument),%1$sand%2$d(argument n),%%for a%.%dtakes an integer;%sand{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 itsother. - For the active code, say
fi_FI,@tshows thefi_FItext, else thefitext, 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)withimport 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:
import locale from "locale";
locale.set("fi"); // Finnish from now on
locale.set(""); // the source stringsFor the active code, say fi_FI, @t shows the fi_FI translation, else
the fi translation, else the source string. See
locale.
Limits
@tisn’t available in#testregions (test the functions that use it, afterlocale.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
@tstrings for translators and loading translations from a file at run time are planned.
Last modified: