Log inGet started

The player's language

Updated October 12, 2026

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:

platformread froma change reaches the engine
LinuxLANGUAGE, 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, macOSthe user's language list in the OS settingsat boot, and each time the engine's window regains focus
browsernavigator.languageson the page's languagechange event
Androidthe activity's configuration locales: the app's own language when the player set one for it, the device's language list otherwiseon 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:

  1. 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.
  2. 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 zeromindLocale boot argument; a desktop launch passes --zeromind-locale <tag>. An engine booted without it skips the link.
  3. The platform's preferred languages, in order.
  4. en, the engine's source language. i18n.pick takes 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:

platformwhere
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)
Androidaccount_prefs/ in the app's files directory
browserIndexedDB 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-locale header, 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/locale reads the locale it uses.