комментарии и сообщения переведены на английский

- комментарии, тексты ошибок, вывод CLI и сообщения тестов теперь на английском
- по-русски остались только литералы словаря ru и содержимое фикстур: это
  данные под проверкой, а не текст инструмента
- согласование числительных в итоге упростилось до английского plural
This commit is contained in:
av
2026-07-27 10:04:27 +03:00
parent 0b8cc125b3
commit b2d07ae55d
17 changed files with 548 additions and 524 deletions
+74 -67
View File
@@ -10,8 +10,13 @@ import (
"git.vakhrushev.me/av/convy/internal/suite"
)
// versionLine — строка о версии языка. Она перечисляет ключевые слова набора,
// поэтому единственная законно несёт модальные слова вне правил.
// The fixtures below stay in Russian on purpose: they are the data under test,
// written in the natural language the suite declares. Only the tool's own text
// is English.
// versionLine is the language version line. It lists the key words of the
// suite, which makes it the only place lawfully carrying modal words outside a
// rule.
const versionLine = `Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
конвенций версии 1 — тогда и только тогда, когда написаны заглавными.`
@@ -53,8 +58,9 @@ prefix: TIME
**ПОЧЕМУ.** Без явного смещения не видно, в какой зоне запись сделана.
`
// files — содержимое набора: путь от корня к тексту файла. Пустая строка
// значит «файла нет»: так тест снимает файл, который есть в основе.
// files is the content of a suite: a path from the root mapped to the text of
// the file. An empty string means "no such file": that is how a test drops a
// file the base fixture provides.
type files map[string]string
func base() files {
@@ -66,7 +72,7 @@ func base() files {
}
}
// run записывает набор во временную директорию и прогоняет проверки.
// run writes a suite into a temporary directory and runs the checks over it.
func run(t *testing.T, f files) []check.Finding {
t.Helper()
root := t.TempDir()
@@ -84,7 +90,7 @@ func run(t *testing.T, f files) []check.Finding {
}
s, err := suite.Load(root)
if err != nil {
t.Fatalf("загрузка набора: %v", err)
t.Fatalf("loading the suite: %v", err)
}
return check.Suite(s).Findings()
}
@@ -102,25 +108,26 @@ func messages(findings []check.Finding) string {
func TestCleanSuite(t *testing.T) {
if got := run(t, base()); len(got) != 0 {
t.Fatalf("исправный набор дал находки:\n%s", messages(got))
t.Fatalf("a sound suite produced findings:\n%s", messages(got))
}
}
// TestRuleHeadingIsNotAReference держит границу между объявлением и ссылкой.
// Заголовок с чужим префиксом — ошибка формы, и только она: разрешать заголовок
// по манифесту не к чему, иначе одна опечатка даёт две находки о разном.
// TestRuleHeadingIsNotAReference holds the line between a declaration and a
// reference. A heading with a foreign prefix is an error of form, and only
// that: there is nothing to resolve against the manifest, otherwise one typo
// would yield two findings about different things.
func TestRuleHeadingIsNotAReference(t *testing.T) {
f := base()
f["conventions/time.md"] = strings.Replace(baseTime, "### TIME-1.", "### GTIM-1.", 1)
for _, got := range run(t, f) {
if got.Family == check.Links {
t.Errorf("заголовок разобран как ссылка: %s", got.Msg)
t.Errorf("a heading was parsed as a reference: %s", got.Msg)
}
}
}
// rule собирает правило целиком, чтобы тесты не повторяли его форму.
// rule builds a whole rule so that tests do not repeat its form.
func rule(id, title, norm, rationale string) string {
return "\n### " + id + ". " + title + "\n\n**ДОЛЖЕН.** " + norm + "\n\n**ПОЧЕМУ.** " + rationale + "\n"
}
@@ -131,191 +138,191 @@ func TestChecks(t *testing.T) {
setup func(files)
want string
}{{
name: "префикс в шапке расходится с манифестом",
name: "front matter prefix diverges from the manifest",
setup: func(f files) {
f["conventions/time.md"] = strings.Replace(baseTime, "prefix: TIME", "prefix: GTIM", 1)
},
want: "префикс в шапке — GTIM, а манифест объявляет за этим файлом TIME",
want: "the front matter declares prefix GTIM, while the manifest assigns TIME to this file",
}, {
name: "заголовок правила несёт чужой префикс",
name: "rule heading carries a foreign prefix",
setup: func(f files) {
f["conventions/time.md"] = strings.Replace(baseTime, "### TIME-1.", "### GTIM-1.", 1)
},
want: "использует префикс GTIM, а файлу принадлежит TIME",
want: "the rule heading uses prefix GTIM, while the file owns TIME",
}, {
name: "нумерация с дырой",
name: "numbering has a gap",
setup: func(f files) {
f["conventions/time.md"] = baseTime + rule("TIME-3", "Третье", "Норма.", "Причина.")
},
want: "нумерация не сплошная",
want: "numbering is not contiguous",
}, {
name: "номер занят дважды",
name: "a number is taken twice",
setup: func(f files) {
f["conventions/time.md"] = baseTime + rule("TIME-1", "Ещё раз первое", "Норма.", "Причина.")
},
want: "номер TIME-1 занят дважды",
want: "number TIME-1 is taken twice",
}, {
name: "правило без обоснования",
name: "rule without a rationale",
setup: func(f files) {
f["conventions/time.md"] = strings.Replace(baseTime,
"**ПОЧЕМУ.** Без явного смещения не видно, в какой зоне запись сделана.", "", 1)
},
want: "нет блока ПОЧЕМУ: обоснование обязательно",
want: "has no ПОЧЕМУ block: the rationale is mandatory",
}, {
name: "правило без нормы и без заглушки",
name: "rule with neither a norm nor a stub",
setup: func(f files) {
f["conventions/time.md"] = strings.Replace(baseTime,
"**ДОЛЖЕН.** Момент времени записывается с суффиксом Z.", "Просто текст.", 1)
},
want: "нет ни блока нормы, ни заглушки СНЯТО",
want: "has neither a norm block nor a СНЯТО stub",
}, {
name: "две нормы под одним номером",
name: "two norms under one number",
setup: func(f files) {
f["conventions/time.md"] = strings.Replace(baseTime,
"**ПОЧЕМУ.**", "**СЛЕДУЕТ.** Вторая норма.\n\n**ПОЧЕМУ.**", 1)
},
want: "две нормы (ДОЛЖЕН и СЛЕДУЕТ)",
want: "holds two norms (ДОЛЖЕН and СЛЕДУЕТ)",
}, {
name: "обоснование стоит раньше нормы",
name: "rationale precedes the norm",
setup: func(f files) {
f["conventions/time.md"] = strings.Replace(baseTime,
"**ДОЛЖЕН.** Момент времени записывается с суффиксом Z.\n\n**ПОЧЕМУ.** Без явного смещения не видно, в какой зоне запись сделана.",
"**ПОЧЕМУ.** Причина вперёд.\n\n**ДОЛЖЕН.** Момент времени записывается с суффиксом Z.", 1)
},
want: "обоснование стоит раньше нормы",
want: "the rationale precedes the norm",
}, {
name: "примеры стоят раньше обоснования",
name: "examples precede the rationale",
setup: func(f files) {
f["conventions/time.md"] = strings.Replace(baseTime,
"**ПОЧЕМУ.**", "**ПРИМЕРЫ.** Иллюстрация.\n\n**ПОЧЕМУ.**", 1)
},
want: "блок ПРИМЕРЫ стоит раньше обоснования",
want: "the ПРИМЕРЫ block precedes the rationale",
}, {
name: "заглушка снятого без даты",
name: "stub of a retired rule without a date",
setup: func(f files) {
f["conventions/time.md"] = strings.Replace(baseTime,
"**ДОЛЖЕН.** Момент времени записывается с суффиксом Z.\n\n**ПОЧЕМУ.** Без явного смещения не видно, в какой зоне запись сделана.",
"**СНЯТО.** Правило убрано за ненадобностью.", 1)
},
want: "не несёт даты снятия",
want: "carries no date of retirement",
}, {
name: "у снятого правила осталась норма",
name: "retired rule still holds a norm",
setup: func(f files) {
f["conventions/time.md"] = baseTime +
"\n### TIME-2. Снятое\n\n**СНЯТО 2026-07-26.** Причина снятия.\n\n**ДОЛЖЕН.** Остаток нормы.\n"
},
want: "остался блок нормы",
want: "still holds a norm block",
}, {
name: "нет строки о версии языка",
name: "no language version line",
setup: func(f files) {
f["conventions/time.md"] = strings.Replace(baseTime, versionLine, "Просто вводная проза.", 1)
},
want: "нет строки о версии языка",
want: "holds no language version line",
}, {
name: "строка о версии называет чужую версию",
name: "version line names a foreign version",
setup: func(f files) {
f["conventions/time.md"] = strings.Replace(baseTime, "конвенций версии 1", "конвенций версии 2", 1)
},
want: "не называет версию 1",
want: "does not name version 1",
}, {
name: "модальное слово вне области правила",
name: "modal word outside a rule area",
setup: func(f files) {
f["conventions/time.md"] = strings.Replace(baseTime,
"Как приложение записывает моменты.", "Приложение ДОЛЖЕН писать моменты.", 1)
},
want: "стоит вне области правила",
want: "stands outside a rule area",
}, {
name: "слово чужого словаря",
name: "word of a foreign vocabulary",
setup: func(f files) {
f["conventions/time.md"] = strings.Replace(baseTime,
"**ПОЧЕМУ.** Без явного", "**ПОЧЕМУ.** Здесь MUST не к месту. Без явного", 1)
},
want: `слово MUST принадлежит словарю "en"`,
want: `the word MUST belongs to the "en" vocabulary`,
}, {
name: "ссылка на несуществующее правило",
name: "reference to a rule that does not exist",
setup: func(f files) {
f["conventions/time.md"] = strings.Replace(baseTime,
"Без явного смещения", "Смотри TIME-9. Без явного смещения", 1)
},
want: "ссылка TIME-9 не разрешается",
want: "reference TIME-9 does not resolve",
}, {
name: "ссылка на неизвестный префикс",
name: "reference to an unknown prefix",
setup: func(f files) {
f["conventions/time.md"] = strings.Replace(baseTime,
"Без явного смещения", "Смотри ZZZZ-1. Без явного смещения", 1)
},
want: "префикс ZZZZ, которого в манифесте набора нет",
want: "prefix ZZZZ, which the suite manifest does not declare",
}, {
name: "тема не объявлена в манифесте",
name: "topic not declared in the manifest",
setup: func(f files) {
f["conventions/time.md"] = strings.Replace(baseTime, "topic: time", "topic: clocks", 1)
},
want: `тема "clocks" не объявлена`,
want: `topic "clocks" is not declared`,
}, {
name: "тема значится среди выбывших",
name: "topic listed among the retired ones",
setup: func(f files) {
f["suite.toml"] = strings.Replace(baseManifest,
"[topics.retired]", `[topics.retired]`+"\ntime = \"снята 2026-07-01\"", 1)
},
want: "значится и среди живых, и среди выбывших",
want: "is listed both live and retired",
}, {
name: "у живой темы нет слоёв",
name: "live topic without layers",
setup: func(f files) {
f["suite.toml"] = strings.Replace(baseManifest,
`time = "время: хранение, зоны, форматы"`,
`time = "время"`+"\nlogging = \"логирование\"", 1)
},
want: `тема "logging" объявлена живой, а слоёв у неё в наборе нет`,
want: `topic "logging" is declared live while the suite holds no layer of it`,
}, {
name: "объявленного файла нет",
name: "declared file is missing",
setup: func(f files) {
f["suite.toml"] = strings.Replace(baseManifest,
`TIME = "conventions/time.md"`,
`TIME = "conventions/time.md"`+"\nSLOG = \"conventions/logging.md\"", 1)
},
want: `префикс SLOG объявлен за файлом "conventions/logging.md", а файла в наборе нет`,
want: `prefix SLOG is assigned to the file "conventions/logging.md", which the suite does not hold`,
}, {
name: "файл не зарегистрирован в манифесте",
name: "file not registered in the manifest",
setup: func(f files) {
f["conventions/logging.md"] = "---\ntopic: logging\nprefix: SLOG\n---\n\n# Логирование\n"
},
want: "в манифесте набора не объявлен",
want: "not declared in the suite manifest",
}, {
name: "префикс начинается на X",
name: "prefix starts with X",
setup: func(f files) {
f["suite.toml"] = strings.Replace(baseManifest,
`TIME = "conventions/time.md"`, `XTIM = "conventions/time.md"`, 1)
f["conventions/time.md"] = strings.ReplaceAll(baseTime, "TIME", "XTIM")
},
want: "зарезервирована за локальными правилами потребителей",
want: "reserved for the local rules of consumers",
}, {
name: "на один файл объявлено два префикса",
name: "one file with two prefixes declared",
setup: func(f files) {
f["suite.toml"] = strings.Replace(baseManifest,
`TIME = "conventions/time.md"`,
`TIME = "conventions/time.md"`+"\nGTIM = \"conventions/time.md\"", 1)
},
want: "объявлено несколько префиксов",
want: "has several prefixes declared for it",
}, {
name: "ключ манифеста неизвестен",
name: "unknown manifest key",
setup: func(f files) {
f["suite.toml"] = baseManifest + "\n[extra]\nkey = 1\n"
},
want: "инструменту неизвестен",
want: "is unknown to the tool",
}, {
name: "метка механизации в тексте конвенции",
name: "mechanization mark in convention text",
setup: func(f files) {
f["conventions/time.md"] = strings.Replace(baseTime,
"Без явного смещения", "Правило МЕХАНИЗИРОВАНО линтером. Без явного смещения", 1)
},
want: "её место — запись о механизации в локальной части копии",
want: "its place is the note of mechanization in the local part of the copy",
}, {
name: "путь канона в тексте конвенции",
name: "canon path in convention text",
setup: func(f files) {
f["conventions/time.md"] = strings.Replace(baseTime,
"Без явного смещения", "Смотри conventions/time.md. Без явного смещения", 1)
},
want: "стоит путь файла канона",
want: "the text holds the canon file path",
}}
for _, tc := range cases {
@@ -324,7 +331,7 @@ func TestChecks(t *testing.T) {
tc.setup(f)
got := messages(run(t, f))
if !strings.Contains(got, tc.want) {
t.Fatalf("проверка не сработала\nждали: %s\nполучили:\n%s", tc.want, got)
t.Fatalf("the check did not fire\nwanted: %s\ngot:\n%s", tc.want, got)
}
})
}