feat(i18n): per-session language preference and !lang command

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.
This commit is contained in:
Houmgaor
2026-04-06 19:52:19 +02:00
parent 803996adac
commit 5b38bfde3f
18 changed files with 341 additions and 8 deletions

View File

@@ -446,6 +446,7 @@ func registerDefaults() {
{Name: "Ban", Enabled: false, Description: "Ban/Temp Ban a user", Prefix: "ban"},
{Name: "Timer", Enabled: true, Description: "Toggle the Quest timer", Prefix: "timer"},
{Name: "Playtime", Enabled: true, Description: "Show your playtime", Prefix: "playtime"},
{Name: "Language", Enabled: true, Description: "Show or change your preferred language (en|jp|fr|es)", Prefix: "lang"},
})
// Courses

View File

@@ -552,8 +552,8 @@ func TestMinimalConfigDefaults(t *testing.T) {
}
// Commands should be present
if len(cfg.Commands) != 12 {
t.Errorf("Commands = %d, want 12", len(cfg.Commands))
if len(cfg.Commands) != 13 {
t.Errorf("Commands = %d, want 13", len(cfg.Commands))
}
// Courses should be present
@@ -644,8 +644,8 @@ func TestFullConfigBackwardCompat(t *testing.T) {
if len(cfg.Entrance.Entries) != 6 {
t.Errorf("Entrance.Entries = %d, want 6", len(cfg.Entrance.Entries))
}
if len(cfg.Commands) != 12 {
t.Errorf("Commands = %d, want 12", len(cfg.Commands))
if len(cfg.Commands) != 13 {
t.Errorf("Commands = %d, want 13", len(cfg.Commands))
}
if cfg.GameplayOptions.MaximumNP != 100000 {
t.Errorf("MaximumNP = %d, want 100000", cfg.GameplayOptions.MaximumNP)