mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-05-06 14:24:15 +02:00
feat(i18n): add Chinese (zh) language support
This commit is contained in:
@@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Server-side multi-language support ([#188](https://github.com/Mezeporta/Erupe/issues/188)): each player picks their own language with `!lang <en|jp|fr|es>`, persisted per user (migration `0022_user_language`) and loaded on login. Chat replies, guild invite mails, and cafe/timer broadcasts are served in that language via `Session.I18n()`. Quest and scenario JSON text fields now accept either a plain string (unchanged) or a `{"jp":"...","en":"...","fr":"..."}` map; the compiler resolves per session and the quest cache is keyed by `(questID, lang)`. Existing single-language JSONs and `.bin` round-trips remain byte-identical. Shift-JIS wire encoding still applies (ASCII/kana/CJK only). Raviente world-wide broadcasts stay on the server default since they have no single session.
|
- Chinese (`zh`) language strings for chat commands, guild mails, cafe/timer broadcasts and prayer beads. Note: Shift-JIS wire encoding only covers characters shared with Japanese — simplified-only glyphs may fail to encode.
|
||||||
|
- Server-side multi-language support ([#188](https://github.com/Mezeporta/Erupe/issues/188)): each player picks their own language with `!lang <en|jp|fr|es|zh>`, persisted per user (migration `0022_user_language`) and loaded on login. Chat replies, guild invite mails, and cafe/timer broadcasts are served in that language via `Session.I18n()`. Quest and scenario JSON text fields now accept either a plain string (unchanged) or a `{"jp":"...","en":"...","fr":"..."}` map; the compiler resolves per session and the quest cache is keyed by `(questID, lang)`. Existing single-language JSONs and `.bin` round-trips remain byte-identical. Shift-JIS wire encoding still applies (ASCII/kana/CJK only). Raviente world-wide broadcasts stay on the server default since they have no single session.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -189,7 +189,7 @@
|
|||||||
}, {
|
}, {
|
||||||
"Name": "Language",
|
"Name": "Language",
|
||||||
"Enabled": true,
|
"Enabled": true,
|
||||||
"Description": "Show or change your preferred language (en|jp|fr|es)",
|
"Description": "Show or change your preferred language (en|jp|fr|es|zh)",
|
||||||
"Prefix": "lang"
|
"Prefix": "lang"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -446,7 +446,7 @@ func registerDefaults() {
|
|||||||
{Name: "Ban", Enabled: false, Description: "Ban/Temp Ban a user", Prefix: "ban"},
|
{Name: "Ban", Enabled: false, Description: "Ban/Temp Ban a user", Prefix: "ban"},
|
||||||
{Name: "Timer", Enabled: true, Description: "Toggle the Quest timer", Prefix: "timer"},
|
{Name: "Timer", Enabled: true, Description: "Toggle the Quest timer", Prefix: "timer"},
|
||||||
{Name: "Playtime", Enabled: true, Description: "Show your playtime", Prefix: "playtime"},
|
{Name: "Playtime", Enabled: true, Description: "Show your playtime", Prefix: "playtime"},
|
||||||
{Name: "Language", Enabled: true, Description: "Show or change your preferred language (en|jp|fr|es)", Prefix: "lang"},
|
{Name: "Language", Enabled: true, Description: "Show or change your preferred language (en|jp|fr|es|zh)", Prefix: "lang"},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Courses
|
// Courses
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ func langEnglish() i18n {
|
|||||||
i.commands.timer.enabled = "Quest timer enabled"
|
i.commands.timer.enabled = "Quest timer enabled"
|
||||||
i.commands.timer.disabled = "Quest timer disabled"
|
i.commands.timer.disabled = "Quest timer disabled"
|
||||||
|
|
||||||
i.commands.lang.usage = "Usage: %s <en|jp|fr|es>"
|
i.commands.lang.usage = "Usage: %s <en|jp|fr|es|zh>"
|
||||||
i.commands.lang.invalid = "Unknown language %q. Supported: en, jp, fr, es"
|
i.commands.lang.invalid = "Unknown language %q. Supported: en, jp, fr, es, zh"
|
||||||
i.commands.lang.success = "Language set to %s"
|
i.commands.lang.success = "Language set to %s"
|
||||||
i.commands.lang.current = "Current language: %s"
|
i.commands.lang.current = "Current language: %s"
|
||||||
|
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ func langSpanish() i18n {
|
|||||||
i.commands.timer.enabled = "Temporizador de misión activado"
|
i.commands.timer.enabled = "Temporizador de misión activado"
|
||||||
i.commands.timer.disabled = "Temporizador de misión desactivado"
|
i.commands.timer.disabled = "Temporizador de misión desactivado"
|
||||||
|
|
||||||
i.commands.lang.usage = "Uso: %s <en|jp|fr|es>"
|
i.commands.lang.usage = "Uso: %s <en|jp|fr|es|zh>"
|
||||||
i.commands.lang.invalid = "Idioma desconocido %q. Compatibles: en, jp, fr, es"
|
i.commands.lang.invalid = "Idioma desconocido %q. Compatibles: en, jp, fr, es, zh"
|
||||||
i.commands.lang.success = "Idioma establecido en %s"
|
i.commands.lang.success = "Idioma establecido en %s"
|
||||||
i.commands.lang.current = "Idioma actual: %s"
|
i.commands.lang.current = "Idioma actual: %s"
|
||||||
|
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ func langFrench() i18n {
|
|||||||
i.commands.timer.enabled = "Minuteur de quête activé"
|
i.commands.timer.enabled = "Minuteur de quête activé"
|
||||||
i.commands.timer.disabled = "Minuteur de quête désactivé"
|
i.commands.timer.disabled = "Minuteur de quête désactivé"
|
||||||
|
|
||||||
i.commands.lang.usage = "Utilisation : %s <en|jp|fr|es>"
|
i.commands.lang.usage = "Utilisation : %s <en|jp|fr|es|zh>"
|
||||||
i.commands.lang.invalid = "Langue inconnue %q. Prises en charge : en, jp, fr, es"
|
i.commands.lang.invalid = "Langue inconnue %q. Prises en charge : en, jp, fr, es, zh"
|
||||||
i.commands.lang.success = "Langue définie sur %s"
|
i.commands.lang.success = "Langue définie sur %s"
|
||||||
i.commands.lang.current = "Langue actuelle : %s"
|
i.commands.lang.current = "Langue actuelle : %s"
|
||||||
|
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ func langJapanese() i18n {
|
|||||||
i.commands.timer.enabled = "クエストタイマーが有効になりました"
|
i.commands.timer.enabled = "クエストタイマーが有効になりました"
|
||||||
i.commands.timer.disabled = "クエストタイマーが無効になりました"
|
i.commands.timer.disabled = "クエストタイマーが無効になりました"
|
||||||
|
|
||||||
i.commands.lang.usage = "使い方: %s <en|jp|fr|es>"
|
i.commands.lang.usage = "使い方: %s <en|jp|fr|es|zh>"
|
||||||
i.commands.lang.invalid = "未対応の言語 %q。対応言語: en, jp, fr, es"
|
i.commands.lang.invalid = "未対応の言語 %q。対応言語: en, jp, fr, es, zh"
|
||||||
i.commands.lang.success = "言語を %s に設定しました"
|
i.commands.lang.success = "言語を %s に設定しました"
|
||||||
i.commands.lang.current = "現在の言語: %s"
|
i.commands.lang.current = "現在の言語: %s"
|
||||||
|
|
||||||
|
|||||||
104
server/channelserver/lang_zh.go
Normal file
104
server/channelserver/lang_zh.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package channelserver
|
||||||
|
|
||||||
|
func langChinese() i18n {
|
||||||
|
var i i18n
|
||||||
|
|
||||||
|
i.language = "中文"
|
||||||
|
i.cafe.reset = "重置于 %d/%d"
|
||||||
|
i.timer = "时间:%02d:%02d:%02d.%03d (%df)"
|
||||||
|
|
||||||
|
i.commands.noOp = "您没有使用此命令的权限"
|
||||||
|
i.commands.disabled = "%s 命令已禁用"
|
||||||
|
i.commands.reload = "正在重新加载玩家..."
|
||||||
|
i.commands.playtime = "游戏时间:%d 小时 %d 分钟 %d 秒"
|
||||||
|
|
||||||
|
i.commands.kqf.get = "KQF:%x"
|
||||||
|
i.commands.kqf.set.error = "命令错误。格式:%s set xxxxxxxxxxxxxxxx"
|
||||||
|
i.commands.kqf.set.success = "已设置 KQF,请切换区域/世界"
|
||||||
|
i.commands.kqf.version = "此命令在 MHFG10 之前已禁用"
|
||||||
|
i.commands.rights.error = "命令错误。格式:%s x"
|
||||||
|
i.commands.rights.success = "设置权限整数:%d"
|
||||||
|
i.commands.course.error = "命令错误。格式:%s <名称>"
|
||||||
|
i.commands.course.disabled = "%s 课程已禁用"
|
||||||
|
i.commands.course.enabled = "%s 课程已启用"
|
||||||
|
i.commands.course.locked = "%s 课程已锁定"
|
||||||
|
i.commands.teleport.error = "命令错误。格式:%s x y"
|
||||||
|
i.commands.teleport.success = "传送至 %d %d"
|
||||||
|
i.commands.psn.error = "命令错误。格式:%s <psn id>"
|
||||||
|
i.commands.psn.success = "已连接 PSN ID:%s"
|
||||||
|
i.commands.psn.exists = "该 PSN ID 已连接到其他账户!"
|
||||||
|
|
||||||
|
i.commands.discord.success = "您的 Discord 令牌:%s"
|
||||||
|
|
||||||
|
i.commands.ban.noUser = "找不到用户"
|
||||||
|
i.commands.ban.success = "已成功封禁 %s"
|
||||||
|
i.commands.ban.invalid = "角色 ID 无效"
|
||||||
|
i.commands.ban.error = "命令错误。格式:%s <id> [时长]"
|
||||||
|
i.commands.ban.length = " 直到 %s"
|
||||||
|
|
||||||
|
i.commands.timer.enabled = "任务计时器已启用"
|
||||||
|
i.commands.timer.disabled = "任务计时器已禁用"
|
||||||
|
|
||||||
|
i.commands.lang.usage = "用法:%s <en|jp|fr|es|zh>"
|
||||||
|
i.commands.lang.invalid = "未知语言 %q。支持的语言:en, jp, fr, es, zh"
|
||||||
|
i.commands.lang.success = "语言已设置为 %s"
|
||||||
|
i.commands.lang.current = "当前语言:%s"
|
||||||
|
|
||||||
|
i.commands.ravi.noCommand = "未指定 Raviente 命令!"
|
||||||
|
i.commands.ravi.start.success = "大讨伐战即将开始"
|
||||||
|
i.commands.ravi.start.error = "大讨伐战已经开始!"
|
||||||
|
i.commands.ravi.multiplier = "Raviente 倍率当前为 %.2fx"
|
||||||
|
i.commands.ravi.res.success = "正在发送复活支援!"
|
||||||
|
i.commands.ravi.res.error = "尚未请求复活支援!"
|
||||||
|
i.commands.ravi.sed.success = "若有请求则发送镇静支援!"
|
||||||
|
i.commands.ravi.request = "请求镇静支援!"
|
||||||
|
i.commands.ravi.error = "无法识别的 Raviente 命令!"
|
||||||
|
i.commands.ravi.noPlayers = "无人参加大讨伐战!"
|
||||||
|
i.commands.ravi.version = "此命令在 MHFZZ 以外已禁用"
|
||||||
|
|
||||||
|
i.raviente.berserk = "<大讨伐战:狂暴> 正在进行!"
|
||||||
|
i.raviente.extreme = "<大讨伐战:极限> 正在进行!"
|
||||||
|
i.raviente.extremeLimited = "<大讨伐战:极限(限定)> 正在进行!"
|
||||||
|
i.raviente.berserkSmall = "<大讨伐战:狂暴(小型)> 正在进行!"
|
||||||
|
|
||||||
|
i.guild.rookieGuildName = "新人猎团 %d"
|
||||||
|
i.guild.returnGuildName = "回归猎团 %d"
|
||||||
|
|
||||||
|
i.guild.invite.title = "邀请!"
|
||||||
|
i.guild.invite.body = "您已被邀请加入\n「%s」\n是否接受?"
|
||||||
|
|
||||||
|
i.guild.invite.success.title = "成功!"
|
||||||
|
i.guild.invite.success.body = "您已成功加入\n「%s」。"
|
||||||
|
|
||||||
|
i.guild.invite.accepted.title = "已接受"
|
||||||
|
i.guild.invite.accepted.body = "对方已接受您加入\n「%s」的邀请。"
|
||||||
|
|
||||||
|
i.guild.invite.rejected.title = "已拒绝"
|
||||||
|
i.guild.invite.rejected.body = "您拒绝了加入\n「%s」的邀请。"
|
||||||
|
|
||||||
|
i.guild.invite.declined.title = "已婉拒"
|
||||||
|
i.guild.invite.declined.body = "对方婉拒了您加入\n「%s」的邀请。"
|
||||||
|
|
||||||
|
i.beads = []Bead{
|
||||||
|
{1, "风暴之珠", "蕴含风暴之力的祈祷珠。\n召唤狂风助益同伴。"},
|
||||||
|
{3, "斩击之珠", "蕴含斩击之力的祈祷珠。\n增强同伴的斩击力。"},
|
||||||
|
{4, "活力之珠", "蕴含活力的祈祷珠。\n提升周围同伴的生命值。"},
|
||||||
|
{8, "治愈之珠", "蕴含治愈之力的祈祷珠。\n以恢复能量守护同伴。"},
|
||||||
|
{9, "狂怒之珠", "蕴含狂怒能量的祈祷珠。\n以战斗怒火激励同伴。"},
|
||||||
|
{10, "瘴气之珠", "蕴含瘴气的祈祷珠。\n为同伴注入毒性之力。"},
|
||||||
|
{11, "力量之珠", "蕴含原始力量的祈祷珠。\n赋予同伴压倒性的力量。"},
|
||||||
|
{14, "雷鸣之珠", "蕴含闪电的祈祷珠。\n为同伴充填电力。"},
|
||||||
|
{15, "寒冰之珠", "蕴含酷寒的祈祷珠。\n赋予同伴冰属性之力。"},
|
||||||
|
{17, "烈火之珠", "蕴含灼热的祈祷珠。\n以烈火属性点燃同伴。"},
|
||||||
|
{18, "流水之珠", "蕴含流水的祈祷珠。\n赋予同伴水属性之力。"},
|
||||||
|
{19, "神龙之珠", "蕴含龙之能量的祈祷珠。\n赋予同伴龙属性之力。"},
|
||||||
|
{20, "大地之珠", "蕴含大地之力的祈祷珠。\n以大地属性稳固同伴。"},
|
||||||
|
{21, "疾风之珠", "蕴含疾风的祈祷珠。\n提升同伴的敏捷。"},
|
||||||
|
{22, "光辉之珠", "蕴含光辉的祈祷珠。\n以光明能量鼓舞同伴。"},
|
||||||
|
{23, "暗影之珠", "蕴含黑暗的祈祷珠。\n为同伴注入暗影之力。"},
|
||||||
|
{24, "铁壁之珠", "蕴含钢铁之力的祈祷珠。\n为同伴强化防御。"},
|
||||||
|
{25, "免疫之珠", "蕴含封印之力的祈祷珠。\n消除同伴的属性弱点。"},
|
||||||
|
}
|
||||||
|
|
||||||
|
return i
|
||||||
|
}
|
||||||
@@ -141,7 +141,7 @@ func (i *i18n) beadDescription(beadType int) string {
|
|||||||
// supportedLangs lists the language codes the server can serve. Kept in one
|
// supportedLangs lists the language codes the server can serve. Kept in one
|
||||||
// place so the !lang command validator and future API handlers stay in sync
|
// place so the !lang command validator and future API handlers stay in sync
|
||||||
// with getLangStringsFor.
|
// with getLangStringsFor.
|
||||||
var supportedLangs = []string{"en", "jp", "fr", "es"}
|
var supportedLangs = []string{"en", "jp", "fr", "es", "zh"}
|
||||||
|
|
||||||
// isSupportedLang reports whether the given code is one the server can serve.
|
// isSupportedLang reports whether the given code is one the server can serve.
|
||||||
func isSupportedLang(code string) bool {
|
func isSupportedLang(code string) bool {
|
||||||
@@ -166,6 +166,8 @@ func getLangStringsFor(lang string) i18n {
|
|||||||
return langFrench()
|
return langFrench()
|
||||||
case "es":
|
case "es":
|
||||||
return langSpanish()
|
return langSpanish()
|
||||||
|
case "zh":
|
||||||
|
return langChinese()
|
||||||
case "en":
|
case "en":
|
||||||
return langEnglish()
|
return langEnglish()
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ func TestGetLangStringsFor(t *testing.T) {
|
|||||||
{"jp", "日本語", false, ""},
|
{"jp", "日本語", false, ""},
|
||||||
{"fr", "Français", true, ""},
|
{"fr", "Français", true, ""},
|
||||||
{"es", "Español", true, ""},
|
{"es", "Español", true, ""},
|
||||||
|
{"zh", "中文", true, ""},
|
||||||
{"", "English", true, ""},
|
{"", "English", true, ""},
|
||||||
{"xx", "English", true, ""},
|
{"xx", "English", true, ""},
|
||||||
}
|
}
|
||||||
@@ -145,7 +146,7 @@ func TestGetLangStringsFor(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIsSupportedLang(t *testing.T) {
|
func TestIsSupportedLang(t *testing.T) {
|
||||||
for _, code := range []string{"en", "jp", "fr", "es"} {
|
for _, code := range []string{"en", "jp", "fr", "es", "zh"} {
|
||||||
if !isSupportedLang(code) {
|
if !isSupportedLang(code) {
|
||||||
t.Errorf("isSupportedLang(%q) = false, want true", code)
|
t.Errorf("isSupportedLang(%q) = false, want true", code)
|
||||||
}
|
}
|
||||||
@@ -186,6 +187,7 @@ func TestLangCompleteness(t *testing.T) {
|
|||||||
"jp": langJapanese(),
|
"jp": langJapanese(),
|
||||||
"fr": langFrench(),
|
"fr": langFrench(),
|
||||||
"es": langSpanish(),
|
"es": langSpanish(),
|
||||||
|
"zh": langChinese(),
|
||||||
}
|
}
|
||||||
for code, lang := range languages {
|
for code, lang := range languages {
|
||||||
t.Run(code, func(t *testing.T) {
|
t.Run(code, func(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user