diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/cli/cli.go | 33 | ||||
| -rw-r--r-- | internal/config/config.go | 20 | ||||
| -rw-r--r-- | internal/config/config.toml | 1 | ||||
| -rw-r--r-- | internal/config/config_test.go | 37 | ||||
| -rw-r--r-- | internal/i18n/i18n.go | 114 | ||||
| -rw-r--r-- | internal/i18n/i18n_test.go | 145 | ||||
| -rw-r--r-- | internal/render/render.go | 66 | ||||
| -rw-r--r-- | internal/render/render_test.go | 82 | ||||
| -rw-r--r-- | internal/tui/tui.go | 22 | ||||
| -rw-r--r-- | internal/tui/tui_test.go | 54 | ||||
| -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 |
15 files changed, 600 insertions, 84 deletions
diff --git a/internal/cli/cli.go b/internal/cli/cli.go index e7a2439..b660878 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -14,6 +14,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" @@ -274,7 +275,7 @@ func fetchAndPrint(cfg config.Config, version, date string, all, raw bool, width w := resolveWidth(width, false, stdout) if !raw { - banner := bannerFor(all, date) + banner := bannerFor(cfg.UILanguage, all, date) fmt.Fprintln(stdout, banner) fmt.Fprintln(stdout, strings.Repeat("=", minInt(len(banner), w))) fmt.Fprintln(stdout) @@ -282,33 +283,39 @@ func fetchAndPrint(cfg config.Config, version, date string, all, raw bool, width pieces := make([]string, 0, len(secs)) for _, sec := range secs { - pieces = append(pieces, renderSection(sec, version, cfg.Lectionary, w, raw)) + pieces = append(pieces, renderSection(sec, version, cfg.Lectionary, cfg.UILanguage, w, raw)) } fmt.Fprintln(stdout, strings.Join(pieces, "\n\n")) return 0 } -// bannerFor mirrors ewangelia.py's banner text: "Czytania na D" when every -// part is shown, "Ewangelia na D" for the gospel-only default. -func bannerFor(all bool, date string) string { +// bannerFor builds the "<Gospel|Readings> — DATE" banner: the "readings" +// word when every part is shown, "gospel" for the gospel-only default, both +// localised via i18n.Get(lang) (lang="pl" reproduces ewangelia.py's original +// Polish wording, just with "—" in place of "na"). +func bannerFor(lang string, all bool, date string) string { + ui := i18n.Get(lang) + word := ui.BannerGospel if all { - return "Czytania na " + date + word = ui.BannerReadings } - return "Ewangelia na " + date + return word + " — " + date } // renderSection formats one section as its heading (unless raw) followed by -// render.GatherVersion's blocks, each wrapped to width. -func renderSection(sec liturgy.Section, version, lectionary string, width int, raw bool) string { +// render.GatherVersion's blocks, each wrapped to width. The heading's part +// label is localised via render.LocalizeHeading (lang); the citation/verse +// text is never touched. +func renderSection(sec liturgy.Section, version, lectionary, lang string, width int, raw bool) string { var lines []string if !raw { - lines = append(lines, sec.Heading) + lines = append(lines, render.LocalizeHeading(sec.Heading, sec.PartID, lang)) if sec.Subtitle != "" { lines = append(lines, sec.Subtitle) } lines = append(lines, "") } - _, blocks := render.GatherVersion(version, sec, lectionary) + _, blocks := render.GatherVersion(version, sec, lectionary, lang) for _, b := range blocks { lines = append(lines, wrapText(b, width)) } @@ -359,12 +366,12 @@ func renderCompare(cfg config.Config, list, date string, all, raw bool, width in w := resolveWidth(width, true, stdout) if !raw { - banner := bannerFor(all, date) + banner := bannerFor(cfg.UILanguage, all, date) fmt.Fprintln(stdout, banner) fmt.Fprintln(stdout, strings.Repeat("=", minInt(len(banner), w))) fmt.Fprintln(stdout) } - fmt.Fprintln(stdout, render.Compare(secs, versions, w, cfg.Lectionary)) + fmt.Fprintln(stdout, render.Compare(secs, versions, w, cfg.Lectionary, cfg.UILanguage)) return 0 } diff --git a/internal/config/config.go b/internal/config/config.go index e501cf8..dd81544 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -51,6 +51,23 @@ func NormalizeDisplay(display string) string { return d } +// validUILanguages are the two UI chrome languages lectio understands. +var validUILanguages = map[string]bool{ + "en": true, + "pl": true, +} + +// NormalizeUILanguage lower-cases lang and falls back to "en" when it is not +// one of validUILanguages -- the same lenient style as NormalizeDisplay (no +// error, just a safe default). +func NormalizeUILanguage(lang string) string { + l := strings.ToLower(lang) + if !validUILanguages[l] { + return "en" + } + return l +} + // Config holds lectio's user-configurable settings. type Config struct { SchemaVersion int `toml:"schema_version"` @@ -61,6 +78,7 @@ type Config struct { Width int `toml:"width"` All bool `toml:"all"` Offline bool `toml:"offline"` + UILanguage string `toml:"ui_language"` WebTheme string `toml:"web_theme"` WebPort int `toml:"web_port"` WebDisplay string `toml:"web_display"` @@ -91,6 +109,7 @@ func Default() Config { Width: 0, All: false, Offline: false, + UILanguage: "en", WebTheme: "transfiguration", WebPort: 0, WebDisplay: "horizontal", @@ -162,6 +181,7 @@ func Load() (Config, error) { return def, nil } cfg.WebDisplay = NormalizeDisplay(cfg.WebDisplay) + cfg.UILanguage = NormalizeUILanguage(cfg.UILanguage) if err := validate(cfg); err != nil { return Config{}, err diff --git a/internal/config/config.toml b/internal/config/config.toml index 3554435..c1f97db 100644 --- a/internal/config/config.toml +++ b/internal/config/config.toml @@ -6,6 +6,7 @@ default_version = "pl" # TUI start / `lectio show` default width = 0 # CLI wrap width; 0 = detect terminal all = false # default to all parts (true) or just the gospel (false) offline = false # true = never fetch; read only harvested sigla + cache +ui_language = "en" # interface language (labels/keybar/banner): "en" or "pl". Readings stay source-language. web_theme = "transfiguration" # default lectio-web theme (built-in order/season name, or a user theme in ~/.config/lectio/themes/) web_port = 0 # lectio-web port; 0 = try 1099, then any free port web_display = "horizontal" # default lectio-web layout ("uklad"): "horizontal" (stacked), "vertical" (columns), "interlinear" (verse-by-verse) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index ff513ea..616f0cd 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -107,6 +107,43 @@ func TestWebDisplayNormalizesUnknown(t *testing.T) { } } +func TestUILanguageDefault(t *testing.T) { + def := Default() + if def.UILanguage != "en" { + t.Errorf("UILanguage default wrong: got %q, want %q", def.UILanguage, "en") + } +} + +func TestUILanguageLoadsPL(t *testing.T) { + dir := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", dir) + os.MkdirAll(filepath.Join(dir, "lectio"), 0o755) + os.WriteFile(filepath.Join(dir, "lectio", "config.toml"), + []byte("ui_language = \"pl\"\n"), 0o644) + cfg, err := Load() + if err != nil { + t.Fatal(err) + } + if cfg.UILanguage != "pl" { + t.Errorf("UILanguage = %q, want %q", cfg.UILanguage, "pl") + } +} + +func TestUILanguageNormalizesUnknown(t *testing.T) { + dir := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", dir) + os.MkdirAll(filepath.Join(dir, "lectio"), 0o755) + os.WriteFile(filepath.Join(dir, "lectio", "config.toml"), + []byte("ui_language = \"bogus\"\n"), 0o644) + cfg, err := Load() + if err != nil { + t.Fatal(err) + } + if cfg.UILanguage != "en" { + t.Errorf("UILanguage = %q, want %q (normalized from bogus)", cfg.UILanguage, "en") + } +} + func TestPartShown(t *testing.T) { var empty Config if !empty.PartShown("new", "psalm") { diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go new file mode 100644 index 0000000..a5cb77b --- /dev/null +++ b/internal/i18n/i18n.go @@ -0,0 +1,114 @@ +// Package i18n holds lectio's UI-chrome string tables (English and Polish) +// for the ui_language config setting. It localises the chrome only -- +// version-column labels, the TUI keybar/messages, the CLI banner words, the +// web control labels, and the modern-lectionary section-heading label words +// -- never the scripture verse text or citation, which stay source-language. +package i18n + +// UI is one language's complete set of chrome strings. +type UI struct { + // Version names each bible version for column headers / prose, keyed by + // version code (pl, wuj, vul, grb, drb). + Version map[string]string + + // PartLabel names each modern-lectionary section's heading label word, + // keyed by liturgy.Section.PartID (pierwsze_czytanie, psalm, + // drugie_czytanie, aklamacja, ewangelia). The pl entries are the exact + // prefixes niedziela.pl's scraped headings carry, used by + // render.LocalizeHeading to recognise and swap the label word while + // keeping the citation untouched. + PartLabel map[string]string + + // TUI keybar and status messages. + FooterKeys, Loading, NoReadingsFor, ErrorPrefix, ErrorHint string + + // CLI banner label words: the banner is "<word> — <date>". + BannerGospel, BannerReadings string + + // Web control labels. + Lectionary, OptModern, OptTraditional string + Parts, OptGospel, OptAll string + Layout, OptHorizontal, OptColumns string + OptInterlinear, Theme, Mono string +} + +// Get returns lang's chrome string set, falling back to English for +// anything other than "pl". +func Get(lang string) UI { + if lang == "pl" { + return plUI + } + return enUI +} + +var enUI = UI{ + Version: map[string]string{ + "pl": "Polish (niedziela.pl)", + "wuj": "Wujek (Polish)", + "vul": "Vulgate (Latin)", + "grb": "Greek", + "drb": "Douay-Rheims (English)", + }, + PartLabel: map[string]string{ + "pierwsze_czytanie": "1st reading", + "psalm": "Psalm", + "drugie_czytanie": "2nd reading", + "aklamacja": "Acclamation", + "ewangelia": "Gospel", + }, + FooterKeys: "tab/⇧tab version ←/→ day j/k scroll space/b page g/G top/bottom r refresh q quit", + Loading: "loading…", + NoReadingsFor: "no readings for ", + ErrorPrefix: "error: ", + ErrorHint: "change date (←/→) or refresh (r)", + BannerGospel: "Gospel", + BannerReadings: "Readings", + Lectionary: "lectionary", + OptModern: "modern", + OptTraditional: "traditional", + Parts: "parts", + OptGospel: "Gospel", + OptAll: "all parts", + Layout: "layout", + OptHorizontal: "horizontal", + OptColumns: "columns", + OptInterlinear: "interlinear", + Theme: "theme", + Mono: "mono", +} + +var plUI = UI{ + Version: map[string]string{ + "pl": "Polski (niedziela.pl)", + "wuj": "Wujek (pol.)", + "vul": "Wulgata (lac.)", + "grb": "Grecki", + "drb": "Douay-Rheims (ang.)", + }, + PartLabel: map[string]string{ + "pierwsze_czytanie": "1. czytanie", + "psalm": "Psalm", + "drugie_czytanie": "2. czytanie", + "aklamacja": "Aklamacja", + "ewangelia": "Ewangelia", + }, + FooterKeys: "tab/⇧tab wersja ←/→ dzień j/k przewiń spacja/b strona g/G góra/dół r odśwież q wyjście", + Loading: "ładowanie…", + NoReadingsFor: "brak czytań na ", + ErrorPrefix: "błąd: ", + ErrorHint: "zmień datę (←/→) lub odśwież (r)", + BannerGospel: "Ewangelia", + BannerReadings: "Czytania", + Lectionary: "lekcjonarz", + OptModern: "nowy", + OptTraditional: "tradycyjny", + Parts: "zakres", + OptGospel: "Ewangelia", + OptAll: "wszystkie części", + Layout: "układ", + OptHorizontal: "poziomo", + OptColumns: "kolumny", + OptInterlinear: "interlinearnie", + Theme: "motyw", + Mono: "mono", +} diff --git a/internal/i18n/i18n_test.go b/internal/i18n/i18n_test.go new file mode 100644 index 0000000..8c6f137 --- /dev/null +++ b/internal/i18n/i18n_test.go @@ -0,0 +1,145 @@ +package i18n + +import "testing" + +func TestGetTheme(t *testing.T) { + if got := Get("pl").Theme; got != "motyw" { + t.Errorf(`Get("pl").Theme = %q, want "motyw"`, got) + } + if got := Get("en").Theme; got != "theme" { + t.Errorf(`Get("en").Theme = %q, want "theme"`, got) + } +} + +func TestGetUnknownFallsBackToEnglish(t *testing.T) { + en := Get("en") + if got := Get("xx"); got.Theme != en.Theme || got.BannerGospel != en.BannerGospel { + t.Errorf(`Get("xx") = %+v, want the English set %+v`, got, en) + } + if got := Get(""); got.Theme != en.Theme || got.BannerGospel != en.BannerGospel { + t.Errorf(`Get("") = %+v, want the English set %+v`, got, en) + } +} + +func TestVersionLabels(t *testing.T) { + cases := []struct { + lang, code, want string + }{ + {"en", "pl", "Polish (niedziela.pl)"}, + {"pl", "pl", "Polski (niedziela.pl)"}, + {"en", "wuj", "Wujek (Polish)"}, + {"pl", "wuj", "Wujek (pol.)"}, + {"en", "vul", "Vulgate (Latin)"}, + {"pl", "vul", "Wulgata (lac.)"}, + {"en", "grb", "Greek"}, + {"pl", "grb", "Grecki"}, + {"en", "drb", "Douay-Rheims (English)"}, + {"pl", "drb", "Douay-Rheims (ang.)"}, + } + for _, c := range cases { + if got := Get(c.lang).Version[c.code]; got != c.want { + t.Errorf("Get(%q).Version[%q] = %q, want %q", c.lang, c.code, got, c.want) + } + } +} + +func TestTUIStrings(t *testing.T) { + en := Get("en") + pl := Get("pl") + + if en.FooterKeys != "tab/⇧tab version ←/→ day j/k scroll space/b page g/G top/bottom r refresh q quit" { + t.Errorf("en.FooterKeys = %q", en.FooterKeys) + } + if pl.FooterKeys != "tab/⇧tab wersja ←/→ dzień j/k przewiń spacja/b strona g/G góra/dół r odśwież q wyjście" { + t.Errorf("pl.FooterKeys = %q", pl.FooterKeys) + } + if en.Loading != "loading…" || pl.Loading != "ładowanie…" { + t.Errorf("Loading en=%q pl=%q", en.Loading, pl.Loading) + } + if en.NoReadingsFor != "no readings for " || pl.NoReadingsFor != "brak czytań na " { + t.Errorf("NoReadingsFor en=%q pl=%q", en.NoReadingsFor, pl.NoReadingsFor) + } + if en.ErrorPrefix != "error: " || pl.ErrorPrefix != "błąd: " { + t.Errorf("ErrorPrefix en=%q pl=%q", en.ErrorPrefix, pl.ErrorPrefix) + } + if en.ErrorHint != "change date (←/→) or refresh (r)" || pl.ErrorHint != "zmień datę (←/→) lub odśwież (r)" { + t.Errorf("ErrorHint en=%q pl=%q", en.ErrorHint, pl.ErrorHint) + } +} + +func TestBannerWords(t *testing.T) { + en := Get("en") + pl := Get("pl") + if en.BannerGospel != "Gospel" || pl.BannerGospel != "Ewangelia" { + t.Errorf("BannerGospel en=%q pl=%q", en.BannerGospel, pl.BannerGospel) + } + if en.BannerReadings != "Readings" || pl.BannerReadings != "Czytania" { + t.Errorf("BannerReadings en=%q pl=%q", en.BannerReadings, pl.BannerReadings) + } +} + +func TestWebControlLabels(t *testing.T) { + en := Get("en") + pl := Get("pl") + + cases := []struct { + name, en, pl string + }{ + {"Lectionary", en.Lectionary, pl.Lectionary}, + {"OptModern", en.OptModern, pl.OptModern}, + {"OptTraditional", en.OptTraditional, pl.OptTraditional}, + {"Parts", en.Parts, pl.Parts}, + {"OptGospel", en.OptGospel, pl.OptGospel}, + {"OptAll", en.OptAll, pl.OptAll}, + {"Layout", en.Layout, pl.Layout}, + {"OptHorizontal", en.OptHorizontal, pl.OptHorizontal}, + {"OptColumns", en.OptColumns, pl.OptColumns}, + {"OptInterlinear", en.OptInterlinear, pl.OptInterlinear}, + {"Theme", en.Theme, pl.Theme}, + {"Mono", en.Mono, pl.Mono}, + } + want := map[string][2]string{ + "Lectionary": {"lectionary", "lekcjonarz"}, + "OptModern": {"modern", "nowy"}, + "OptTraditional": {"traditional", "tradycyjny"}, + "Parts": {"parts", "zakres"}, + "OptGospel": {"Gospel", "Ewangelia"}, + "OptAll": {"all parts", "wszystkie części"}, + "Layout": {"layout", "układ"}, + "OptHorizontal": {"horizontal", "poziomo"}, + "OptColumns": {"columns", "kolumny"}, + "OptInterlinear": {"interlinear", "interlinearnie"}, + "Theme": {"theme", "motyw"}, + "Mono": {"mono", "mono"}, + } + for _, c := range cases { + w := want[c.name] + if c.en != w[0] { + t.Errorf("en.%s = %q, want %q", c.name, c.en, w[0]) + } + if c.pl != w[1] { + t.Errorf("pl.%s = %q, want %q", c.name, c.pl, w[1]) + } + } +} + +func TestModernPartLabels(t *testing.T) { + en := Get("en") + pl := Get("pl") + + want := map[string][2]string{ + "pierwsze_czytanie": {"1. czytanie", "1st reading"}, + "psalm": {"Psalm", "Psalm"}, + "drugie_czytanie": {"2. czytanie", "2nd reading"}, + "aklamacja": {"Aklamacja", "Acclamation"}, + "ewangelia": {"Ewangelia", "Gospel"}, + } + for partID, w := range want { + if got := pl.PartLabel[partID]; got != w[0] { + t.Errorf("pl.PartLabel[%q] = %q, want %q", partID, got, w[0]) + } + if got := en.PartLabel[partID]; got != w[1] { + t.Errorf("en.PartLabel[%q] = %q, want %q", partID, got, w[1]) + } + } +} diff --git a/internal/render/render.go b/internal/render/render.go index b8d778c..c5a2ab0 100644 --- a/internal/render/render.go +++ b/internal/render/render.go @@ -9,17 +9,43 @@ import ( "strings" "github.com/lukaszkasprzak/lectio/internal/bible" + "github.com/lukaszkasprzak/lectio/internal/i18n" "github.com/lukaszkasprzak/lectio/internal/liturgy" ) -// versionLabels names each version for column headers / prose. Ported -// verbatim from ewangelia.py VERSION_LABELS. -var versionLabels = map[string]string{ - "pl": "Polski (niedziela.pl)", - "wuj": "Wujek (pol.)", - "vul": "Wulgata (lac.)", - "grb": "Grecki", - "drb": "Douay-Rheims (ang.)", +// versionLabel returns version's column-header/prose label in lang (see +// internal/i18n.Get), falling back to the bare version code if lang has no +// entry for it. lang="pl" reproduces ewangelia.py's original VERSION_LABELS +// exactly. +func versionLabel(version, lang string) string { + if l, ok := i18n.Get(lang).Version[version]; ok { + return l + } + return version +} + +// LocalizeHeading swaps a modern (niedziela.pl) section heading's leading +// label word for its lang translation, keeping the rest of the heading (the +// parenthetical citation, exactly as scraped) untouched: e.g. +// "Ewangelia (J 20, 1. 11-18)" with partID "ewangelia" and lang "en" becomes +// "Gospel (J 20, 1. 11-18)". It only ever localises the label word, never +// the citation or the verse text. +// +// It is a safe no-op (returns heading unchanged) unless all of: lang is +// "en", partID names a known modern-lectionary part (see +// internal/i18n.UI.PartLabel), and heading actually starts with that part's +// Polish label -- which excludes traditional (missalemeum) headings, already +// in the requested language, and anything unrecognised. +func LocalizeHeading(heading, partID, lang string) string { + if lang != "en" { + return heading + } + plLabel, ok := i18n.Get("pl").PartLabel[partID] + if !ok || !strings.HasPrefix(heading, plLabel) { + return heading + } + enLabel := i18n.Get(lang).PartLabel[partID] + return enLabel + heading[len(plLabel):] } // versionSystem maps a bible version to the Psalter system bible.ToEnglishRef @@ -49,8 +75,10 @@ const incipit = "Słowa Ewangelii" // lectionary selects how sec.Citation is read: "new" (modern/niedziela.pl) // citations are Polish and go through bible.ToEnglishRef; "traditional" // (missalemeum) citations are already English kjv-style and are used as-is. -func GatherVersion(version string, sec liturgy.Section, lectionary string) (label string, blocks []string) { - label = versionLabels[version] +// lang selects the UI chrome language the label comes from (see +// internal/i18n); it never affects the verse text/citation itself. +func GatherVersion(version string, sec liturgy.Section, lectionary, lang string) (label string, blocks []string) { + label = versionLabel(version, lang) if version == "pl" { return label, gatherPL(sec) } @@ -105,9 +133,10 @@ func resolveRef(version string, sec liturgy.Section, lectionary string) (string, // GatherVerses returns one version's verses for a section as raw bible.Verse // structs (for column/interlinear alignment). versified is false for "pl" // (paragraph text, no verse numbers) and on any resolution/lookup failure -- -// callers fall back to GatherVersion's string blocks for those. -func GatherVerses(version string, sec liturgy.Section, lectionary string) (label string, verses []bible.Verse, versified bool) { - label = versionLabels[version] +// callers fall back to GatherVersion's string blocks for those. lang selects +// the UI chrome language the label comes from, same as GatherVersion. +func GatherVerses(version string, sec liturgy.Section, lectionary, lang string) (label string, verses []bible.Verse, versified bool) { + label = versionLabel(version, lang) if version == "pl" { return label, nil, false } @@ -177,16 +206,17 @@ func OfflineVersions(versions []string) []string { } // Compare lays the versions of one reading section out as parallel columns, -// side by side, wrapped to fit width. Ports render_compare. -func Compare(secs []liturgy.Section, versions []string, width int, lectionary string) string { +// side by side, wrapped to fit width. Ports render_compare. lang selects the +// column-header label language (see GatherVersion). +func Compare(secs []liturgy.Section, versions []string, width int, lectionary, lang string) string { var out []string for _, sec := range secs { - out = append(out, compareSection(sec, versions, width, lectionary)) + out = append(out, compareSection(sec, versions, width, lectionary, lang)) } return strings.Join(out, "\n\n") } -func compareSection(sec liturgy.Section, versions []string, width int, lectionary string) string { +func compareSection(sec liturgy.Section, versions []string, width int, lectionary, lang string) string { type column struct { label string lines []string @@ -205,7 +235,7 @@ func compareSection(sec liturgy.Section, versions []string, width int, lectionar cols := make([]column, 0, n) height := 0 for _, v := range versions { - label, blocks := GatherVersion(v, sec, lectionary) + label, blocks := GatherVersion(v, sec, lectionary, lang) var lines []string for _, b := range blocks { lines = append(lines, strings.Split(wrap(b, w), "\n")...) diff --git a/internal/render/render_test.go b/internal/render/render_test.go index 1b338a0..b016d6b 100644 --- a/internal/render/render_test.go +++ b/internal/render/render_test.go @@ -12,7 +12,7 @@ func TestGatherPLDedup(t *testing.T) { Heading: "Psalm (Ps 1)", Paragraphs: [][]string{{"stanza one"}, {"refrain"}, {"stanza two"}, {"refrain"}}, } - _, blocks := GatherVersion("pl", sec, "new") + _, blocks := GatherVersion("pl", sec, "new", "pl") n := 0 for _, b := range blocks { if b == "refrain" { @@ -26,7 +26,7 @@ func TestGatherPLDedup(t *testing.T) { func TestGatherBible(t *testing.T) { sec := liturgy.Section{Heading: "Ewangelia (J 20, 1. 11-18)"} - label, blocks := GatherVersion("wuj", sec, "new") + label, blocks := GatherVersion("wuj", sec, "new", "pl") if !strings.Contains(label, "Wujek") { t.Errorf("label = %q", label) } @@ -37,12 +37,45 @@ func TestGatherBible(t *testing.T) { func TestGatherTraditional(t *testing.T) { sec := liturgy.Section{Citation: "Luke 7:36-50"} - _, blocks := GatherVersion("vul", sec, "traditional") + _, blocks := GatherVersion("vul", sec, "traditional", "pl") if len(blocks) == 0 || !strings.HasPrefix(blocks[0], "7:36") { t.Errorf("first block = %q", blocks) } } +// TestGatherVersionLabelLang checks that the label's language follows the +// lang parameter -- pl reproduces today's Polish labels exactly, en gives +// the English set from internal/i18n. +func TestGatherVersionLabelLang(t *testing.T) { + sec := liturgy.Section{Heading: "Ewangelia (J 20, 1. 11-18)"} + + label, _ := GatherVersion("vul", sec, "new", "pl") + if label != "Wulgata (lac.)" { + t.Errorf(`GatherVersion(..., "pl") label = %q, want "Wulgata (lac.)"`, label) + } + + label, _ = GatherVersion("vul", sec, "new", "en") + if label != "Vulgate (Latin)" { + t.Errorf(`GatherVersion(..., "en") label = %q, want "Vulgate (Latin)"`, label) + } +} + +// TestCompareLabelLang checks Compare's column-header row follows lang, +// same as GatherVersion (which it wraps via compareSection). +func TestCompareLabelLang(t *testing.T) { + sec := liturgy.Section{Heading: "Ewangelia (J 20, 1. 11-18)"} + + out := Compare([]liturgy.Section{sec}, []string{"vul"}, 80, "new", "pl") + if !strings.Contains(out, "Wulgata (lac.)") { + t.Errorf(`Compare(..., "pl") = %q, want it to contain "Wulgata (lac.)"`, out) + } + + out = Compare([]liturgy.Section{sec}, []string{"vul"}, 80, "new", "en") + if !strings.Contains(out, "Vulgate (Latin)") { + t.Errorf(`Compare(..., "en") = %q, want it to contain "Vulgate (Latin)"`, out) + } +} + func TestOfflineVersions(t *testing.T) { got := OfflineVersions([]string{"pl", "wuj", "vul"}) for _, v := range got { @@ -54,7 +87,7 @@ func TestOfflineVersions(t *testing.T) { func TestGatherVersesBible(t *testing.T) { sec := liturgy.Section{Heading: "Ewangelia (J 20, 1. 11-18)"} - label, verses, versified := GatherVerses("wuj", sec, "new") + label, verses, versified := GatherVerses("wuj", sec, "new", "pl") if !strings.Contains(label, "Wujek") { t.Errorf("label = %q", label) } @@ -74,7 +107,7 @@ func TestGatherVersesPL(t *testing.T) { Heading: "Psalm (Ps 1)", Paragraphs: [][]string{{"stanza one"}}, } - _, verses, versified := GatherVerses("pl", sec, "new") + _, verses, versified := GatherVerses("pl", sec, "new", "pl") if versified { t.Error("versified = true, want false for pl (paragraph text, no verse numbers)") } @@ -82,3 +115,42 @@ func TestGatherVersesPL(t *testing.T) { t.Errorf("verses = %+v, want nil for pl", verses) } } + +// TestGatherVersesLabelLang checks GatherVerses' label also follows lang, +// same as GatherVersion. +func TestGatherVersesLabelLang(t *testing.T) { + sec := liturgy.Section{Heading: "Ewangelia (J 20, 1. 11-18)"} + label, _, _ := GatherVerses("vul", sec, "new", "en") + if label != "Vulgate (Latin)" { + t.Errorf(`GatherVerses(..., "en") label = %q, want "Vulgate (Latin)"`, label) + } +} + +// TestLocalizeHeading covers render.LocalizeHeading's part-label swap +// (brief §3b): the pl label prefix of a modern (niedziela.pl) heading is +// replaced by its en label, the citation kept exactly as scraped, and every +// other case (pl, unknown/traditional partID, prefix mismatch) is a safe +// no-op that returns heading unchanged. +func TestLocalizeHeading(t *testing.T) { + cases := []struct { + name, heading, partID, lang, want string + }{ + {"gospel en", "Ewangelia (J 20, 1. 11-18)", "ewangelia", "en", "Gospel (J 20, 1. 11-18)"}, + {"first reading en", "1. czytanie (Dz 2, 14. 22-33)", "pierwsze_czytanie", "en", "1st reading (Dz 2, 14. 22-33)"}, + {"psalm en", "Psalm (Ps 15)", "psalm", "en", "Psalm (Ps 15)"}, + {"acclamation en", "Aklamacja (Alleluja)", "aklamacja", "en", "Acclamation (Alleluja)"}, + {"pl unchanged", "Ewangelia (J 20, 1. 11-18)", "ewangelia", "pl", "Ewangelia (J 20, 1. 11-18)"}, + {"unknown partID unchanged", "Coś innego (X 1)", "", "en", "Coś innego (X 1)"}, + {"already-English heading unchanged (traditional lectionary, no pl prefix to match)", "Gospel (Luke 7:36-50)", "ewangelia", "en", "Gospel (Luke 7:36-50)"}, + {"prefix mismatch unchanged", "Nieoczekiwany tytuł (J 1)", "ewangelia", "en", "Nieoczekiwany tytuł (J 1)"}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := LocalizeHeading(c.heading, c.partID, c.lang) + if got != c.want { + t.Errorf("LocalizeHeading(%q, %q, %q) = %q, want %q", c.heading, c.partID, c.lang, got, c.want) + } + }) + } +} diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 1e3b948..bcfb3c1 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -13,6 +13,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "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" @@ -42,8 +43,6 @@ type errMsg struct { err error } -const footerKeys = "tab/⇧tab wersja ←/→ dzień j/k przewiń spacja/b strona g/G góra/dół r odśwież q wyjście" - // New builds the initial model: cfg.Offline drops "pl" from the version // list (render.OfflineVersions), the active version starts at // cfg.DefaultVersion (falling back to the first version if not found, or @@ -256,7 +255,7 @@ func (m Model) View() string { } header := headerStyle.Width(w).Render(m.headerText()) - footer := footerStyle.Width(w).Render(footerKeys) + footer := footerStyle.Width(w).Render(i18n.Get(m.cfg.UILanguage).FooterKeys) bodyLines := m.bodyLines(innerW) @@ -276,7 +275,7 @@ func (m Model) View() string { func (m Model) headerText() string { label := m.version() if len(m.sections) > 0 { - if l, _ := render.GatherVersion(m.version(), m.sections[0], m.cfg.Lectionary); l != "" { + if l, _ := render.GatherVersion(m.version(), m.sections[0], m.cfg.Lectionary, m.cfg.UILanguage); l != "" { label = l } } @@ -287,17 +286,19 @@ func (m Model) headerText() string { // through: a loading/error/empty notice, or each section's heading + // render.GatherVersion blocks for the active version. func (m Model) bodyLines(w int) []string { + ui := i18n.Get(m.cfg.UILanguage) + switch { case m.err != nil: return []string{ - errStyle.Render("błąd: " + m.err.Error()), + errStyle.Render(ui.ErrorPrefix + m.err.Error()), "", - citationStyle.Render("zmień datę (←/→) lub odśwież (r)"), + citationStyle.Render(ui.ErrorHint), } case m.loading: - return []string{citationStyle.Render("ładowanie…")} + return []string{citationStyle.Render(ui.Loading)} case len(m.sections) == 0: - return []string{citationStyle.Render("brak czytań na " + m.date)} + return []string{citationStyle.Render(ui.NoReadingsFor + m.date)} } ver := m.version() @@ -306,13 +307,14 @@ func (m Model) bodyLines(w int) []string { if i > 0 { lines = append(lines, "") } - lines = append(lines, headingStyle.Render(sec.Heading)) + heading := render.LocalizeHeading(sec.Heading, sec.PartID, m.cfg.UILanguage) + lines = append(lines, headingStyle.Render(heading)) if sec.Subtitle != "" { lines = append(lines, citationStyle.Render(sec.Subtitle)) } lines = append(lines, "") - _, blocks := render.GatherVersion(ver, sec, m.cfg.Lectionary) + _, blocks := render.GatherVersion(ver, sec, m.cfg.Lectionary, m.cfg.UILanguage) isPsalm := sec.PartID == "psalm" for bi, b := range blocks { refrain := isPsalm && bi == 0 diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go index 2aa7b84..baa5c05 100644 --- a/internal/tui/tui_test.go +++ b/internal/tui/tui_test.go @@ -1,9 +1,11 @@ package tui import ( + "strings" "testing" "github.com/lukaszkasprzak/lectio/internal/config" + "github.com/lukaszkasprzak/lectio/internal/liturgy" ) func TestVersionCycle(t *testing.T) { @@ -29,3 +31,55 @@ func TestOfflineDropsPL(t *testing.T) { } } } + +// TestBodyLinesLocalisesMessages checks that the loading/no-readings/error +// messages follow cfg.UILanguage. +func TestBodyLinesLocalisesMessages(t *testing.T) { + en := Model{cfg: config.Config{UILanguage: "en"}, loading: true} + if lines := en.bodyLines(80); len(lines) == 0 || !strings.Contains(lines[0], "loading") { + t.Errorf("en loading bodyLines = %v, want it to contain %q", lines, "loading") + } + + pl := Model{cfg: config.Config{UILanguage: "pl"}, loading: true} + if lines := pl.bodyLines(80); len(lines) == 0 || !strings.Contains(lines[0], "ładowanie") { + t.Errorf("pl loading bodyLines = %v, want it to contain %q", lines, "ładowanie") + } + + enEmpty := Model{cfg: config.Config{UILanguage: "en"}, date: "2026-07-22"} + if lines := enEmpty.bodyLines(80); len(lines) == 0 || !strings.Contains(lines[0], "no readings for") { + t.Errorf("en no-readings bodyLines = %v, want it to contain %q", lines, "no readings for") + } +} + +// TestBodyLinesLocalisesHeading checks that a modern-lectionary section +// heading's label word follows cfg.UILanguage while the citation stays +// exactly as scraped (render.LocalizeHeading, brief §3b). +func TestBodyLinesLocalisesHeading(t *testing.T) { + sec := liturgy.Section{Heading: "Ewangelia (J 20, 1. 11-18)", PartID: "ewangelia"} + + en := Model{cfg: config.Config{UILanguage: "en", Lectionary: "new"}, sections: []liturgy.Section{sec}} + body := strings.Join(en.bodyLines(80), "\n") + if !strings.Contains(body, "Gospel (J 20, 1. 11-18)") { + t.Errorf("en bodyLines missing localised heading: %q", body) + } + + pl := Model{cfg: config.Config{UILanguage: "pl", Lectionary: "new"}, sections: []liturgy.Section{sec}} + body = strings.Join(pl.bodyLines(80), "\n") + if !strings.Contains(body, "Ewangelia (J 20, 1. 11-18)") { + t.Errorf("pl bodyLines missing unchanged heading: %q", body) + } +} + +// TestFooterKeysLocalised checks the footer keybar text follows +// cfg.UILanguage. +func TestFooterKeysLocalised(t *testing.T) { + en := Model{cfg: config.Config{UILanguage: "en"}, date: "2026-07-22"} + if out := en.View(); !strings.Contains(out, "q quit") { + t.Errorf("en View() footer missing %q: %q", "q quit", out) + } + + pl := Model{cfg: config.Config{UILanguage: "pl"}, date: "2026-07-22"} + if out := pl.View(); !strings.Contains(out, "q wyjście") { + t.Errorf("pl View() footer missing %q: %q", "q wyjście", out) + } +} 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> |
