feat(i18n): per-session i18n routing and localized scenarios

Phase C of #188 — the last phase of server-side multi-language support.

Adds Session.I18n(), a cached per-session i18n table resolver built via
getLangStringsFor(s.Lang()). The pointer is stable until SetLang
invalidates the cache, so hot-path handlers pay zero allocations on
repeated calls. All 51 s.server.i18n.* call sites across commands,
guild, guild scout, cafe, and cast-binary handlers now route through
s.I18n().*, so chat replies, guild invite mail templates, cafe reset
notices, and quest-timer broadcasts are served in the player's
preferred language instead of the server-wide default.

Scenario JSON gets the same plain-or-map LocalizedString treatment
that quests received in phase B: subheader Strings and inline entry
Text accept either a plain string (backwards compatible) or a
language-keyed object. CompileScenarioJSON takes the compiling
session's language, loadScenarioBinary passes s.Lang(), and
ParseScenarioBinary emits plain-string LocalizedStrings so existing
.bin files round-trip byte-for-byte through the JSON path.

World-wide broadcasts (Raviente siege announcements via
BroadcastRaviente) intentionally stay on the server default — they
have no single-session context to resolve against.
This commit is contained in:
Houmgaor
2026-04-06 20:08:27 +02:00
parent f7ea275540
commit 5361e67b1a
11 changed files with 278 additions and 99 deletions

View File

@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Per-session i18n routing and localized scenarios (phase C of [#188](https://github.com/Mezeporta/Erupe/issues/188)): `Session.I18n()` returns a cached, language-resolved `i18n` table built via `getLangStringsFor(s.Lang())`; `SetLang` invalidates the cache. All 51 existing `s.server.i18n.*` call sites in `handlers_commands.go`, `handlers_guild.go`, `handlers_guild_scout.go`, `handlers_cafe.go`, and `handlers_cast_binary.go` now route through `s.I18n().*`, so chat replies, guild invite mails, cafe reset notices, and quest-timer broadcasts are served in the player's preferred language. Scenario JSON subheader `strings` and inline entry `text` fields now accept the same plain/map `LocalizedString` schema as quests (phase B); `CompileScenarioJSON` takes a language argument and `loadScenarioBinary` passes `s.Lang()`. Round-tripping existing `.bin` files through the JSON path still produces byte-identical output. World-wide broadcasts (Raviente siege announcements via `BroadcastRaviente`) intentionally remain on the server default because they have no single-session context.
- Localized quest text (phase B of [#188](https://github.com/Mezeporta/Erupe/issues/188)): quest JSON `title`, `description`, `text_main`, `text_sub_a`, `text_sub_b`, `success_cond`, `fail_cond`, and `contractor` now accept either a plain string (existing behaviour, single-language) or a language-keyed object like `{"jp": "...", "en": "...", "fr": "..."}`. `CompileQuestJSON` takes the requesting session's language and resolves each field with a fallback chain (requested → plain → jp → en → any non-empty). The quest cache is now keyed by `(questID, language)` so compiled binaries for different languages never leak between sessions. `ParseQuestBinary` emits plain strings unchanged, so round-tripping existing `.bin` files through the JSON path produces byte-identical output. New `LocalizedString` type is reusable for phase C (scenarios, mail, shop). Shift-JIS encoding limits still apply — localized strings must use characters representable in Shift-JIS (ASCII, kana, CJK).
- Per-session language preference (phase A of [#188](https://github.com/Mezeporta/Erupe/issues/188)): new `users.language` column (migration `0022_user_language`), `UserRepo.GetLanguage`/`SetLanguage`, `Session.Lang()`/`SetLang()` accessors, and a `!lang <en|jp|fr|es>` command to show or change the session's language live. The preference is loaded on login and persisted across sessions; an empty value falls back to `config.Language`. The `getLangStringsFor(code)` primitive is the new way to resolve an i18n table from a concrete code — existing callers keep working via the unchanged `getLangStrings(server)` wrapper. This is plumbing only: localized quest/scenario content comes in phase B.