Files
convy/internal/lang/vocabulary.go
T
av b2d07ae55d комментарии и сообщения переведены на английский
- комментарии, тексты ошибок, вывод CLI и сообщения тестов теперь на английском
- по-русски остались только литералы словаря ru и содержимое фикстур: это
  данные под проверкой, а не текст инструмента
- согласование числительных в итоге упростилось до английского plural
2026-07-27 10:04:27 +03:00

287 lines
7.1 KiB
Go

// Package lang holds the vocabularies of the conventions language: the words
// that spell out a rule's modality, the marks of its blocks, and the
// connectives of a scenario block.
//
// A vocabulary belongs to a language version and to the suite's natural
// language, not to the suite itself: version 1 in Russian names the same words
// in every repository, and repeating that list in every manifest buys nothing.
// While the language specification lives together with the canon, the
// vocabularies live here; once it moves to its own repository with vocabulary
// files of its own, those become the source, and the Vocabulary type together
// with every check built on it stays as it is.
package lang
import (
"fmt"
"sort"
"strings"
"unicode"
"unicode/utf8"
)
// Level is a step on the scale of obligation. There are five steps in the four
// categories of ISO/IEC Directives, Part 2; which words name them is a
// parameter of the natural language, while the scale is one for all
// vocabularies.
type Level int
const (
Requirement Level = iota + 1
Prohibition
Recommendation
RecommendationAgainst
Permission
)
// String names the step for diagnostics — the role, not the vocabulary word.
func (l Level) String() string {
switch l {
case Requirement:
return "requirement"
case Prohibition:
return "prohibition"
case Recommendation:
return "recommendation"
case RecommendationAgainst:
return "recommendation against"
case Permission:
return "permission"
}
return "unknown level"
}
// Mark labels a block of a rule. Marks set no obligation, they only say what
// this is: a rationale, an illustration, a note about mechanization, a stub in
// place of a retired rule.
type Mark int
const (
Rationale Mark = iota + 1
Examples
Mechanized
Retired
)
func (m Mark) String() string {
switch m {
case Rationale:
return "rationale"
case Examples:
return "examples"
case Mechanized:
return "mechanized"
case Retired:
return "retired"
}
return "unknown mark"
}
// Connective is a service word of a scenario block. Such words stay out of the
// language version line and out of the "modal words outside rules" check: they
// set no obligation, only structure.
type Connective int
const (
When Connective = iota + 1
Then
And
Or
)
// Vocabulary is the vocabulary of one language version in one natural language.
type Vocabulary struct {
Version int
Code string
Modals map[string]Level
Marks map[string]Mark
Scenario map[string]Connective
}
// Modal reports the step of a word if the word belongs to this scale.
func (v Vocabulary) Modal(word string) (Level, bool) {
l, ok := v.Modals[word]
return l, ok
}
// Mark reports the role of a mark if the word belongs to these marks.
func (v Vocabulary) Mark(word string) (Mark, bool) {
m, ok := v.Marks[word]
return m, ok
}
// Word returns the word this vocabulary uses for a step.
func (v Vocabulary) Word(l Level) string {
for w, got := range v.Modals {
if got == l {
return w
}
}
return ""
}
// MarkWord returns the word this vocabulary uses for a mark.
func (v Vocabulary) MarkWord(m Mark) string {
for w, got := range v.Marks {
if got == m {
return w
}
}
return ""
}
// Lead returns the vocabulary word that opens text. The longest match wins:
// "MUST NOT" must not be read as "MUST", and a mark carrying a date
// ("RETIRED 2026-07-26") must not be read as a different mark.
func (v Vocabulary) Lead(text string) (string, bool) {
best := ""
for _, w := range v.Words() {
if len(w) <= len(best) {
continue
}
if !hasWordPrefix(text, w) {
continue
}
best = w
}
return best, best != ""
}
// Words lists every word the language names in its version line: the steps of
// the scale and the marks. Scenario connectives are not among them.
func (v Vocabulary) Words() []string {
words := make([]string, 0, len(v.Modals)+len(v.Marks))
for w := range v.Modals {
words = append(words, w)
}
for w := range v.Marks {
words = append(words, w)
}
sort.Strings(words)
return words
}
// hasWordPrefix reports whether text starts with word w and the word ends
// there: "MUSTARD" is not the word "MUST".
func hasWordPrefix(text, w string) bool {
if !strings.HasPrefix(text, w) {
return false
}
rest := text[len(w):]
if rest == "" {
return true
}
r, _ := utf8.DecodeRuneInString(rest)
return !unicode.IsLetter(r) && !unicode.IsDigit(r)
}
// registry holds the vocabularies known to the binary. The outer key is the
// language version, the inner one the code of the suite's natural language.
var registry = map[int]map[string]Vocabulary{
1: {
"ru": {
Version: 1,
Code: "ru",
Modals: map[string]Level{
"ДОЛЖЕН": Requirement,
"НЕ ДОЛЖЕН": Prohibition,
"СЛЕДУЕТ": Recommendation,
"НЕ СЛЕДУЕТ": RecommendationAgainst,
"ДОПУСКАЕТСЯ": Permission,
},
Marks: map[string]Mark{
"ПОЧЕМУ": Rationale,
"ПРИМЕРЫ": Examples,
"МЕХАНИЗИРОВАНО": Mechanized,
"СНЯТО": Retired,
},
Scenario: map[string]Connective{
"КОГДА": When,
"ТОГДА": Then,
"И": And,
"ИЛИ": Or,
},
},
"en": {
Version: 1,
Code: "en",
Modals: map[string]Level{
"MUST": Requirement,
"MUST NOT": Prohibition,
"SHOULD": Recommendation,
"SHOULD NOT": RecommendationAgainst,
"MAY": Permission,
},
Marks: map[string]Mark{
"WHY": Rationale,
"EXAMPLES": Examples,
"MECHANIZED": Mechanized,
"RETIRED": Retired,
},
Scenario: map[string]Connective{
"WHEN": When,
"THEN": Then,
"AND": And,
"OR": Or,
},
},
},
}
// Lookup returns the vocabulary of a language version in the given natural
// language.
func Lookup(version int, code string) (Vocabulary, error) {
byCode, ok := registry[version]
if !ok {
return Vocabulary{}, fmt.Errorf("language version %d is unknown to the tool; known versions: %s", version, versions())
}
v, ok := byCode[code]
if !ok {
return Vocabulary{}, fmt.Errorf("vocabulary %q of language version %d is unknown to the tool; known: %s", code, version, codes(version))
}
return v, nil
}
// Foreign lists the words of the other vocabularies of the same version — the
// ones that give away a mixture of vocabularies. Words that coincide with the
// suite's own are dropped.
func Foreign(version int, code string) map[string]string {
own, err := Lookup(version, code)
if err != nil {
return nil
}
mine := make(map[string]bool)
for _, w := range own.Words() {
mine[w] = true
}
foreign := make(map[string]string)
for otherCode, other := range registry[version] {
if otherCode == code {
continue
}
for _, w := range other.Words() {
if !mine[w] {
foreign[w] = otherCode
}
}
}
return foreign
}
func versions() string {
out := make([]string, 0, len(registry))
for v := range registry {
out = append(out, fmt.Sprint(v))
}
sort.Strings(out)
return strings.Join(out, ", ")
}
func codes(version int) string {
out := make([]string, 0, len(registry[version]))
for c := range registry[version] {
out = append(out, c)
}
sort.Strings(out)
return strings.Join(out, ", ")
}