feat(i18n): localized quest text and per-lang quest cache

Phase B of #188. Quest JSON title/description/text_main/text_sub_a/
text_sub_b/success_cond/fail_cond/contractor now accept either a plain
string (existing behaviour) or a language-keyed object like
  "title": { "jp": "...", "en": "...", "fr": "..." }
CompileQuestJSON takes the compiling session's language and resolves
each field through a fallback chain (requested -> plain -> jp -> en ->
any non-empty), so existing single-language quest JSONs keep working
byte-for-byte unchanged.

The quest cache is re-keyed on (questID, language) so compiled binaries
for different languages never leak between sessions on a multi-language
server. loadQuestBinary and loadQuestFile now pass s.Lang() into both
the compiler and the cache.

ParseQuestBinary emits plain-string LocalizedStrings, so the
binary -> JSON -> binary round-trip still produces identical output.

The new LocalizedString type lives in its own file and is reusable by
phase C (scenarios, mail templates, shop text). Shift-JIS encoding
still applies to the wire format, so localized values must use
characters representable in Shift-JIS — ASCII, kana, CJK — which is
documented on the type.
This commit is contained in:
Houmgaor
2026-04-06 20:00:43 +02:00
parent 5b38bfde3f
commit f7ea275540
9 changed files with 506 additions and 75 deletions

View File

@@ -171,14 +171,17 @@ func ParseQuestBinary(data []byte) (*QuestJSON, error) {
}
texts[i] = s
}
q.Title = texts[0]
q.TextMain = texts[1]
q.TextSubA = texts[2]
q.TextSubB = texts[3]
q.SuccessCond = texts[4]
q.FailCond = texts[5]
q.Contractor = texts[6]
q.Description = texts[7]
// The binary carries only one language, so the reverse path emits
// plain-string LocalizedStrings. Editors wanting multi-language
// quests should wrap these as {"jp": "...", "en": "..."} by hand.
q.Title = NewLocalizedPlain(texts[0])
q.TextMain = NewLocalizedPlain(texts[1])
q.TextSubA = NewLocalizedPlain(texts[2])
q.TextSubB = NewLocalizedPlain(texts[3])
q.SuccessCond = NewLocalizedPlain(texts[4])
q.FailCond = NewLocalizedPlain(texts[5])
q.Contractor = NewLocalizedPlain(texts[6])
q.Description = NewLocalizedPlain(texts[7])
}
// ── Stages ───────────────────────────────────────────────────────────