feat(i18n): add completeness test and fix missing JP strings

TestLangCompleteness uses reflection to walk the i18n struct and fail
on any empty string field, catching incomplete translations at CI time.
Running it immediately found three missing strings in lang_jp.go
(playtime, timer.enabled, timer.disabled), which are now filled in.
CHANGELOG updated with i18n refactor, FR/ES languages, and the new test.
This commit is contained in:
Houmgaor
2026-03-22 17:09:16 +01:00
parent aff7953ab1
commit 05adda00d7
3 changed files with 43 additions and 0 deletions

View File

@@ -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.

View File

@@ -34,6 +34,11 @@ func langJapanese() i18n {
i.commands.ban.error = "Error in command. Format: %s <id> [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 = "大討伐は既に開催されています"

View File

@@ -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)
})
}
}