Translating Wren: how the texts are written
Every text the user reads goes through the i18n package (src/i18n), so that a language can be added without going through
the code again. Today English is the only language and tr returns its argument: writing the texts the right way now is what
makes the translation cheap later. The plan and the open questions are in the maintainer's notes ("Internationalisation").
import "wb:i18n"
label := i18n.tr("Open in a new tab")
close := i18n.tr("tab strip", "Close") // a context for the translator, never shown
text := fmt.tprintf(i18n.tr(count, "%d request", "%d requests"), count) // singular, plural, by countOne name, tr, a procedure group: the compiler picks the form from the arguments (a text; a context and a text; a count and two forms).
The English text is the key: no identifier to invent, and the code reads as before.
English only, and no cost
English is the only language for now, and the package has no code to load another one yet. tr returns its argument, a string
literal in the executable's read-only data: no lookup, no allocation, no copy, and the texts stay side by side where the linker
put them. A test (english_returns_the_same_bytes) keeps it that way.
A language, when there is one, must not cost more than that, because the interface is drawn every frame and tr runs for every
label:
- One blob, not many strings. The translations of a language are embedded in the executable (
#load) and sit one after the other in a single block that is never freed or moved. No translated text is allocated on its own. - One flat table, not a map of heap strings. A single allocation, open addressing. The key is the address and the length of the English literal (the pointer is the identity of a literal: hashing two machine words, never the characters); the value is the address and the length of the translation in the blob. A text with no entry returns itself.
- Switching the language swaps the table, and nothing else moves. Nothing is rebuilt per frame.
Writers keep the English text a literal, in the call, so that its address is stable: no
trof a text built at run time (that is also what the extraction needs).What goes through it
- Everything shown in the browser's own interface: labels, buttons, menus and their headings, tooltips, placeholders, the status
bar, toasts, dialogs (Setup, the DevTools, the ad blocker), the names and descriptions of commands and of preferences, error
pages, the texts of the internal pages (
about:). - The accessible name of a control.
What does not
- Technical names: preference names (
browser.frame.…), command line options, API names, file names, URLs, key names. - What goes to a log or a file for the developer (
stderr,shutdown.log, the profiler reports), and the test pages. - The licenses (
about:licenses): they are legal texts in their language. - What the web page itself says: the page's content,
Accept-Language,Intlin JavaScript: they follow the language of the browsing, not the language of the interface.
How to write a text
- A whole sentence, never pieces.
tr("Delete this entry?"), nottr("Delete") + " " + name + "?": word order differs between languages. A text with a value takes the value as a format argument:fmt.tprintf(tr("Loaded in %d ms"), ms). With two or more values, write the verbs with their index (Odin'sfmtcounts from 0):tr("%[0]d of %[1]d"), then a translation may reorder them (%[1]d of %[0]d) without any code change. Say what each is in a context when it is not obvious:tr("page, then total", "%[0]d of %[1]d"). A single value needs no index. - Plurals through
tr(count, "one", "other"), nevercount == 1in the caller. The call gives the two English forms; a translation supplies as many as its language has (Polish and Russian three, Arabic six), chosen by the language's own rule: the call does not change. - One literal in the call, on one line, so that
wrentool i18nfinds it. Notr(some_variable), no text built by another call: the extraction reads the call sites. - A context when an English word has two meanings (
tr("tab strip", "Close")andtr("dialog", "Close")): the translation may differ. - Leave room. German and Finnish texts run 30 to 40 % longer than English: a label that is exactly as wide as its English text is a bug. Truncate with an ellipsis and a tooltip with the whole text.
- No text in an image or in a glyph. An icon with a tooltip is translated through the tooltip.
- Keep an identifier apart from its text: the widget id after
###is an id, not a text ("Install location###setup_loc_label"becomestr("Install location") + "###setup_loc_label", or the toolkit's own form for it).
Checking
build\wrentool i18nlists the texts that go through the package, per file.build\wrentool i18n -Missinglists the interface calls that give a literal text withouttr: the work left. Run it on what you added; it should report nothing new.-Json file.jsonwrites the texts for a translation tool.