aboutsummaryrefslogtreecommitdiffstats
path: root/internal/i18n/i18n.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/i18n/i18n.go')
-rw-r--r--internal/i18n/i18n.go110
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())
+}