Русский и английский языки интерфейса
Язык выбирается в шапке панели, а до первого выбора берётся из настроек браузера. Переведены интерфейс, подписи и описания всех параметров конфигов, ошибки API и служебные сообщения менеджера в консоли. Подписи параметров вынесены из app.js в hints-<язык>.json: их больше четырёхсот на язык, и грузить чужой язык незачем. Сообщения менеджера переводятся на выходе из API по словарю, где ключом служит русская фраза из кода, — вывод самого игрового сервера через него проходит без изменений. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
// Package i18n переводит сообщения менеджера на язык веб-панели.
|
||||
//
|
||||
// Исходный язык кода — русский, он же служит ключом перевода: словарь связывает
|
||||
// русскую фразу с английской, а незнакомая фраза остаётся как есть. Благодаря
|
||||
// этому через перевод можно безопасно пропускать и вывод игрового сервера — он
|
||||
// английский и в словаре не встретится.
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Lang — язык сообщений панели.
|
||||
type Lang string
|
||||
|
||||
const (
|
||||
RU Lang = "ru"
|
||||
EN Lang = "en"
|
||||
// Default — язык, на котором сообщения написаны в коде.
|
||||
Default = RU
|
||||
)
|
||||
|
||||
// CookieName — кука, которой панель сообщает выбранный человеком язык.
|
||||
const CookieName = "pzm_lang"
|
||||
|
||||
// Parse разбирает код языка; для незнакомого возвращает язык по умолчанию.
|
||||
func Parse(value string) (Lang, bool) {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "ru":
|
||||
return RU, true
|
||||
case "en":
|
||||
return EN, true
|
||||
}
|
||||
return Default, false
|
||||
}
|
||||
|
||||
// FromRequest определяет язык ответа. Выбор человека (кука, которую ставит
|
||||
// панель) важнее списка языков браузера: на сервер могут заходить с чужой
|
||||
// машины, а язык панели человек выбирает сам.
|
||||
func FromRequest(r *http.Request) Lang {
|
||||
if r == nil {
|
||||
return Default
|
||||
}
|
||||
if cookie, err := r.Cookie(CookieName); err == nil {
|
||||
if lang, ok := Parse(cookie.Value); ok {
|
||||
return lang
|
||||
}
|
||||
}
|
||||
return fromAcceptLanguage(r.Header.Get("Accept-Language"))
|
||||
}
|
||||
|
||||
// fromAcceptLanguage берёт первый понятный язык из заголовка. Веса (q=) не
|
||||
// разбираем: браузеры и так перечисляют языки по убыванию предпочтения.
|
||||
func fromAcceptLanguage(header string) Lang {
|
||||
for _, part := range strings.Split(header, ",") {
|
||||
tag := strings.TrimSpace(part)
|
||||
if i := strings.IndexByte(tag, ';'); i >= 0 {
|
||||
tag = tag[:i]
|
||||
}
|
||||
if i := strings.IndexByte(tag, '-'); i >= 0 {
|
||||
tag = tag[:i]
|
||||
}
|
||||
if lang, ok := Parse(tag); ok {
|
||||
return lang
|
||||
}
|
||||
}
|
||||
return Default
|
||||
}
|
||||
|
||||
// T переводит сообщение на указанный язык. Неизвестное сообщение возвращается
|
||||
// без изменений: потерять текст хуже, чем показать его на чужом языке.
|
||||
func T(lang Lang, message string) string {
|
||||
if lang == Default || lang == "" || message == "" {
|
||||
return message
|
||||
}
|
||||
return translate(message, 0)
|
||||
}
|
||||
|
||||
// maxDepth ограничивает разбор вложенных ошибок: «прочитать X: создать Y: ...».
|
||||
const maxDepth = 4
|
||||
|
||||
func translate(message string, depth int) string {
|
||||
if text, ok := messages[message]; ok {
|
||||
return text
|
||||
}
|
||||
if depth >= maxDepth {
|
||||
return message
|
||||
}
|
||||
for _, p := range patterns {
|
||||
match := p.re.FindStringSubmatch(message)
|
||||
if match == nil {
|
||||
continue
|
||||
}
|
||||
// Подставленные значения переводим тоже: в них попадает вложенная
|
||||
// ошибка целиком, и она может быть знакома словарю.
|
||||
args := make([]any, 0, len(match)-1)
|
||||
for _, group := range match[1:] {
|
||||
args = append(args, translate(group, depth+1))
|
||||
}
|
||||
return fmt.Sprintf(p.en, args...)
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
// pattern — пара форматов «русский → английский». Значения, подставленные в
|
||||
// сообщение при его сборке, вылавливаются группами регулярного выражения.
|
||||
type pattern struct {
|
||||
re *regexp.Regexp
|
||||
en string
|
||||
// weight — длина неизменяемой части. Чем она больше, тем конкретнее
|
||||
// шаблон, и тем раньше его стоит примерить.
|
||||
weight int
|
||||
}
|
||||
|
||||
var patterns []pattern
|
||||
|
||||
func init() {
|
||||
patterns = make([]pattern, 0, len(formats))
|
||||
for _, f := range formats {
|
||||
patterns = append(patterns, compile(f[0], f[1]))
|
||||
}
|
||||
sort.SliceStable(patterns, func(i, j int) bool {
|
||||
return patterns[i].weight > patterns[j].weight
|
||||
})
|
||||
}
|
||||
|
||||
func compile(ru, en string) pattern {
|
||||
parts := strings.Split(ru, "%s")
|
||||
weight := 0
|
||||
for _, part := range parts {
|
||||
weight += len(part)
|
||||
}
|
||||
|
||||
var body strings.Builder
|
||||
body.WriteString(regexp.QuoteMeta(parts[0]))
|
||||
for i := 1; i < len(parts); i++ {
|
||||
// Последняя группа жадная: в неё попадает вложенная ошибка целиком,
|
||||
// вместе со своими двоеточиями.
|
||||
group := "(.+?)"
|
||||
if i == len(parts)-1 && parts[i] == "" {
|
||||
group = "(.+)"
|
||||
}
|
||||
body.WriteString(group)
|
||||
body.WriteString(regexp.QuoteMeta(parts[i]))
|
||||
}
|
||||
// (?s) — сообщения бывают многострочными.
|
||||
return pattern{re: regexp.MustCompile("(?s)^" + body.String() + "$"), en: en, weight: weight}
|
||||
}
|
||||
Reference in New Issue
Block a user