Two unrelated problems wear the one word, and neither buys anything toward the other. Reading a translated client is game text — GGG's words, shipped in the bundle, load-bearing: get one wrong and an item does not parse. Translating our own text is our words, compiled in, cosmetic: get one wrong and a button reads oddly. So they are two settings and two tables.
Config::client_languagepicks the assetsGameData::openmaps and the vocabularydata/lexiconreads item text with. Read once at startup (DataUpdater::set_language), because the bundle is opened with it and every parsed item points into that bundle — so Settings offers it, states that it lands on the next run, and does not pretend otherwise. Its options areGameData::languages(), off the manifest: asking for a language the bundle does not carry simply fails to open it, which is a worse way to find out.Config::ui_languageissrc/ui/strings.cpp—ui::Msg, one enum entry per piece of our own text,ui::text(), and one compiled-in table per language with English at index 0 as the fallback. A null or empty entry falls through to English, so a table can be added with only the rows somebody has actually translated.static_asserton the table length, because a short one would otherwise zero-fill its tail silently. Defaults to"auto", which follows the client. One binary for every language: a table is a few kilobytes against an executable already embedding four typefaces, and per-language builds would contradict the rule that a new league needs a data build rather than a release.
Three things that had to move for any of it to work, and are better even in English:
Property::key(data::PropertyKey). The printed label used to be the key:parsedispatched on"Attacks per Second"anditem/planmatched the same string again, two copies of one vocabulary drifting apart. The label stays on thePropertyfor the tooltip to draw; everything downstream decides on the key.Item::class_kind(data::ClassKind).is_flask()wasitem_class.ends_with("Flasks")andis_map()was== "Maps". The lexicon says which class is which kind, and parse needs no bundle to ask.BaseType::ref_nameon the wire. The trade API'sname/typeterms are English whatever language the client is, sowire_nameinitem/planstates the order once —trade_name(where the site files the item somewhere else entirely, i.e. a transfigured gem), thenref_name, then the printedname, which is the same string asref_nameon an English bundle.GameData::find_bases_by_refreads<lang>-items-ref.index.bin— which has shipped in every bundle and was never opened until now — and is how the app names a record the clipboard did not print: the blighted-map redirect to theMapbase is the case that proves it, since"Map"is a reference name and not what a translated client shows.