ci / release (push) Skipped
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
GitHub / darwin amd64 native GitHub-hosted native CI: success
GitHub / windows amd64 native GitHub-hosted native CI: failure
GitHub / darwin arm64 native GitHub-hosted native CI: success
GitHub / linux amd64 native GitHub-hosted native CI: success
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 18m33s
ci / checks (push) Failing after 59m31s
269 lines
8.7 KiB
Go
269 lines
8.7 KiB
Go
//go:build !aego_runtime
|
|
|
|
package docs
|
|
|
|
import (
|
|
"io/fs"
|
|
"path"
|
|
"regexp"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"aego/engine/module"
|
|
)
|
|
|
|
// Translation is an entry in another language: a page from its name.<lang>.md file, or a reference entry from the catalogs (D348).
|
|
type Translation struct {
|
|
Title string `json:",omitempty"`
|
|
Summary string `json:",omitempty"`
|
|
Body string `json:",omitempty"`
|
|
// Fix is a diagnostic's fix sentence; Why says why a code has no example.
|
|
Fix string `json:",omitempty"`
|
|
Why string `json:",omitempty"`
|
|
// Source is the translated file, for the site's edit link; empty when the text comes from a catalog.
|
|
Source string `json:",omitempty"`
|
|
Line int `json:",omitempty"`
|
|
}
|
|
|
|
var translationFile = regexp.MustCompile(`\.([a-z]{2})\.md$`)
|
|
|
|
// translationLang is the language of a file named name.<lang>.md, or "".
|
|
func translationLang(file string) string {
|
|
if m := translationFile.FindStringSubmatch(file); m != nil {
|
|
return m[1]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// translations reads the name.<lang>.md files of a source into the entries they translate.
|
|
func (r *Registry) translations(src module.DocSource, files []string) {
|
|
for _, f := range files {
|
|
lang := translationLang(f)
|
|
data, err := fs.ReadFile(src.FS, f)
|
|
if err != nil {
|
|
r.problems = append(r.problems, Problem{ID: src.Owner + ":" + f, Error: true, Message: err.Error()})
|
|
continue
|
|
}
|
|
recs, err := parseRecords(string(data))
|
|
if err != nil {
|
|
r.problems = append(r.problems, Problem{ID: src.Owner + ":" + f, Error: true, Message: err.Error()})
|
|
continue
|
|
}
|
|
source := ""
|
|
if src.Dir != "" {
|
|
source = path.Join(src.Dir, f)
|
|
}
|
|
for _, rec := range recs {
|
|
id := rec.id
|
|
if rec.code != "" {
|
|
id = rec.code
|
|
}
|
|
i, ok := r.byID[id]
|
|
if !ok {
|
|
r.problems = append(r.problems, Problem{ID: src.Owner + ":" + f, Error: true, Message: "translation of " + id + ", which no page declares"})
|
|
continue
|
|
}
|
|
e := &r.entries[i]
|
|
// Search aliases are shared, just like public component labels: an
|
|
// English UI name must also find the translated article and vice versa.
|
|
e.SearchNames = append(e.SearchNames, rec.searchNames...)
|
|
if e.Translations == nil {
|
|
e.Translations = map[string]Translation{}
|
|
}
|
|
t := e.Translations[lang]
|
|
t.Body = joinBody(t.Body, rec.body)
|
|
if rec.title != "" && e.Kind == Code {
|
|
t.Summary = rec.title
|
|
} else if rec.title != "" {
|
|
t.Title = rec.title
|
|
} else if t.Title == "" {
|
|
t.Title = heading(rec.body)
|
|
}
|
|
if rec.summary != "" {
|
|
t.Summary = rec.summary
|
|
} else if t.Summary == "" && e.Kind == Page {
|
|
t.Summary = firstSentence(summary(rec.body))
|
|
}
|
|
if t.Source == "" {
|
|
t.Source, t.Line = source, rec.line
|
|
}
|
|
e.Translations[lang] = t
|
|
if rec.groupTitle != "" {
|
|
r.groupTitles(rec.group, lang, rec.groupTitle)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *Registry) groupTitles(id, lang, title string) {
|
|
if r.groupT == nil {
|
|
r.groupT = map[string]map[string]string{}
|
|
}
|
|
if r.groupT[id] == nil {
|
|
r.groupT[id] = map[string]string{}
|
|
}
|
|
r.groupT[id][lang] = title
|
|
}
|
|
|
|
// Localize fills the reference entries of lang from the catalogs through tr: components, fields, script functions, settings, script diagnostics and the builtin groups. Pages keep their file translations.
|
|
func (r *Registry) Localize(lang string, tr func(key, en string) string) {
|
|
set := func(e *Entry, t Translation) {
|
|
if t == (Translation{}) {
|
|
return
|
|
}
|
|
if t.Summary == "" && t.Body != "" {
|
|
t.Summary = summary(t.Body)
|
|
}
|
|
if e.Translations == nil {
|
|
e.Translations = map[string]Translation{}
|
|
}
|
|
if f, file := e.Translations[lang]; file {
|
|
if f.Title == "" {
|
|
f.Title = t.Title
|
|
}
|
|
if e.Kind == Type {
|
|
// A value type may be documented entirely by a localized file.
|
|
// Its catalog entry then translates the same body, not a second
|
|
// paragraph to prepend to the file's existing translation.
|
|
if strings.TrimSpace(t.Body) != strings.TrimSpace(f.Body) {
|
|
f.Body = joinBody(t.Body, f.Body)
|
|
}
|
|
if f.Summary == "" {
|
|
f.Summary = t.Summary
|
|
}
|
|
}
|
|
e.Translations[lang] = f
|
|
return
|
|
}
|
|
e.Translations[lang] = t
|
|
}
|
|
keep := func(key, en string) string {
|
|
if en == "" {
|
|
return ""
|
|
}
|
|
if t := tr(key, en); t != en {
|
|
return t
|
|
}
|
|
return ""
|
|
}
|
|
for i := range r.entries {
|
|
e := &r.entries[i]
|
|
switch {
|
|
case e.Kind == Type && e.Component != nil:
|
|
set(e, Translation{Title: keep("schema."+e.ID, e.Title), Body: keep("schema."+e.ID+".doc", e.Body)})
|
|
case strings.HasPrefix(e.ID, SettingPrefix):
|
|
id := strings.TrimPrefix(e.ID, SettingPrefix)
|
|
set(e, Translation{Title: keep("setting."+id, e.Title), Body: keep("setting."+id+".doc", e.Body)})
|
|
case e.Kind == Field && e.Field != nil && strings.HasPrefix(e.Parent, SectionsPrefix) && !strings.HasPrefix(e.ID, SettingPrefix):
|
|
set(e, Translation{Body: keep("docs.field."+e.ID, e.Body)})
|
|
case e.Kind == Field && e.Field != nil:
|
|
key := "schema." + e.Parent + "." + lowerFirst(e.Title)
|
|
set(e, Translation{Title: keep(key, e.Field.Label), Body: keep(key+".doc", e.Body)})
|
|
case e.Kind == Code && e.Diag != nil && e.Diag.Fix != "":
|
|
what, fix := keep("diag."+e.ID, e.Summary), keep("diag."+e.ID+".fix", e.Diag.Fix)
|
|
if what != "" {
|
|
set(e, Translation{Summary: what, Body: what + "\n\n" + tr("docs.fix", "Fix:") + " " + fix, Fix: fix, Why: keep("diag."+e.ID+".why", e.Diag.Why)})
|
|
}
|
|
case e.Kind == Type && e.Component == nil:
|
|
set(e, Translation{Body: keep(APIKey(e.ID), e.Body)})
|
|
case e.Kind == Page && e.Location.Source == "":
|
|
lead, rest, _ := strings.Cut(e.Body, "\n\n")
|
|
if t := keep("docs.page."+e.ID+".doc", lead); t != "" {
|
|
if rest != "" {
|
|
t += "\n\n" + strings.Replace(rest, "## Default block", "## "+tr("docs.default_block", "Default block"), 1)
|
|
}
|
|
set(e, Translation{Title: keep("docs.page."+e.ID, e.Title), Body: t})
|
|
}
|
|
case e.Kind == Module && strings.HasPrefix(e.ID, ModulePrefix):
|
|
if t := keep("docs.module."+e.Owner, e.Title); t != "" {
|
|
set(e, Translation{Title: t, Body: strings.Replace(tr("docs.module", "Components of the %s module."), "%s", t, 1)})
|
|
}
|
|
case e.Kind == Function || e.Kind == Method || e.Kind == Signal || e.Kind == Const || e.Kind == Enum:
|
|
set(e, Translation{Body: keep(APIKey(e.ID), e.Body)})
|
|
}
|
|
}
|
|
for _, g := range groups {
|
|
if t := keep("docs.group."+g.ID, g.Title); t != "" {
|
|
r.groupTitles(g.ID, lang, t)
|
|
}
|
|
}
|
|
}
|
|
|
|
func lowerFirst(s string) string {
|
|
r, n := utf8.DecodeRuneInString(s)
|
|
return string(unicode.ToLower(r)) + s[n:]
|
|
}
|
|
|
|
var refTarget = regexp.MustCompile(`\]\(ref:([^)\s#]+)`)
|
|
|
|
// LangProblems checks the translations into lang: every page has one, and each keeps the code blocks, the headings and valid ref: links of its original (D348).
|
|
func (r *Registry) LangProblems(lang string, strict bool) []Problem {
|
|
var out []Problem
|
|
for i := range r.entries {
|
|
e := &r.entries[i]
|
|
if (e.Kind != Page && e.Kind != Code) || e.Location.Source == "" || e.EditorOnly {
|
|
continue
|
|
}
|
|
if e.Kind == Code && e.Diag != nil && e.Diag.Fix != "" {
|
|
continue
|
|
}
|
|
t, ok := e.Translations[lang]
|
|
if !ok || t.Source == "" {
|
|
out = append(out, Problem{ID: e.ID, Error: strict, Message: "no " + lang + " translation (" + translationName(e.Location.Source, lang) + ")"})
|
|
continue
|
|
}
|
|
at := t.Source
|
|
if a, b := codeBlocks(e.Body), codeBlocks(t.Body); !slices.Equal(a, b) {
|
|
out = append(out, Problem{ID: e.ID, Error: true, Message: at + ": the code blocks differ from the English page (" + strconv.Itoa(len(b)) + " of " + strconv.Itoa(len(a)) + " match in order); copy them unchanged"})
|
|
}
|
|
if a, b := headingCount(e.Body), headingCount(t.Body); a != b {
|
|
out = append(out, Problem{ID: e.ID, Error: true, Message: at + ": " + strconv.Itoa(b) + " headings, the English page has " + strconv.Itoa(a)})
|
|
}
|
|
for _, m := range refTarget.FindAllStringSubmatch(t.Body, -1) {
|
|
if _, ok := r.Link(m[1]); !ok {
|
|
if _, ok := r.byID[m[1]]; !ok {
|
|
out = append(out, Problem{ID: e.ID, Error: true, Message: at + ": broken link ref:" + m[1]})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func translationName(source, lang string) string {
|
|
return strings.TrimSuffix(source, ".md") + "." + lang + ".md"
|
|
}
|
|
|
|
func codeBlocks(md string) []string {
|
|
var out []string
|
|
for _, s := range snippets(md) {
|
|
out = append(out, s.Lang+"\n"+s.Code)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func headingCount(md string) int {
|
|
n, fence := 0, false
|
|
for _, l := range strings.Split(md, "\n") {
|
|
if isFence(l) {
|
|
fence = !fence
|
|
continue
|
|
}
|
|
if !fence && strings.HasPrefix(l, "#") {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
// APIKey is the catalog key of a reference entry; a method named like a field (Mass()) gets .method instead of the parentheses.
|
|
func APIKey(id string) string {
|
|
if base, ok := strings.CutSuffix(id, "()"); ok {
|
|
return "api." + base + ".method"
|
|
}
|
|
return "api." + id
|
|
}
|