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 }