diff options
Diffstat (limited to 'internal/i18n/i18n.go')
| -rw-r--r-- | internal/i18n/i18n.go | 110 |
1 files changed, 110 insertions, 0 deletions
diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go new file mode 100644 index 0000000..4f31667 --- /dev/null +++ b/internal/i18n/i18n.go @@ -0,0 +1,110 @@ +// Package i18n holds the languages a blog can be in and the translations of +// the fixed text (dashboard, messages, dates). Keys are the English strings +// themselves, so an untranslated key renders as English rather than blank. +package i18n + +import ( + "fmt" + "strings" + "time" +) + +// Default is what new blogs get and what anything unknown falls back to. +const Default = "en" + +type Language struct { + Code, Name string // Name is in the language itself, for the settings menu +} + +var languages = []Language{ + {Code: "en", Name: "English"}, + {Code: "el", Name: "Ελληνικά"}, +} + +// catalogs maps a language to its translations; English is the identity. +var catalogs = map[string]map[string]string{ + "el": el, +} + +func Languages() []Language { return languages } + +func Valid(code string) bool { + for _, l := range languages { + if l.Code == code { + return true + } + } + return false +} + +// T translates key into lang, returning the key itself when there is no translation. +func T(lang, key string) string { + if s, ok := catalogs[lang][key]; ok { + return s + } + return key +} + +// Tf is T followed by Sprintf; translations may reorder arguments with %[n]s. +func Tf(lang, key string, args ...any) string { + return fmt.Sprintf(T(lang, key), args...) +} + +// Keys lists what a catalog translates (for the completeness test). +func Keys(lang string) []string { + out := make([]string, 0, len(catalogs[lang])) + for k := range catalogs[lang] { + out = append(out, k) + } + return out +} + +// Match picks a supported language from an Accept-Language header: the first +// tag (in header order) whose primary subtag we have, else Default. +func Match(acceptLanguage string) string { + for _, part := range strings.Split(acceptLanguage, ",") { + tag, _, _ := strings.Cut(strings.TrimSpace(part), ";") + primary, _, _ := strings.Cut(strings.ToLower(tag), "-") + if Valid(primary) { + return primary + } + } + return Default +} + +// months are the names a language needs: standalone (archive headings), in a +// date ("14 September" — Greek uses the genitive there) and abbreviated. +type months struct { + name, inDate, short [12]string +} + +var monthTables = map[string]months{ + "el": elMonths, +} + +// Month is the standalone month name (archive headings). +func Month(lang string, m time.Month) string { + if t, ok := monthTables[lang]; ok { + return t.name[m-1] + } + return m.String() +} + +// FormatDate renders a post date in the blogger's chosen style (long | short | iso). +func FormatDate(lang, style string, at time.Time) string { + if style == "iso" { + return at.Format("2006-01-02") + } + t, ok := monthTables[lang] + if !ok { // English: the Go layouts + if style == "short" { + return at.Format("2 Jan 2006") + } + return at.Format("2 January 2006") + } + m := t.inDate[at.Month()-1] + if style == "short" { + m = t.short[at.Month()-1] + } + return fmt.Sprintf("%d %s %d", at.Day(), m, at.Year()) +} |
