Files
pzmanager/web/i18n_test.go
T
mrleo1nidandClaude Opus 5 9a224bc070
CI / test (push) Successful in 22s
Release / release (push) Successful in 29s
Русский и английский языки интерфейса
Язык выбирается в шапке панели, а до первого выбора берётся из настроек
браузера. Переведены интерфейс, подписи и описания всех параметров конфигов,
ошибки API и служебные сообщения менеджера в консоли.

Подписи параметров вынесены из app.js в hints-<язык>.json: их больше четырёхсот
на язык, и грузить чужой язык незачем. Сообщения менеджера переводятся на
выходе из API по словарю, где ключом служит русская фраза из кода, — вывод
самого игрового сервера через него проходит без изменений.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 21:14:21 +03:00

179 lines
5.4 KiB
Go

package web
import (
"encoding/json"
"os"
"regexp"
"sort"
"strings"
"testing"
)
// Переводы легко разъезжаются: строку добавили в один язык и забыли про другой.
// Тесты ниже сверяют словари между собой и с разметкой.
func TestUIStringsCoverBothLanguages(t *testing.T) {
tables := parseStrings(t)
ru, en := tables["ru"], tables["en"]
if len(ru) == 0 || len(en) == 0 {
t.Fatalf("словари не разобраны: ru=%d en=%d", len(ru), len(en))
}
for key := range ru {
if _, ok := en[key]; !ok {
t.Errorf("нет английского перевода: %s", key)
}
}
for key := range en {
if _, ok := ru[key]; !ok {
t.Errorf("нет русского перевода: %s", key)
}
}
}
// Подстановки вида {count} должны совпадать: иначе в одном языке значение
// просто не появится.
func TestUIStringsPlaceholders(t *testing.T) {
tables := parseStrings(t)
placeholder := regexp.MustCompile(`\{(\w+)\}`)
for key, ruText := range tables["ru"] {
enText, ok := tables["en"][key]
if !ok {
continue
}
if got, want := names(placeholder, enText), names(placeholder, ruText); !equal(got, want) {
t.Errorf("%s: подстановки ru=%v, en=%v", key, want, got)
}
}
}
// Каждый ключ из разметки должен существовать в словаре, иначе на странице
// вместо подписи окажется сам ключ.
func TestMarkupKeysExist(t *testing.T) {
markup, err := os.ReadFile("static/index.html")
if err != nil {
t.Fatal(err)
}
tables := parseStrings(t)
attr := regexp.MustCompile(`data-i18n(?:-html|-placeholder|-title)?="([^"]+)"`)
for _, match := range attr.FindAllStringSubmatch(string(markup), -1) {
if _, ok := tables["ru"][match[1]]; !ok {
t.Errorf("ключ из index.html отсутствует в словаре: %s", match[1])
}
}
script, err := os.ReadFile("static/app.js")
if err != nil {
t.Fatal(err)
}
// Ключи, собранные из кусков (`state.${state}`), сюда не попадают — у них
// в коде есть запасной вариант на случай отсутствия перевода.
call := regexp.MustCompile(`\bt\("([^"]+)"`)
found := 0
for _, match := range call.FindAllStringSubmatch(string(script), -1) {
found++
if _, ok := tables["ru"][match[1]]; !ok {
t.Errorf("ключ из app.js отсутствует в словаре: %s", match[1])
}
}
if found == 0 {
t.Error("в app.js не найдено ни одного обращения к словарю")
}
}
// Подписи параметров конфигов лежат отдельными файлами: в них должны быть одни
// и те же ключи, и у каждого — подпись и описание.
func TestParamHintsMatch(t *testing.T) {
ru := readHints(t, "static/hints-ru.json")
en := readHints(t, "static/hints-en.json")
for _, section := range []string{"config", "sandbox"} {
if len(ru[section]) == 0 {
t.Fatalf("раздел %s пуст", section)
}
if len(ru[section]) != len(en[section]) {
t.Errorf("%s: ru=%d, en=%d параметров", section, len(ru[section]), len(en[section]))
}
for key, value := range ru[section] {
translated, ok := en[section][key]
if !ok {
t.Errorf("%s: нет английской подписи для %s", section, key)
continue
}
for _, pair := range [][]string{value, translated} {
if len(pair) != 2 || strings.TrimSpace(pair[0]) == "" || strings.TrimSpace(pair[1]) == "" {
t.Errorf("%s.%s: ожидались подпись и описание, получено %v", section, key, pair)
}
}
}
}
}
func readHints(t *testing.T, path string) map[string]map[string][]string {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
var hints map[string]map[string][]string
if err := json.Unmarshal(data, &hints); err != nil {
t.Fatalf("%s: %v", path, err)
}
return hints
}
// parseStrings вытаскивает словари из i18n.js. Разбирать JavaScript целиком
// незачем: строки записаны построчно, ключ в кавычках и двоеточие.
func parseStrings(t *testing.T) map[string]map[string]string {
t.Helper()
source, err := os.ReadFile("static/i18n.js")
if err != nil {
t.Fatal(err)
}
tables := map[string]map[string]string{}
entry := regexp.MustCompile(`^\s{4}"([^"]+)": "(.*)",$`)
current := ""
for _, line := range strings.Split(string(source), "\n") {
switch strings.TrimSpace(strings.TrimSuffix(line, "\r")) {
case "ru: {":
current = "ru"
tables[current] = map[string]string{}
continue
case "en: {":
current = "en"
tables[current] = map[string]string{}
continue
}
if current == "" {
continue
}
if match := entry.FindStringSubmatch(strings.TrimSuffix(line, "\r")); match != nil {
tables[current][match[1]] = match[2]
}
}
return tables
}
func names(re *regexp.Regexp, text string) []string {
found := []string{}
for _, match := range re.FindAllStringSubmatch(text, -1) {
found = append(found, match[1])
}
sort.Strings(found)
return found
}
func equal(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}