diff options
Diffstat (limited to 'internal/web')
| -rw-r--r-- | internal/web/render.go | 31 | ||||
| -rw-r--r-- | internal/web/render_test.go | 30 | ||||
| -rw-r--r-- | internal/web/server.go | 14 | ||||
| -rw-r--r-- | internal/web/server_test.go | 11 | ||||
| -rw-r--r-- | internal/web/templates/index.html | 24 |
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}}"}'>→</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> |
