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

@@ -146,6 +146,31 @@ func (r *UserRepository) SetTimer(userID uint32, value bool) error {
return err
}
// GetLanguage returns the user's preferred language code. An empty string
// means "no preference set" (caller should fall back to the server default).
func (r *UserRepository) GetLanguage(userID uint32) (string, error) {
var lang sql.NullString
err := r.db.QueryRow(`SELECT language FROM users WHERE id=$1`, userID).Scan(&lang)
if err != nil {
return "", err
}
if !lang.Valid {
return "", nil
}
return lang.String, nil
}
// SetLanguage stores the user's preferred language code. An empty string
// clears the preference.
func (r *UserRepository) SetLanguage(userID uint32, lang string) error {
if lang == "" {
_, err := r.db.Exec(`UPDATE users SET language=NULL WHERE id=$1`, userID)
return err
}
_, err := r.db.Exec(`UPDATE users SET language=$1 WHERE id=$2`, lang, userID)
return err
}
// CountByPSNID returns the number of users with the given PSN ID.
func (r *UserRepository) CountByPSNID(psnID string) (int, error) {
var count int