aboutsummaryrefslogtreecommitdiff
path: root/internal/web
diff options
context:
space:
mode:
Diffstat (limited to 'internal/web')
-rw-r--r--internal/web/render.go31
-rw-r--r--internal/web/render_test.go30
-rw-r--r--internal/web/server.go14
-rw-r--r--internal/web/server_test.go11
-rw-r--r--internal/web/templates/index.html24
5 files changed, 72 insertions, 38 deletions
diff --git a/internal/web/render.go b/internal/web/render.go
index 63c1da4..ca5bb89 100644
--- a/internal/web/render.go
+++ b/internal/web/render.go
@@ -95,15 +95,17 @@ type ilLineView struct {
// In every mode, heading, citation (subtitle), verse-number and refrain
// text are wrapped in class="heading|citation|vnum|refrain|version-label"
// spans so theme CSS can restyle them; verse/paragraph text is escaped by
-// html/template.
-func RenderReadings(secs []liturgy.Section, versions []string, lectionary, display string) template.HTML {
+// html/template. lang localises the version-column labels and each section
+// heading's part-label word (render.LocalizeHeading, brief §3b); the
+// citation/verse text is never touched.
+func RenderReadings(secs []liturgy.Section, versions []string, lectionary, display, lang string) template.HTML {
switch display {
case "vertical":
- return renderTemplate("readings-vertical.html", buildColumnViews(secs, versions, lectionary))
+ return renderTemplate("readings-vertical.html", buildColumnViews(secs, versions, lectionary, lang))
case "interlinear":
- return renderTemplate("readings-interlinear.html", buildInterlinearViews(secs, versions, lectionary))
+ return renderTemplate("readings-interlinear.html", buildInterlinearViews(secs, versions, lectionary, lang))
default:
- return renderTemplate("readings.html", buildColumnViews(secs, versions, lectionary))
+ return renderTemplate("readings.html", buildColumnViews(secs, versions, lectionary, lang))
}
}
@@ -122,15 +124,17 @@ func renderTemplate(name string, data any) template.HTML {
// buildColumnViews gathers each section's per-version columns via
// render.GatherVersion -- the shared data both the "horizontal"
// (readings.html) and "vertical" (readings-vertical.html) templates range
-// over; only the surrounding markup differs between the two layouts.
-func buildColumnViews(secs []liturgy.Section, versions []string, lectionary string) []sectionView {
+// over; only the surrounding markup differs between the two layouts. lang
+// localises the column labels and the heading's part-label word (see
+// RenderReadings).
+func buildColumnViews(secs []liturgy.Section, versions []string, lectionary, lang string) []sectionView {
views := make([]sectionView, 0, len(secs))
for _, sec := range secs {
isPsalm := sec.PartID == "psalm"
cols := make([]columnView, 0, len(versions))
for _, v := range versions {
- label, blocks := render.GatherVersion(v, sec, lectionary)
+ label, blocks := render.GatherVersion(v, sec, lectionary, lang)
bviews := make([]blockView, 0, len(blocks))
for bi, b := range blocks {
@@ -148,7 +152,7 @@ func buildColumnViews(secs []liturgy.Section, versions []string, lectionary stri
}
views = append(views, sectionView{
- Heading: sec.Heading,
+ Heading: render.LocalizeHeading(sec.Heading, sec.PartID, lang),
Subtitle: sec.Subtitle,
PartID: sec.PartID,
Columns: cols,
@@ -175,8 +179,9 @@ func interlinearVersions(versions []string) []string {
// that version's order (stable, no duplicates). A version that comes back
// unversified (a bible lookup miss) is simply skipped -- the remaining
// versions still render. A section where nothing could be interleaved gets
-// a short escaped Note instead of an empty Verses list.
-func buildInterlinearViews(secs []liturgy.Section, versions []string, lectionary string) []ilSectionView {
+// a short escaped Note instead of an empty Verses list. lang localises the
+// per-version labels and the heading's part-label word (see RenderReadings).
+func buildInterlinearViews(secs []liturgy.Section, versions []string, lectionary, lang string) []ilSectionView {
mapped := interlinearVersions(versions)
type verseSet struct {
@@ -186,11 +191,11 @@ func buildInterlinearViews(secs []liturgy.Section, versions []string, lectionary
views := make([]ilSectionView, 0, len(secs))
for _, sec := range secs {
- view := ilSectionView{Heading: sec.Heading, Subtitle: sec.Subtitle, PartID: sec.PartID}
+ view := ilSectionView{Heading: render.LocalizeHeading(sec.Heading, sec.PartID, lang), Subtitle: sec.Subtitle, PartID: sec.PartID}
var sets []verseSet
for _, v := range mapped {
- label, verses, versified := render.GatherVerses(v, sec, lectionary)
+ label, verses, versified := render.GatherVerses(v, sec, lectionary, lang)
if !versified {
continue
}
diff --git a/internal/web/render_test.go b/internal/web/render_test.go
index 0e64168..ea44ed2 100644
--- a/internal/web/render_test.go
+++ b/internal/web/render_test.go
@@ -11,7 +11,7 @@ import (
func TestRenderReadings(t *testing.T) {
secs := []liturgy.Section{{Heading: "Ewangelia (J 20, 1. 11-18)", PartID: "ewangelia"}}
- html := string(RenderReadings(secs, []string{"wuj"}, "new", "horizontal"))
+ html := string(RenderReadings(secs, []string{"wuj"}, "new", "horizontal", "pl"))
if !strings.Contains(html, "Ewangelia") || !strings.Contains(html, "class=") {
t.Errorf("reading pane missing heading/classes: %q", html[:min(200, len(html))])
}
@@ -45,7 +45,7 @@ func TestRenderReadingsEscapesScriptText(t *testing.T) {
PartID: "pierwsze_czytanie",
Paragraphs: [][]string{{"<script>alert(1)</script>"}},
}}
- html := string(RenderReadings(secs, []string{"pl"}, "new", "horizontal"))
+ html := string(RenderReadings(secs, []string{"pl"}, "new", "horizontal", "pl"))
if strings.Contains(html, "<script>alert(1)</script>") {
t.Errorf("raw <script> leaked into rendered output: %q", html)
}
@@ -56,7 +56,7 @@ func TestRenderReadingsEscapesScriptText(t *testing.T) {
func TestRenderReadingsVertical(t *testing.T) {
secs := []liturgy.Section{{Heading: "Ewangelia (J 20, 1. 11-18)", PartID: "ewangelia"}}
- html := string(RenderReadings(secs, []string{"wuj", "vul"}, "new", "vertical"))
+ html := string(RenderReadings(secs, []string{"wuj", "vul"}, "new", "vertical", "pl"))
if !strings.Contains(html, "display-vertical") {
t.Errorf("vertical output missing display-vertical container: %q", html[:min(300, len(html))])
}
@@ -70,7 +70,7 @@ func TestRenderReadingsVertical(t *testing.T) {
func TestRenderReadingsInterlinear(t *testing.T) {
secs := []liturgy.Section{{Heading: "Ewangelia (J 20, 1. 11-18)", PartID: "ewangelia"}}
- html := string(RenderReadings(secs, []string{"wuj", "vul"}, "new", "interlinear"))
+ html := string(RenderReadings(secs, []string{"wuj", "vul"}, "new", "interlinear", "pl"))
if !strings.Contains(html, "ilverse") {
t.Errorf("interlinear output missing ilverse: %q", html[:min(300, len(html))])
}
@@ -91,7 +91,7 @@ func TestRenderReadingsInterlinear(t *testing.T) {
func TestRenderReadingsInterlinearExcludesPL(t *testing.T) {
secs := []liturgy.Section{{Heading: "Ewangelia (J 20, 1. 11-18)", PartID: "ewangelia"}}
- html := string(RenderReadings(secs, []string{"pl", "vul"}, "new", "interlinear"))
+ html := string(RenderReadings(secs, []string{"pl", "vul"}, "new", "interlinear", "pl"))
if strings.Contains(html, "Polski (niedziela.pl)") {
t.Errorf("interlinear output should substitute wuj for pl, not carry pl's label: %q", html[:min(300, len(html))])
}
@@ -102,12 +102,30 @@ func TestRenderReadingsInterlinearExcludesPL(t *testing.T) {
func TestRenderReadingsInterlinearNoVersifiedNote(t *testing.T) {
secs := []liturgy.Section{{Heading: "Bez odwołania", PartID: "ewangelia"}}
- html := string(RenderReadings(secs, []string{"wuj"}, "new", "interlinear"))
+ html := string(RenderReadings(secs, []string{"wuj"}, "new", "interlinear", "pl"))
if strings.Contains(html, "ilverse") {
t.Errorf("expected no ilverse blocks when nothing resolves: %q", html)
}
}
+// TestRenderReadingsLocalizesEN checks that lang="en" localises both the
+// version-column label (via render.GatherVersion) and the section heading's
+// part-label word (via render.LocalizeHeading, brief §3b), while the
+// citation stays exactly as scraped.
+func TestRenderReadingsLocalizesEN(t *testing.T) {
+ secs := []liturgy.Section{{Heading: "Ewangelia (J 20, 1. 11-18)", PartID: "ewangelia"}}
+ html := string(RenderReadings(secs, []string{"vul"}, "new", "horizontal", "en"))
+ if !strings.Contains(html, "Gospel (J 20, 1. 11-18)") {
+ t.Errorf("en output missing localised heading: %q", html[:min(300, len(html))])
+ }
+ if !strings.Contains(html, "Vulgate (Latin)") {
+ t.Errorf("en output missing localised version label: %q", html[:min(300, len(html))])
+ }
+ if strings.Contains(html, "Ewangelia") {
+ t.Errorf("en output should not carry the Polish heading label: %q", html[:min(300, len(html))])
+ }
+}
+
func TestUserTheme(t *testing.T) {
dir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", dir)
diff --git a/internal/web/server.go b/internal/web/server.go
index 10401ed..3b7102c 100644
--- a/internal/web/server.go
+++ b/internal/web/server.go
@@ -19,6 +19,7 @@ import (
"time"
"github.com/lukaszkasprzak/lectio/internal/config"
+ "github.com/lukaszkasprzak/lectio/internal/i18n"
"github.com/lukaszkasprzak/lectio/internal/liturgy"
"github.com/lukaszkasprzak/lectio/internal/readings"
"github.com/lukaszkasprzak/lectio/internal/render"
@@ -183,6 +184,10 @@ type indexData struct {
Display string
Mono bool
Reading template.HTML
+ // L holds the localised control labels (lectionary/layout/theme/...),
+ // set from i18n.Get(cfg.UILanguage) -- index.html references its
+ // fields (e.g. {{.L.Lectionary}}) instead of hardcoded Polish text.
+ L i18n.UI
}
type versionOpt struct {
@@ -207,7 +212,7 @@ func indexHandler(cfg config.Config) http.HandlerFunc {
}
secs, loadVersions, err := loadSections(cfg, lectionary, date, all, versions)
- reading := renderOrError(secs, loadVersions, lectionary, display, err)
+ reading := renderOrError(secs, loadVersions, lectionary, display, cfg.UILanguage, err)
// Check the boxes for the versions actually rendered (loadVersions),
// not the raw request: traditional/offline substitute pl->wuj, so the
@@ -239,6 +244,7 @@ func indexHandler(cfg config.Config) http.HandlerFunc {
Display: display,
Mono: queryBool(r, "mono", cfg.WebMono),
Reading: reading,
+ L: i18n.Get(cfg.UILanguage),
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -255,7 +261,7 @@ func readingsHandler(cfg config.Config) http.HandlerFunc {
date, lectionary, all, versions, display := resolveQuery(cfg, r)
secs, loadVersions, err := loadSections(cfg, lectionary, date, all, versions)
- reading := renderOrError(secs, loadVersions, lectionary, display, err)
+ reading := renderOrError(secs, loadVersions, lectionary, display, cfg.UILanguage, err)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
io.WriteString(w, string(reading))
@@ -266,14 +272,14 @@ func readingsHandler(cfg config.Config) http.HandlerFunc {
// error) a small escaped error paragraph -- readings.Load errors are
// expected in normal operation (an unpublished date, no network while
// online, ...) so the pane should show them, not 500.
-func renderOrError(secs []liturgy.Section, versions []string, lectionary, display string, err error) template.HTML {
+func renderOrError(secs []liturgy.Section, versions []string, lectionary, display, lang string, err error) template.HTML {
if err != nil {
return template.HTML(`<p class="error">` + template.HTMLEscapeString(err.Error()) + `</p>`)
}
if len(secs) == 0 {
return template.HTML(`<p class="error">brak czytań na ten dzień</p>`)
}
- return RenderReadings(secs, versions, lectionary, display)
+ return RenderReadings(secs, versions, lectionary, display, lang)
}
// themeCSSHandler serves one theme's stylesheet: the requested name, or
diff --git a/internal/web/server_test.go b/internal/web/server_test.go
index 47c38e9..25d21f4 100644
--- a/internal/web/server_test.go
+++ b/internal/web/server_test.go
@@ -38,7 +38,10 @@ func TestServer(t *testing.T) {
t.Fatalf("status = %d, want 200", rec.Code)
}
body := rec.Body.String()
- if !strings.Contains(body, "Ewangelia") {
+ // config.Default() -> UILanguage "en", so the gospel heading's part
+ // label is localised to "Gospel" (render.LocalizeHeading); the
+ // citation stays exactly as scraped.
+ if !strings.Contains(body, "Gospel") {
t.Errorf("body missing reading heading: %q", body)
}
if !strings.Contains(body, "htmx") {
@@ -59,7 +62,8 @@ func TestServer(t *testing.T) {
if strings.Contains(body, "<html") {
t.Errorf("partial is not a fragment: %q", body)
}
- if !strings.Contains(body, "Ewangelia") {
+ // See "index page" above: config.Default() is English chrome.
+ if !strings.Contains(body, "Gospel") {
t.Errorf("partial missing reading heading: %q", body)
}
})
@@ -118,7 +122,8 @@ func TestServer(t *testing.T) {
if j := strings.Index(body[i+1:], `class="vnum"`); j != -1 {
block = body[i : i+1+j]
}
- if !strings.Contains(block, "Wujek") || !strings.Contains(block, "Wulgata") {
+ // config.Default() is English chrome: "Wujek (Polish)"/"Vulgate (Latin)".
+ if !strings.Contains(block, "Wujek") || !strings.Contains(block, "Vulgate") {
t.Errorf("interlinear verse block missing both version labels grouped together: %q", block)
}
})
diff --git a/internal/web/templates/index.html b/internal/web/templates/index.html
index a1247e6..8c9fd86 100644
--- a/internal/web/templates/index.html
+++ b/internal/web/templates/index.html
@@ -32,10 +32,10 @@
hx-vals='{"date":"{{.NextDate}}"}'>&rarr;</button>
</span>
- <label>lekcjonarz
+ <label>{{.L.Lectionary}}
<select name="lectionary">
- <option value="new" {{if eq .Lectionary "new"}}selected{{end}}>nowy</option>
- <option value="traditional" {{if eq .Lectionary "traditional"}}selected{{end}}>tradycyjny</option>
+ <option value="new" {{if eq .Lectionary "new"}}selected{{end}}>{{.L.OptModern}}</option>
+ <option value="traditional" {{if eq .Lectionary "traditional"}}selected{{end}}>{{.L.OptTraditional}}</option>
</select>
</label>
@@ -43,23 +43,23 @@
<label><input type="checkbox" name="v" value="{{.Code}}" {{if .Checked}}checked{{end}}> {{.Code}}</label>
{{end}}
- <label>zakres
+ <label>{{.L.Parts}}
<select name="all">
- <option value="0" {{if not .All}}selected{{end}}>Ewangelia</option>
- <option value="1" {{if .All}}selected{{end}}>wszystkie części</option>
+ <option value="0" {{if not .All}}selected{{end}}>{{.L.OptGospel}}</option>
+ <option value="1" {{if .All}}selected{{end}}>{{.L.OptAll}}</option>
</select>
</label>
- <label>układ
+ <label>{{.L.Layout}}
<select name="display">
- <option value="horizontal" {{if eq .Display "horizontal"}}selected{{end}}>poziomo</option>
- <option value="vertical" {{if eq .Display "vertical"}}selected{{end}}>kolumny</option>
- <option value="interlinear" {{if eq .Display "interlinear"}}selected{{end}}>interlinearnie</option>
+ <option value="horizontal" {{if eq .Display "horizontal"}}selected{{end}}>{{.L.OptHorizontal}}</option>
+ <option value="vertical" {{if eq .Display "vertical"}}selected{{end}}>{{.L.OptColumns}}</option>
+ <option value="interlinear" {{if eq .Display "interlinear"}}selected{{end}}>{{.L.OptInterlinear}}</option>
</select>
</label>
</form>
- <label class="theme-picker">motyw
+ <label class="theme-picker">{{.L.Theme}}
<select id="theme-select"
onchange="var o=document.getElementById('theme'),n=o.cloneNode(false);n.setAttribute('href','/theme.css?name='+encodeURIComponent(this.value));o.replaceWith(n);">
{{range .ThemeOpts}}
@@ -69,7 +69,7 @@
</label>
<label class="mono-toggle"><input type="checkbox" {{if .Mono}}checked{{end}}
- onchange="document.body.classList.toggle('mono', this.checked)"> mono</label>
+ onchange="document.body.classList.toggle('mono', this.checked)"> {{.L.Mono}}</label>
<div id="pane">{{.Reading}}</div>