Phase A plumbing for #188. Adds a users.language column (migration
0022), UserRepo.GetLanguage/SetLanguage, and Session.Lang()/SetLang
accessors so future phases can resolve localized content per session
instead of falling back to the server-wide config.Language.
The preference is loaded from the DB on login and persisted via a new
!lang <en|jp|fr|es> chat command that shows the current language when
called without an argument, validates the code (case-insensitive), and
replies in the newly selected language so the switch is visible
immediately. An empty stored value falls back to config.Language.
sys_language.go exposes getLangStringsFor(code) as the new dispatch
primitive; getLangStrings(server) is now a thin wrapper so existing
callers keep working unchanged. isSupportedLang + supportedLangs keep
the !lang validator in sync with the dispatcher.
Localized quest/scenario content and per-session i18n lookups in
existing handlers are deliberately out of scope for phase A — this
commit ships only the plumbing so it can be reviewed and deployed
independently.
TestLangCompleteness uses reflection to walk the i18n struct and fail
on any empty string field, catching incomplete translations at CI time.
Running it immediately found three missing strings in lang_jp.go
(playtime, timer.enabled, timer.disabled), which are now filled in.
CHANGELOG updated with i18n refactor, FR/ES languages, and the new test.
sys_language.go now contains only the i18n struct, Bead type, lookup
helpers, and a minimal getLangStrings dispatcher. All string data lives
in lang_en.go and lang_jp.go, making it straightforward to add a new
language by creating a single self-contained file.