diff --git a/CHANGELOG.md b/CHANGELOG.md index 785074072..da2fe9e9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- French (`fr`) and Spanish (`es`) server language translations. Set `"Language": "fr"` or `"Language": "es"` in `config.json` to activate. +- `TestLangCompleteness` uses reflection to verify that every string field in `i18n` is populated for all registered languages — catches missing translations at CI time rather than silently serving empty strings in-game. +- Server-generated strings (commands, mail templates, Raviente announcements, Diva bead names, guild names) are now split into one file per language (`lang_en.go`, `lang_jp.go`, etc.). Adding a new language requires only a single self-contained file and a one-line registration in `getLangStrings` ([#185](https://github.com/Mezeporta/Erupe/issues/185)). - Hunting Tournament system: all six tournament handlers are now fully implemented and DB-backed. `MsgMhfEnterTournamentQuest` (0x00D2) wire format was derived from `mhfo-hd.dll` binary analysis. Schedule, cups, sub-events, player registrations, and run submissions are stored in five new tables. `EnumerateRanking` returns the active tournament schedule with phase-state computation; `EnumerateOrder` returns per-event leaderboards ranked by submission time. `TournamentDefaults.sql` seeds cup and sub-event data from live tournament #150. One field (`Unk2` / event_id mapping) remains unconfirmed pending a packet capture ([#184](https://github.com/Mezeporta/Erupe/issues/184)). Database migration `0015_tournament` (`tournaments`, `tournament_cups`, `tournament_sub_events`, `tournament_entries`, `tournament_results`). - Return/Rookie Guild system: new players are automatically placed in a temporary rookie guild (`return_type=1`) and returning players in a comeback guild (`return_type=2`) via `MSG_MHF_ENTRY_ROOKIE_GUILD`. Players graduate (leave) via `OperateGuildGraduateRookie`/`OperateGuildGraduateReturn`. Guild info response now reports `isReturnGuild` correctly. Database migration `0014_return_guilds` adds `return_type` to the `guilds` table. - `saveutil` admin CLI (`cmd/saveutil/`): `import`, `export`, `grant-import`, and `revoke-import` commands for transferring character save data between server instances without touching the database manually. diff --git a/server/channelserver/lang_jp.go b/server/channelserver/lang_jp.go index 3754cd2c7..031f0f2a8 100644 --- a/server/channelserver/lang_jp.go +++ b/server/channelserver/lang_jp.go @@ -34,6 +34,11 @@ func langJapanese() i18n { i.commands.ban.error = "Error in command. Format: %s [length]" i.commands.ban.length = " until %s" + i.commands.playtime = "プレイ時間:%d時間%d分%d秒" + + i.commands.timer.enabled = "クエストタイマーが有効になりました" + i.commands.timer.disabled = "クエストタイマーが無効になりました" + i.commands.ravi.noCommand = "ラヴィコマンドが指定されていません" i.commands.ravi.start.success = "大討伐を開始します" i.commands.ravi.start.error = "大討伐は既に開催されています" diff --git a/server/channelserver/sys_language_test.go b/server/channelserver/sys_language_test.go index 8888c07ec..d0665c4d6 100644 --- a/server/channelserver/sys_language_test.go +++ b/server/channelserver/sys_language_test.go @@ -1,6 +1,8 @@ package channelserver import ( + "fmt" + "reflect" "testing" cfg "erupe-ce/config" @@ -92,3 +94,36 @@ func TestGetLangStrings_EmptyLanguage(t *testing.T) { t.Errorf("Empty language should default to English, got %q", lang.language) } } + +// checkNoEmptyStrings recursively walks v and fails the test for any empty string field. +func checkNoEmptyStrings(t *testing.T, v reflect.Value, path string) { + t.Helper() + switch v.Kind() { + case reflect.String: + if v.String() == "" { + t.Errorf("missing translation: %s is empty", path) + } + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + checkNoEmptyStrings(t, v.Field(i), path+"."+v.Type().Field(i).Name) + } + case reflect.Slice: + for i := 0; i < v.Len(); i++ { + checkNoEmptyStrings(t, v.Index(i), fmt.Sprintf("%s[%d]", path, i)) + } + } +} + +func TestLangCompleteness(t *testing.T) { + languages := map[string]i18n{ + "en": langEnglish(), + "jp": langJapanese(), + "fr": langFrench(), + "es": langSpanish(), + } + for code, lang := range languages { + t.Run(code, func(t *testing.T) { + checkNoEmptyStrings(t, reflect.ValueOf(lang), code) + }) + } +}