diff options
Diffstat (limited to 'internal/i18n')
| -rw-r--r-- | internal/i18n/i18n.go | 196 | ||||
| -rw-r--r-- | internal/i18n/i18n_test.go | 88 |
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) + } +} |
