The player's language
Showing your world in the player's language
Say which languages your content has text for, fallback first, and ask which one this player reads:
local GREETING = { en = "Welcome!", pt = "Bem-vindo!", es = "¡Bienvenido!" }
local function refresh()
label.text = GREETING[i18n.pick({ "en", "pt", "es" })]
end
refresh()
i18n.onChange(refresh) -- the player changed their language while playing
i18n.pick runs the engine's chain (below) over your list: a Brazilian
player (pt-BR, then en) gets pt, a player on es-419 gets es, and a
player whose languages your list does not hold gets the first entry. It
answers one of your tags exactly as you wrote it.
To see the world as a visitor with another language list would, stand that list in, and put this machine's own back when done:
i18n.emulate({ "pt-BR", "en" }) -- i18n.preferred, pick, locale and onChange all follow
i18n.emulate(nil) -- this machine's own languages again
The player's own choice (i18n.choose, below) comes before the platform list
in the chain, so an emulated list decides only while no choice is set; clear
one with i18n.choose(nil).
The rest of the surface
i18n.preferred() -- what the platform says the player reads, most preferred first
i18n.locale() -- the locale the engine's own text is in: "en", "pt-BR", ...
i18n.offered() -- the locales the engine offers for its own text, { tag, name, nativeName, fallback }
i18n.choice() -- the player's own choice, or nil
i18n.sources() -- every link of the chain and which one decided:
-- { locale, decidedBy, matched, choice?, zeromind?, platform, signedIn }
What the platform says the player reads
i18n.preferred() is the platform's own list, as BCP 47 tags in its canonical
case (pt-BR, zh-Hant-TW), most preferred first:
| platform | read from | a change reaches the engine |
|---|---|---|
| Linux | LANGUAGE, LC_ALL, LC_MESSAGES, LANG (a POSIX name such as pt_BR.UTF-8 reads as pt-BR; C and POSIX name no language) | at boot |
| Windows, macOS | the user's language list in the OS settings | at boot, and each time the engine's window regains focus |
| browser | navigator.languages | on the page's languagechange event |
| Android | the activity's configuration locales: the app's own language when the player set one for it, the device's language list otherwise | on the configuration change, while the app keeps running |
The list is what the platform reports and nothing more: a world that wants the player's region or second language reads it here.
How the locale is chosen
i18n.locale() and i18n.pick come from one chain. Each link is matched against the
offered locales, and the first link that matches decides:
- The player's choice, set with
i18n.choose(tag)and kept on the device for their ZeroMind account (the signed-out player has a choice of their own). It wins on every later boot. - The language ZeroMind shows the player: their account's language, or
the page's. ZeroMind's page passes it to the browser engine as the
zeromindLocaleboot argument; a desktop launch passes--zeromind-locale <tag>. An engine booted without it skips the link. - The platform's preferred languages, in order.
en, the engine's source language.i18n.picktakes your list's first entry here instead, so the fallback your content names is the one it gets.
A link matches an offered locale with the same tag, then one its tag
truncates to (pt-BR-x-abc to pt-BR, de-CH to de), then the first
offered locale of the same language (pt to pt-BR).
The engine offers a locale once its manifest is launched. Each manifest
states the locale's tag, its name in English and in itself, its fallback
chain (the locales a string missing from it is looked up in, en last) and
whether it is launched. Until a locale launches, every player gets en, and
i18n.sources().decidedBy reads default.
local s = i18n.sources()
-- { locale = "en", decidedBy = "default", matched = "en",
-- choice = nil, zeromind = nil, platform = { "pt-BR", "en-US" }, signedIn = true }
Choosing
local r = i18n.choose("pt-BR")
if not r.ok then
log.warn(r.reason) -- "`pt-BR` is not a locale the engine offers; it offers en"
end
i18n.choose(nil) -- clear the choice; the chain decides again
Only an offered locale can be chosen. The choice is kept for the account the engine is signed in as and comes back on the next boot on this device:
| platform | where |
|---|---|
| Linux, Windows, macOS | <data root>/account_prefs/, one JSON file per account (the data root the engine's other saved data lives under; ZERO_DATA_DIR overrides it) |
| Android | account_prefs/ in the app's files directory |
| browser | IndexedDB database zero-account-prefs |
Following changes
local id = i18n.onChange(function(locale, preferred)
refreshLabels(locale)
end)
i18n.offChange(id)
A subscription made from a component ends with that component, and the
callback runs in edit mode and play alike. The callback runs once per change, after the engine renegotiated: a platform
language change while the engine runs (or an i18n.emulate), a choice made
or cleared, the ZeroMind language moving, or a locale launching. The
locale it receives is already the one i18n.locale() answers, and
i18n.pick inside it answers for the new state.
Where else the locale goes
- Every request the engine sends ZeroMind on the player's behalf carries the
chosen locale in the
x-zeromind-localeheader, the same header the web app's own requests carry. - The text system picks its fallback fonts for the chosen locale (the
regional glyph forms of a script follow it);
/zero/runtime/text/localereads the locale it uses.