Files

373 lines
15 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package site
import (
"encoding/json"
"os"
"path"
"path/filepath"
"regexp"
"strings"
)
// ruChrome is the site's own text in Russian, by its English text; page texts come from the export's translations (D348).
var ruChrome = map[string]string{
"Getting started": "Начало работы",
"Guides": "Руководства",
"Reference": "Справочник",
"Other": "Другое",
"Example": "Пример",
"See the": "Посмотрите",
"module example": "пример модуля",
"for this function in use.": "с этой функцией в работе.",
"One per scene.": "Один на сцену.",
"Usage": "Использование",
"Fields": "Поля",
"Data table": "Таблица данных",
"Runtime state": "Состояние во время игры",
"Values the component has while the game runs. They are not saved with the scene.": "Значения, которые есть у компонента во время игры. Они не сохраняются в сцене.",
"Name": "Имя",
"Type": "Тип",
"Default": "По умолчанию",
"Description": "Описание",
"Sound": "Звук",
"Try it": "Попробовать",
"Change the example parameters and compare the result. Reset restores the initial scene.": "Измените параметры примера и сравните результат. Сброс восстанавливает исходную сцену.",
"Starts the scene; open the example parameters to experiment.": "Запускает сцену; для эксперимента откройте параметры примера.",
"Runs this scene in the browser, with the Inspector next to it": "Запускает эту сцену в браузере, рядом с Инспектором",
"Script": "Скрипт",
"Script events": "События скриптов",
"Related guides": "Связанные руководства",
"On this page": "На этой странице",
"Previous": "Назад",
"Next": "Далее",
"Edit this page": "Править эту страницу",
"Edit the English page": "Править английскую страницу",
"Playground": "Песочница",
"Art Studio": "Art Studio",
"Art Studio lessons": "Уроки Art Studio",
"Draw your first sprite": "Первый спрайт",
"Frame-by-frame animation": "Покадровая анимация",
"Character animation": "Анимация персонажа",
"Explosion flipbook": "Покадровый взрыв",
"Emission and normal channels": "Emission и normal-каналы",
"Export and use in Aego": "Экспорт и использование в Aego",
"Functions": "Функции",
"Filter": "Найти среди",
"members of": "членов",
"Filter members": "Найти член",
"Methods": "Методы",
"Signals": "Сигналы",
"No example": "Без примера",
"Fix:": "Исправление:",
"Broken": "С ошибкой",
"Fixed": "Исправлено",
"Download": "Скачать",
"Docs": "Документация",
"Demo": "Демо",
"Source": "Исходники",
"Source code": "Исходный код",
"Search the docs": "Поиск по документации",
"Search": "Поиск",
"Navigation": "Навигация",
"Documentation": "Документация",
"Site": "Сайт",
"Theme": "Тема",
"No icon sources are available in this build.": "В этой сборке источники иконок недоступны.",
"Copy": "Копировать",
"Copied": "Скопировано",
"Run": "Запустить",
"Components": "Компоненты",
"Assets": "Ассеты",
"built": "собрано",
"Source on": "Исходники на",
"These docs as Markdown, for AI agents": "Эта документация в Markdown, для ИИ-агентов",
"During Play": "Во время игры",
"untranslated": "Эта страница ещё не переведена, ниже её английский текст.",
"partly": "Часть этой страницы ещё не переведена и показана по-английски.",
"Lexer": "Лексер",
"Parser": "Парсер",
"Types": "Типы",
"Builtin types": "Встроенные типы",
"Coroutines, signals and events": "Корутины, сигналы и события",
"Attributes": "Атрибуты",
"Packages and imports": "Пакеты и импорт",
"Engine bindings": "Привязки движка",
"Runtime faults": "Ошибки во время выполнения",
"Compiled code": "Скомпилированный код",
"Needs %s on the same node.": "Нужен %s на том же узле.",
"Add it with %s.": "Добавляется через %s.",
"Scripts on a node with this component receive these events from the %s module.": "Скрипты на узле с этим компонентом получают эти события от модуля «%s».",
"player setting": "настройка игрока",
"project default": "значение проекта",
"applies after a restart": "после перезапуска",
"asks to keep the change": "просит подтвердить",
"not saved": "не сохраняется",
"accessibility option": "доступность",
"control binding": "назначение управления",
"preset": "пресет",
"read-only": "только чтение",
"read-only in scripts": "в скриптах только чтение",
"scripts only": "только в скриптах",
"not in scripts": "нет в скриптах",
"hidden in the Inspector": "скрыто в Инспекторе",
"saved with the game": "сохраняется с игрой",
"live tuning": "настройка на лету",
"mask over %s": "маска по %s",
"gizmo: %s": "гизмо: %s",
"step %s": "шаг %s",
"%d constants and enum values are listed below.": "Ниже перечислены константы и значения перечислений: %d.",
"one of: %s": "одно из: %s",
"one of %s": "одно из %s",
"category %s": "категория %s",
"component": "компонент",
"page": "статья",
"value type": "структура данных",
"type": "тип",
"field": "поле",
"function": "функция",
"method": "метод",
"signal": "сигнал",
"const": "константа",
"enum": "перечисление",
"module": "модуль",
"code": "код",
"A live scene with": "Живая сцена с компонентом",
"edit its fields in the Inspector on the right and the scene restarts with them.": "меняйте его поля в Инспекторе справа, и сцена перезапустится с ними.",
"Open in editor": "Открыть в редакторе",
"Open in Aego": "Открыть в Aego",
"Download example project": "Скачать проект примера",
"Download scene only": "Скачать только сцену",
"Copy as .aescene": "Копировать как .aescene",
"Reset": "Сбросить",
"Builds for %s": "Сборки для %s",
"release notes and all files": "заметки о выпуске и все файлы",
"Installer": "Установщик",
"Portable": "Портативная",
"App": "Приложение",
"Archive": "Архив",
homeLead: "Игровой 2D-движок на Go.",
llmsNote: "Каждая страница ниже — Markdown; /ru/llms-full.txt содержит их все в одном файле.",
}
// tr is the site's own text in the builder's language.
func (b *builder) tr(en string) string {
if b.lang == "ru" {
if t, ok := ruChrome[en]; ok {
return t
}
}
return en
}
// forLang is a builder for lang: the same pages and URLs with every entry and group in that language where a translation exists.
func (b *builder) forLang(lang string) *builder {
rb := *b
rb.lang = lang
rb.en = b
rb.byID = make(map[string]*Entry, len(b.byID))
rb.has = map[string]bool{}
for id, e := range b.byID {
cp := *e
if id == "aego/builtin" {
cp.Title = rb.tr(cp.Title)
}
if t, ok := e.Translations[lang]; ok {
rb.has[id] = true
if t.Title != "" {
if cp.Field != nil {
f := *cp.Field
f.Label = t.Title
cp.Field = &f
} else {
cp.Title = t.Title
}
}
if t.Summary != "" {
cp.Summary = t.Summary
}
if t.Body != "" {
cp.Body = t.Body
}
if (t.Fix != "" || t.Why != "") && cp.Diag != nil {
d := *cp.Diag
if t.Fix != "" {
d.Fix = t.Fix
}
if t.Why != "" {
d.Why = t.Why
}
cp.Diag = &d
}
if t.Source != "" {
cp.Location.Source, cp.Location.Line = t.Source, t.Line
} else {
rb.has[id] = cp.Kind != "page" || t.Body != ""
}
}
rb.byID[id] = &cp
}
rb.groups = make([]navGroup, len(b.groups))
for i, g := range b.groups {
rb.groups[i] = g
if t := g.Titles[lang]; t != "" {
rb.groups[i].Title = t
} else {
rb.groups[i].Title = rb.tr(g.Title)
}
}
return &rb
}
var (
tagWithHref = regexp.MustCompile(`<[a-z]+\b[^>]*\bhref="[^"]*"[^>]*>`)
pageLink = regexp.MustCompile(`href="((?:\.\./)*)([^"#?:]*)([#?][^"]*)?"`)
)
// langLinks points the links of a page in the builder's language at the pages in that language; links marked hreflang="en" stay English.
func (b *builder) langLinks(page string) string {
pages := make(map[string]bool, len(b.urls)+1)
for _, u := range b.urls {
pages[u] = true
}
pages["index.html"] = true
prefix := b.lang + "/"
return tagWithHref.ReplaceAllStringFunc(page, func(tag string) string {
if strings.Contains(tag, `hreflang="en"`) {
return tag
}
return pageLink.ReplaceAllStringFunc(tag, func(m string) string {
sub := pageLink.FindStringSubmatch(m)
target := sub[2]
if target == "" || strings.HasPrefix(target, prefix) || !pages[target] && !pages[target+".html"] {
return m
}
return `href="` + sub[1] + prefix + target + sub[3] + `"`
})
})
}
// writeLang writes the pages of the builder's language under <lang>/, with their search index, Markdown copies and llms.txt.
func (b *builder) writeLang() error {
var search []searchItem
for _, id := range b.order {
e := b.byID[id]
rel := b.lang + "/" + b.urls[id]
content := b.content(e, rootOf(rel))
if b.has[id] && b.en != nil {
content = sameIDs(content, b.en.content(b.en.byID[id], rootOf(b.urls[id])))
}
if !b.has[id] && e.Kind == "page" {
content = `<p class="untranslated">` + esc(b.tr("untranslated")) + "</p>\n" + content
}
if err := b.emit(rel, e.Title, id, content, e); err != nil {
return err
}
}
for i := range b.exp.Entries {
id := b.exp.Entries[i].ID
u, ok := b.urls[id]
if !ok {
continue
}
e := b.byID[id]
item := b.searchEntry(e, cleanURL(b.lang+"/"+u))
if gi, ok := b.groupOf[id]; ok {
// Section is a stable search/ranking key; only its displayed label
// is translated by the page. Localizing the key loses its category.
item.Group, item.Section = b.groups[gi].Title, b.groups[gi].Section
}
search = append(search, item)
}
data, err := json.Marshal(search)
if err != nil {
return err
}
dir := filepath.Join(b.out, b.lang)
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(dir, "search.json"), data, 0o644); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(dir, "llms.txt"), []byte(b.llms()), 0o644); err != nil {
return err
}
return b.writeMarkdown()
}
// langPath is a site path in the builder's language.
func (b *builder) langPath(rel string) string {
if b.lang == "" || b.lang == "en" {
return rel
}
return path.Join(b.lang, rel)
}
// pageLang is the html lang of the builder's pages.
func (b *builder) pageLang() string {
if b.lang == "" {
return "en"
}
return b.lang
}
var headingID = regexp.MustCompile(`(<(?:h[1-6][^>]*|span class="legacy-anchor") id=")([^"]*)(")`)
// sameIDs gives translated prose headings the English ids, so links to a
// section work in both languages. Explicit ids already present in both pages
// must not be reassigned just because translated sections have moved.
func sameIDs(page, en string) string {
english := headingID.FindAllStringSubmatch(en, -1)
translated := headingID.FindAllStringSubmatch(page, -1)
if len(translated) != len(english) {
return page
}
enCount, translatedCount := map[string]int{}, map[string]int{}
for _, h := range english {
enCount[h[2]]++
}
for _, h := range translated {
translatedCount[h[2]]++
}
stable := map[string]bool{}
for id, count := range enCount {
stable[id] = count == 1 && translatedCount[id] == 1
}
var remaining []string
for _, h := range english {
if !stable[h[2]] {
remaining = append(remaining, h[2])
}
}
i := 0
return headingID.ReplaceAllStringFunc(page, func(m string) string {
sub := headingID.FindStringSubmatch(m)
if stable[sub[2]] {
return m
}
out := sub[1] + remaining[i] + sub[3]
i++
return out
})
}
// note is a field note in the builder's language.
func (b *builder) note(n string) string {
for _, p := range []string{"one of: ", "one of ", "category ", "mask over ", "gizmo: ", "step "} {
if rest, ok := strings.CutPrefix(n, p); ok {
return strings.Replace(b.tr(p+"%s"), "%s", rest, 1)
}
}
return b.tr(n)
}
// moduleName is a module's title in the builder's language, from its component page.
func (b *builder) moduleName(title string) string {
for _, e := range b.byID {
if e.Kind == "module" && strings.HasPrefix(e.ID, "module/") && b.en != nil && b.en.byID[e.ID].Title == title {
return e.Title
}
}
return title
}