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

@@ -48,6 +48,7 @@ type Session struct {
prevGuildID uint32 // Stores the last GuildID used in InfoGuild
charID uint32
userID uint32
clientLang string // Per-session language preference; empty = use server default
logKey []byte
sessionStart int64
courses []mhfcourse.Course
@@ -109,6 +110,28 @@ func NewSession(server *Server, conn net.Conn) *Session {
return s
}
// Lang returns the session's effective language code, falling back to the
// server's globally configured language when no per-user preference has been
// loaded. Callers should use this instead of reading erupeConfig.Language
// directly so that later phases can route localized content per session.
func (s *Session) Lang() string {
s.Lock()
lang := s.clientLang
s.Unlock()
if lang != "" {
return lang
}
return s.server.erupeConfig.Language
}
// SetLang updates the session's in-memory language preference. Persistence
// to the database is the caller's responsibility (via userRepo.SetLanguage).
func (s *Session) SetLang(lang string) {
s.Lock()
s.clientLang = lang
s.Unlock()
}
// Start starts the session packet send and recv loop(s).
func (s *Session) Start() {
s.logger.Debug("New connection", zap.String("RemoteAddr", s.rawConn.RemoteAddr().String()))