krz/skunky-art

Alternative privacy frontend for DeviantArt.

clone: git clone https://gitbay.org/krz/skunky-art.git

v1.4.0: app/i18n.go · raw

  1package app
  2
  3import (
  4	"encoding/json"
  5	"io"
  6	"skunkyart/static"
  7	"sort"
  8	"strings"
  9	"sync"
 10)
 11
 12// DefaultLang is the catalogue every other one falls back to, key by key. It is
 13// also the only catalogue guaranteed complete: a translation that has not caught
 14// up shows English for the strings it is missing rather than a blank or a key.
 15const DefaultLang = "en"
 16
 17var (
 18	catalogues = map[string]map[string]string{}
 19	langOnce   sync.Once
 20)
 21
 22// LoadLanguages reads static/lang/*.json into memory. Called once, from the same
 23// startup path that copies the templates.
 24//
 25// A malformed or missing catalogue is not fatal: the interface falls back to
 26// English, which is a worse experience than a correct translation but a better
 27// one than refusing to start.
 28func LoadLanguages() {
 29	langOnce.Do(func() {
 30		for _, name := range static.LanguageFiles() {
 31			f, err := static.Templates.Open("lang/" + name)
 32			if err != nil {
 33				continue
 34			}
 35			body, err := io.ReadAll(f)
 36			_ = f.Close()
 37			if err != nil {
 38				continue
 39			}
 40			var c map[string]string
 41			if json.Unmarshal(body, &c) != nil {
 42				continue
 43			}
 44			catalogues[strings.TrimSuffix(name, ".json")] = c
 45		}
 46	})
 47}
 48
 49// Languages lists the catalogues that loaded, sorted, for the About page.
 50func Languages() []string {
 51	out := make([]string, 0, len(catalogues))
 52	for k := range catalogues {
 53		out = append(out, k)
 54	}
 55	sort.Strings(out)
 56	return out
 57}
 58
 59// T returns the string for key in lang, falling back to English and finally to
 60// the key itself. Returning the key rather than "" makes a missing translation
 61// visible in the page instead of silently blank.
 62func T(lang, key string) string {
 63	if c, ok := catalogues[lang]; ok {
 64		if s, ok := c[key]; ok && s != "" {
 65			return s
 66		}
 67	}
 68	if c, ok := catalogues[DefaultLang]; ok {
 69		if s, ok := c[key]; ok {
 70			return s
 71		}
 72	}
 73	return key
 74}
 75
 76// ResolveLang picks the catalogue for a request.
 77//
 78// When the instance pins a language, that wins. Otherwise the browser's own
 79// Accept-Language header decides — a header it already sends on every request,
 80// so reading it adds nothing to what the instance could fingerprint, and needs
 81// no cookie or query string to remember.
 82func ResolveLang(acceptLanguage string) string {
 83	if CFG.Language != "auto" {
 84		if _, ok := catalogues[CFG.Language]; ok {
 85			return CFG.Language
 86		}
 87		return DefaultLang
 88	}
 89
 90	for _, part := range strings.Split(acceptLanguage, ",") {
 91		tag := strings.TrimSpace(part)
 92		if i := strings.Index(tag, ";"); i >= 0 {
 93			tag = tag[:i]
 94		}
 95		if tag == "" {
 96			continue
 97		}
 98		tag = strings.ToLower(tag)
 99		if _, ok := catalogues[tag]; ok {
100			return tag
101		}
102		// en-GB and en-US both mean the en catalogue when there is no regional one.
103		if i := strings.Index(tag, "-"); i > 0 {
104			if _, ok := catalogues[tag[:i]]; ok {
105				return tag[:i]
106			}
107		}
108	}
109	return DefaultLang
110}