aboutsummaryrefslogtreecommitdiff
path: root/internal/i18n
diff options
context:
space:
mode:
Diffstat (limited to 'internal/i18n')
-rw-r--r--internal/i18n/i18n.go196
-rw-r--r--internal/i18n/i18n_test.go88
2 files changed, 284 insertions, 0 deletions
diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go
new file mode 100644
index 0000000..f0526be
--- /dev/null
+++ b/internal/i18n/i18n.go
@@ -0,0 +1,196 @@
+// Package i18n holds the translations for everything prognosis writes itself:
+// column headers, condition names, section labels, weekday and month names, and
+// pollen species and bands.
+//
+// What is deliberately NOT translated: the body of an IMGW warning. IMGW
+// publishes those in Polish only, and rendering an invented English version of
+// an official warning would be worse than showing the original. In English mode
+// the surrounding labels are English and the warning text stays Polish.
+package i18n
+
+import (
+ "fmt"
+ "time"
+)
+
+// Lang is a supported display language.
+type Lang string
+
+const (
+ EN Lang = "en"
+ PL Lang = "pl"
+)
+
+// Catalog is the set of strings for one language.
+type Catalog struct {
+ lang Lang
+
+ // Table headers, keyed by column name.
+ headers map[string]string
+ // WMO weather code descriptions.
+ conditions map[int]string
+ // Section labels and short words.
+ words map[string]string
+ // Pollen taxa.
+ species map[string]string
+ // Qualitative pollen levels.
+ bands map[string]string
+
+ weekdays [7]string
+ months [12]string
+}
+
+var catalogs = map[Lang]*Catalog{EN: english(), PL: polish()}
+
+// For returns the catalogue for a language, falling back to English so a
+// mistyped language degrades to readable output rather than empty strings.
+func For(lang string) *Catalog {
+ if c, ok := catalogs[Lang(lang)]; ok {
+ return c
+ }
+ return catalogs[EN]
+}
+
+// Lang reports which language this catalogue is.
+func (c *Catalog) Lang() Lang { return c.lang }
+
+// Header is the column heading for a column name.
+func (c *Catalog) Header(column string) string {
+ if s, ok := c.headers[column]; ok {
+ return s
+ }
+ return column
+}
+
+// Condition describes a WMO weather code. Unknown codes are reported as such
+// rather than silently blank, so a new code in the API is visible.
+func (c *Catalog) Condition(code int) string {
+ if s, ok := c.conditions[code]; ok {
+ return s
+ }
+ return fmt.Sprintf("code %d", code)
+}
+
+// Word returns a label such as "day", "pollen" or "warnings".
+func (c *Catalog) Word(key string) string {
+ if s, ok := c.words[key]; ok {
+ return s
+ }
+ return key
+}
+
+// Species is the display name of a pollen taxon.
+func (c *Catalog) Species(name string) string {
+ if s, ok := c.species[name]; ok {
+ return s
+ }
+ return name
+}
+
+// Band is the display name of a qualitative pollen level.
+func (c *Catalog) Band(key string) string {
+ if s, ok := c.bands[key]; ok {
+ return s
+ }
+ return key
+}
+
+// Date formats a date as "Mon 10 Aug" in the catalogue's language. Go's time
+// package has no locale support, so the names come from the catalogue.
+func (c *Catalog) Date(t time.Time) string {
+ return fmt.Sprintf("%s %02d %s", c.weekdays[int(t.Weekday())], t.Day(), c.months[int(t.Month())-1])
+}
+
+func english() *Catalog {
+ return &Catalog{
+ lang: EN,
+ headers: map[string]string{
+ "hour": "hr", "icon": "", "temp": "temp", "feels": "feels",
+ "conditions": "conditions", "mm": "mm", "rain": "rain",
+ "wind": "wind", "gusts": "gusts", "dir": "dir",
+ "humidity": "hum", "dew": "dew", "uv": "uv",
+ "cloud": "cloud", "pressure": "hPa", "visibility": "vis",
+ },
+ conditions: map[int]string{
+ 0: "clear", 1: "mainly clear", 2: "part cloudy", 3: "overcast",
+ 45: "fog", 48: "rime fog",
+ 51: "lt drizzle", 53: "drizzle", 55: "hvy drizzle",
+ 56: "frz drizzle", 57: "frz drizzle",
+ 61: "lt rain", 63: "rain", 65: "hvy rain",
+ 66: "frz rain", 67: "frz rain",
+ 71: "lt snow", 73: "snow", 75: "hvy snow", 77: "snow grains",
+ 80: "lt showers", 81: "showers", 82: "hvy showers",
+ 85: "snow showers", 86: "hvy snow showers",
+ 95: "thunderstorm", 96: "storm + hail", 99: "storm + hail",
+ },
+ words: map[string]string{
+ "sun": "sun", "up": "up", "down": "down",
+ "day": "day", "dry": "dry", "pollen": "pollen",
+ "rainfall": "rain", "over": "over", "daylight": "daylight",
+ "of": "of", "max": "max", "level": "level",
+ "warnings": "warnings", "could_not_check": "could not check IMGW",
+ "poland_only": "IMGW covers Poland only",
+ "hours_available": "hours available, not",
+ "h_per_col": "h/col", "temp_row": "temp", "rain_row": "rain",
+ },
+ species: map[string]string{
+ "grass": "grass", "birch": "birch", "alder": "alder",
+ "mugwort": "mugwort", "ragweed": "ragweed", "olive": "olive",
+ },
+ bands: map[string]string{
+ "none": "none", "low": "low", "medium": "medium",
+ "high": "high", "very high": "very high",
+ },
+ weekdays: [7]string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"},
+ months: [12]string{"Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"},
+ }
+}
+
+func polish() *Catalog {
+ return &Catalog{
+ lang: PL,
+ headers: map[string]string{
+ "hour": "godz", "icon": "", "temp": "temp", "feels": "odczuw",
+ "conditions": "warunki", "mm": "mm", "rain": "deszcz",
+ "wind": "wiatr", "gusts": "porywy", "dir": "kier",
+ "humidity": "wilg", "dew": "ros", "uv": "uv",
+ "cloud": "chmury", "pressure": "hPa", "visibility": "wid",
+ },
+ // Terminology follows IMGW's own vocabulary where it has one, so the
+ // table and a warning describe the same weather in the same words.
+ conditions: map[int]string{
+ 0: "bezchmurnie", 1: "gł. bezchmurnie", 2: "częśc. zachm.", 3: "zachmurzenie",
+ 45: "mgła", 48: "mgła osadz.",
+ 51: "słaba mżawka", 53: "mżawka", 55: "silna mżawka",
+ 56: "mżawka marzn.", 57: "mżawka marzn.",
+ 61: "słaby deszcz", 63: "deszcz", 65: "silny deszcz",
+ 66: "deszcz marzn.", 67: "deszcz marzn.",
+ 71: "słaby śnieg", 73: "śnieg", 75: "silny śnieg", 77: "ziarna śniegu",
+ 80: "słabe przelotne", 81: "przelotne", 82: "silne przelotne",
+ 85: "przelotny śnieg", 86: "silny przel. śnieg",
+ 95: "burza", 96: "burza z gradem", 99: "burza z gradem",
+ },
+ words: map[string]string{
+ "sun": "słońce", "up": "wsch", "down": "zach",
+ "day": "dzień", "dry": "sucho", "pollen": "pyłek",
+ "rainfall": "opad", "over": "przez", "daylight": "dnia",
+ "of": "z", "max": "maks", "level": "stopień",
+ "warnings": "ostrzeżenia", "could_not_check": "nie udało się sprawdzić IMGW",
+ "poland_only": "IMGW obejmuje tylko Polskę",
+ "hours_available": "godzin dostępnych, nie",
+ "h_per_col": "h/kol", "temp_row": "temp", "rain_row": "opad",
+ },
+ species: map[string]string{
+ "grass": "trawy", "birch": "brzoza", "alder": "olcha",
+ "mugwort": "bylica", "ragweed": "ambrozja", "olive": "oliwka",
+ },
+ bands: map[string]string{
+ "none": "brak", "low": "niskie", "medium": "średnie",
+ "high": "wysokie", "very high": "b. wysokie",
+ },
+ weekdays: [7]string{"nd", "pn", "wt", "śr", "cz", "pt", "sb"},
+ months: [12]string{"sty", "lut", "mar", "kwi", "maj", "cze",
+ "lip", "sie", "wrz", "paź", "lis", "gru"},
+ }
+}
diff --git a/internal/i18n/i18n_test.go b/internal/i18n/i18n_test.go
new file mode 100644
index 0000000..2a6a617
--- /dev/null
+++ b/internal/i18n/i18n_test.go
@@ -0,0 +1,88 @@
+package i18n
+
+import (
+ "testing"
+ "time"
+
+ "github.com/lukaszkasprzak/prognosis/internal/config"
+)
+
+func TestForFallsBackToEnglish(t *testing.T) {
+ if For("klingon").Lang() != EN {
+ t.Fatal("an unknown language must degrade to English, not to empty strings")
+ }
+ if For("pl").Lang() != PL {
+ t.Fatal("pl must resolve to the Polish catalogue")
+ }
+}
+
+// Every catalogue must cover every column, or a table in that language would
+// silently fall back to the internal column name.
+func TestEveryLanguageCoversEveryColumn(t *testing.T) {
+ for _, lang := range []Lang{EN, PL} {
+ c := For(string(lang))
+ for _, col := range config.ValidColumns() {
+ if _, ok := c.headers[col]; !ok {
+ t.Errorf("%s: no header for column %q", lang, col)
+ }
+ }
+ }
+}
+
+// The WMO codes must match across languages: a code described in English but
+// not Polish would print "code 71" to a Polish reader.
+func TestConditionCoverageMatchesAcrossLanguages(t *testing.T) {
+ en, pl := For("en"), For("pl")
+ for code := range en.conditions {
+ if _, ok := pl.conditions[code]; !ok {
+ t.Errorf("code %d described in English but not Polish", code)
+ }
+ }
+ for code := range pl.conditions {
+ if _, ok := en.conditions[code]; !ok {
+ t.Errorf("code %d described in Polish but not English", code)
+ }
+ }
+}
+
+func TestWordAndSpeciesCoverageMatches(t *testing.T) {
+ en, pl := For("en"), For("pl")
+ for k := range en.words {
+ if _, ok := pl.words[k]; !ok {
+ t.Errorf("word %q missing from Polish", k)
+ }
+ }
+ for _, s := range config.AllSpecies {
+ if _, ok := en.species[s]; !ok {
+ t.Errorf("species %q missing from English", s)
+ }
+ if _, ok := pl.species[s]; !ok {
+ t.Errorf("species %q missing from Polish", s)
+ }
+ }
+ for _, b := range []string{"none", "low", "medium", "high", "very high"} {
+ if en.Band(b) == b && b != "none" && b != "low" && b != "medium" && b != "high" && b != "very high" {
+ t.Errorf("band %q missing from English", b)
+ }
+ if pl.Band(b) == b {
+ t.Errorf("band %q not translated to Polish", b)
+ }
+ }
+}
+
+func TestUnknownCodeIsVisible(t *testing.T) {
+ got := For("en").Condition(4242)
+ if got != "code 4242" {
+ t.Fatalf("unknown codes must be visible, got %q", got)
+ }
+}
+
+func TestDate(t *testing.T) {
+ d := time.Date(2026, 8, 10, 13, 0, 0, 0, time.UTC) // a Monday
+ if got, want := For("en").Date(d), "Mon 10 Aug"; got != want {
+ t.Errorf("en date = %q, want %q", got, want)
+ }
+ if got, want := For("pl").Date(d), "pn 10 sie"; got != want {
+ t.Errorf("pl date = %q, want %q", got, want)
+ }
+}