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

- комментарии, тексты ошибок, вывод 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
+54 -52
View File
@@ -12,9 +12,9 @@ import (
"git.vakhrushev.me/av/convy/internal/suite"
)
// checkForm проверяет форму правила разбором текста. Применяется к любому
// файлу, который язык употребляет, — и к конвенциям, и к документу, которым
// набор ведёт себя сам.
// checkForm checks the form of a rule by parsing text. It applies to any file
// the language employs — both the conventions and the document the suite
// governs itself by.
func checkForm(s *suite.Suite, d *doc.Document, rep *Report) {
prefix := checkFilePrefix(s, d, rep)
checkHeadings(d, prefix, rep)
@@ -25,47 +25,47 @@ func checkForm(s *suite.Suite, d *doc.Document, rep *Report) {
checkForeignVocabulary(s, d, rep)
}
// checkFilePrefix сверяет префикс в шапке с манифестом и возвращает префикс,
// которым файлу положено пользоваться.
// checkFilePrefix reconciles the prefix in the front matter with the manifest
// and returns the prefix the file is supposed to use.
func checkFilePrefix(s *suite.Suite, d *doc.Document, rep *Report) string {
declared, _ := s.Manifest.PrefixOf(d.Path)
if !d.Front.Present {
rep.Errorf(Form, d.Path, 1, "у файла нет шапки, а манифест объявляет за ним префикс %s", declared)
rep.Errorf(Form, d.Path, 1, "the file has no front matter, while the manifest assigns prefix %s to it", declared)
return declared
}
if d.Front.Prefix == "" {
rep.Errorf(Form, d.Path, 1, "шапка не несёт ключа prefix")
rep.Errorf(Form, d.Path, 1, "the front matter carries no prefix key")
return declared
}
at := d.Front.At["prefix"]
if d.Front.Prefix != declared {
rep.Errorf(Form, d.Path, at,
"префикс в шапке — %s, а манифест объявляет за этим файлом %s", d.Front.Prefix, declared)
"the front matter declares prefix %s, while the manifest assigns %s to this file", d.Front.Prefix, declared)
}
if err := manifest.ValidPrefix(d.Front.Prefix); err != nil {
rep.Errorf(Form, d.Path, at, "%s", err)
}
if s.Manifest.PrefixRetired(d.Front.Prefix) {
rep.Errorf(Form, d.Path, at, "префикс %s значится среди выбывших", d.Front.Prefix)
rep.Errorf(Form, d.Path, at, "prefix %s is listed among the retired ones", d.Front.Prefix)
}
for _, key := range d.Front.Unknown {
rep.Warnf(Form, d.Path, d.Front.At[key], "ключ шапки %q инструменту неизвестен", key)
rep.Warnf(Form, d.Path, d.Front.At[key], "front matter key %q is unknown to the tool", key)
}
return declared
}
// checkHeadings проверяет форму заголовков правил: собственный префикс файла,
// третий уровень, точка после идентификатора, название.
// checkHeadings checks the form of rule headings: the file's own prefix, the
// third level, a period after the identifier, a title.
func checkHeadings(d *doc.Document, prefix string, rep *Report) {
for _, r := range d.Rules {
if r.Prefix != prefix {
rep.Errorf(Form, d.Path, r.Line,
"заголовок правила использует префикс %s, а файлу принадлежит %s", r.Prefix, prefix)
"the rule heading uses prefix %s, while the file owns %s", r.Prefix, prefix)
}
if r.HeadingLevel != 3 {
rep.Errorf(Form, d.Path, r.Line,
"заголовок правила %s стоит на уровне %d, а правило — заголовок третьего уровня", r.ID(), r.HeadingLevel)
"the heading of rule %s sits at level %d, while a rule is a third-level heading", r.ID(), r.HeadingLevel)
}
if r.Malformed != "" {
rep.Errorf(Form, d.Path, r.Line, "%s: %s", r.ID(), r.Malformed)
@@ -73,10 +73,10 @@ func checkHeadings(d *doc.Document, prefix string, rep *Report) {
}
}
// checkNumbering проверяет сплошную нумерацию: от единицы до наибольшего без
// пропусков и без повторов (META-31). Дыра неотличима от опечатки в номере и
// от правила, которое забыли дописать, — поэтому её нет никогда, а снятое
// правило остаётся заглушкой.
// checkNumbering checks that numbering is contiguous: from one up to the
// highest, with no gaps and no repeats (META-31). A gap is indistinguishable
// from a typo in a number and from a rule someone forgot to finish — which is
// why there is never one, and a retired rule stays as a stub.
func checkNumbering(d *doc.Document, prefix string, rep *Report) {
seen := make(map[int][]int)
for _, r := range d.Rules {
@@ -100,7 +100,7 @@ func checkNumbering(d *doc.Document, prefix string, rep *Report) {
for _, n := range nums {
if lines := seen[n]; len(lines) > 1 {
rep.Errorf(Form, d.Path, lines[1],
"номер %s занят дважды: строки %s", ruleID(prefix, n), joinInts(lines))
"number %s is taken twice: lines %s", ruleID(prefix, n), joinInts(lines))
}
}
var gaps []int
@@ -111,15 +111,15 @@ func checkNumbering(d *doc.Document, prefix string, rep *Report) {
}
if len(gaps) > 0 {
rep.Errorf(Form, d.Path, d.Rules[0].Line,
"нумерация не сплошная: наибольший номер %d, пропущены %s — снятое правило остаётся заглушкой, а не исчезает",
"numbering is not contiguous: the highest number is %d, missing %s — a retired rule stays as a stub instead of disappearing",
highest, joinInts(gaps))
}
}
var dateRe = regexp.MustCompile(`\d{4}-\d{2}-\d{2}`)
// checkRules проверяет состав правила: либо норма с обоснованием, либо
// заглушка снятого. Ни норма, ни обоснование не удаляются никогда
// checkRules checks what a rule is made of: either a norm with a rationale, or
// the stub of a retired one. Neither the norm nor the rationale is ever deleted
// (META-8, META-10).
func checkRules(s *suite.Suite, d *doc.Document, rep *Report) {
v := s.Vocab
@@ -133,25 +133,25 @@ func checkRules(s *suite.Suite, d *doc.Document, rep *Report) {
switch len(norms) {
case 0:
rep.Errorf(Form, d.Path, r.Line,
"у правила %s нет ни блока нормы, ни заглушки %s", r.ID(), v.MarkWord(lang.Retired))
"rule %s has neither a norm block nor a %s stub", r.ID(), v.MarkWord(lang.Retired))
case 1:
if norms[0].Rest == "" {
rep.Errorf(Form, d.Path, norms[0].Start,
"у правила %s метка %s не открывает нормы: за ней пусто", r.ID(), norms[0].Word)
"in rule %s the %s mark opens no norm: nothing follows it", r.ID(), norms[0].Word)
}
default:
rep.Errorf(Form, d.Path, norms[1].Start,
"у правила %s две нормы (%s и %s): норма — одна фраза, иначе нарушение одной её половины нечем адресовать",
"rule %s holds two norms (%s and %s): a norm is a single statement, otherwise a violation of one half of it has no address",
r.ID(), norms[0].Word, norms[1].Word)
}
rationale, ok := r.Block(lang.Rationale)
if !ok {
rep.Errorf(Form, d.Path, r.Line,
"у правила %s нет блока %s: обоснование обязательно", r.ID(), v.MarkWord(lang.Rationale))
"rule %s has no %s block: the rationale is mandatory", r.ID(), v.MarkWord(lang.Rationale))
} else if len(norms) > 0 && rationale.Start < norms[0].Start {
rep.Errorf(Form, d.Path, rationale.Start,
"у правила %s обоснование стоит раньше нормы: порядок блоков — норма, %s, %s",
"in rule %s the rationale precedes the norm: the order of blocks is norm, %s, %s",
r.ID(), v.MarkWord(lang.Rationale), v.MarkWord(lang.Examples))
}
@@ -159,57 +159,59 @@ func checkRules(s *suite.Suite, d *doc.Document, rep *Report) {
switch {
case len(r.Blocks) > 0 && r.Blocks[0].Start == examples.Start:
rep.Errorf(Form, d.Path, examples.Start,
"у правила %s блок %s открывает правило: порядок блоков — норма, %s, %s",
"in rule %s the %s block opens the rule: the order of blocks is norm, %s, %s",
r.ID(), v.MarkWord(lang.Examples), v.MarkWord(lang.Rationale), v.MarkWord(lang.Examples))
case ok && rationale.Start > examples.Start:
rep.Errorf(Form, d.Path, examples.Start,
"у правила %s блок %s стоит раньше обоснования: сначала требование, потом причина, потом иллюстрация",
"in rule %s the %s block precedes the rationale: the requirement first, then the reason, then the illustration",
r.ID(), v.MarkWord(lang.Examples))
}
}
}
}
// checkRetired проверяет заглушку снятого правила: дата и причина.
// checkRetired checks the stub of a retired rule: a date and a reason.
func checkRetired(d *doc.Document, r doc.Rule, retired doc.Block, rep *Report) {
if len(r.Norms()) > 0 {
rep.Errorf(Form, d.Path, retired.Start,
"у снятого правила %s остался блок нормы: норму с обоснованием заменяет заглушка", r.ID())
"retired rule %s still holds a norm block: the stub replaces the norm together with the rationale", r.ID())
}
if !dateRe.MatchString(d.Line(retired.Start)) {
rep.Errorf(Form, d.Path, retired.Start,
"заглушка правила %s не несёт даты снятия", r.ID())
"the stub of rule %s carries no date of retirement", r.ID())
}
if strings.TrimSpace(retired.Rest) == "" {
rep.Errorf(Form, d.Path, retired.Start,
"заглушка правила %s не несёт причины снятия", r.ID())
"the stub of rule %s carries no reason for retirement", r.ID())
}
}
// checkVersionLine ищет во вводной прозе строку о версии языка и возвращает
// границы абзаца, который её несёт.
// checkVersionLine looks for the language version line in the introductory
// prose and returns the bounds of the paragraph carrying it.
//
// Строка перечисляет ключевые слова набора и сама несёт правило заглавных —
// поэтому она единственное место вне правил, где модальные слова законны.
// The line lists the key words of the suite and carries the rule of capitals
// itself — which makes it the only place outside rules where modal words are
// lawful.
func checkVersionLine(s *suite.Suite, d *doc.Document, rep *Report) (from, to int) {
p, ok := versionParagraph(s, d)
if !ok {
start, _ := d.Preamble()
rep.Errorf(Form, d.Path, start,
"во вводной прозе нет строки о версии языка: она перечисляет ключевые слова набора и без неё конвенция в чужом репозитории теряет ключ к собственному тексту")
"the introductory prose holds no language version line: it lists the key words of the suite, and without it a convention in a foreign repository loses the key to its own text")
return 0, 0
}
version := strconv.Itoa(s.Manifest.Language.Version)
if !containsNumber(p.Text(), version) {
rep.Errorf(Form, d.Path, p.Start,
"строка о версии языка не называет версию %s, объявленную манифестом набора", version)
"the language version line does not name version %s declared by the suite manifest", version)
}
return p.Start, p.End
}
// versionParagraph ищет во вводной прозе абзац, несущий строку о версии языка:
// тот, где перечислены все ключевые слова набора. Ничего не сообщает — о его
// отсутствии говорит checkVersionLine, и говорить дважды незачем.
// versionParagraph looks in the introductory prose for the paragraph carrying
// the language version line: the one listing every key word of the suite. It
// reports nothing — checkVersionLine speaks about its absence, and speaking
// twice helps no one.
func versionParagraph(s *suite.Suite, d *doc.Document) (doc.Paragraph, bool) {
from, to := d.Preamble()
words := s.Vocab.Words()
@@ -221,9 +223,9 @@ func versionParagraph(s *suite.Suite, d *doc.Document) (doc.Paragraph, bool) {
return doc.Paragraph{}, false
}
// checkModalsOutside ищет заглавные модальные слова вне областей правил.
// Область — от заголовка правила до следующего заголовка; всё остальное проза,
// а проза нормой не является никогда.
// checkModalsOutside looks for capitalized modal words outside rule areas. An
// area runs from the heading of a rule to the next heading; everything else is
// prose, and prose is never a norm.
func checkModalsOutside(s *suite.Suite, d *doc.Document, versionFrom, versionTo int, rep *Report) {
words := modalWords(s.Vocab)
d.Prose(func(n int, text string) bool {
@@ -235,16 +237,16 @@ func checkModalsOutside(s *suite.Suite, d *doc.Document, versionFrom, versionTo
continue
}
rep.Errorf(Form, d.Path, n,
"модальное слово %s стоит вне области правила: заглавное написание нормативно, и в прозе его быть не может", w)
"the modal word %s stands outside a rule area: capitalized spelling is normative, and prose cannot hold it", w)
break
}
return true
})
}
// checkForeignVocabulary ищет слова чужого словаря той же версии языка.
// Словарь один на набор: две формы записи одного требования удваивают каждую
// проверку.
// checkForeignVocabulary looks for words of another vocabulary of the same
// language version. There is one vocabulary per suite: two ways of writing the
// same requirement double every check.
func checkForeignVocabulary(s *suite.Suite, d *doc.Document, rep *Report) {
foreign := lang.Foreign(s.Manifest.Language.Version, s.Manifest.Language.Lang)
if len(foreign) == 0 {
@@ -260,7 +262,7 @@ func checkForeignVocabulary(s *suite.Suite, d *doc.Document, rep *Report) {
for _, w := range words {
if containsWord(text, w) {
rep.Errorf(Form, d.Path, n,
"слово %s принадлежит словарю %q, а набор объявляет словарь %q",
"the word %s belongs to the %q vocabulary, while the suite declares %q",
w, foreign[w], s.Manifest.Language.Lang)
}
}
@@ -286,8 +288,8 @@ func containsAll(text string, words []string) bool {
return true
}
// containsWord ищет слово как целое: «ДОЛЖЕНСТВОВАНИЕ» словом ДОЛЖЕН не
// является, а «ДОЛЖЕН.» — является.
// containsWord looks for a word as a whole: "MUSTARD" is not the word "MUST",
// while "MUST." is.
func containsWord(text, word string) bool {
for i := 0; ; {
j := strings.Index(text[i:], word)