krz/skunky-art
Alternative privacy frontend for DeviantArt.
clone: git clone https://gitbay.org/krz/skunky-art.git
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}