rastrillo / abono Public

Clone
git clone https://amadan.net/rastrillo/abono

Plain git — no account needed to clone.

Download

Download this file

1package abono
2
3import (
4 "embed"
5 "fmt"
6 "io/fs"
7 "strconv"
8 "strings"
9
10 "amadan.net/rastrillo/rastrillo"
11)
12
13//go:embed locales/*.toml
14var localesFS embed.FS
15
16// english is locales/en.toml, decoded once. A malformed file is a build
17// bug, so it panics at start-up, as rastrillo's own base catalogs do.
18var english = func() rastrillo.Catalog {
19 data, err := fs.ReadFile(localesFS, "locales/en.toml")
20 if err != nil {
21 panic(fmt.Sprintf("abono: catalog en: %v", err))
22 }
23 m, err := decodeCatalog(data)
24 if err != nil {
25 panic(fmt.Sprintf("abono: catalog en: %v", err))
26 }
27 return m
28}()
29
30// Catalog is abono's English, the "abono.*" message keys the partials and
31// the starter pages use. It is a copy: change it freely.
32//
33// Merge it into the base catalog you give rastrillo.NewLocales, which sits
34// under every locale's own catalog and above nothing but the key itself:
35//
36// base := rastrillo.BaseCatalog()
37// for k, v := range abono.Catalog() {
38// base[k] = v
39// }
40// locs, err := rastrillo.NewLocales(codes, "en", base, appFS)
41//
42// An app overrides any key, or translates it, in its own
43// locales/<code>.toml, where an "abono.*" key wins over this one. A
44// locale the app has not translated reads the English.
45func Catalog() rastrillo.Catalog {
46 out := make(rastrillo.Catalog, len(english))
47 for k, v := range english {
48 out[k] = v
49 }
50 return out
51}
52
53// decodeCatalog reads rastrillo's flat catalog format: one key = "value"
54// per line, # comments and blank lines. rastrillo's own decoder is
55// internal to it, so this is the same subset: no tables, a dot in a key
56// is just a character, a value is a basic string (with escapes) or a
57// literal one.
58func decodeCatalog(data []byte) (rastrillo.Catalog, error) {
59 out := rastrillo.Catalog{}
60 for i, raw := range strings.Split(string(data), "\n") {
61 line := strings.TrimSpace(raw)
62 if line == "" || strings.HasPrefix(line, "#") {
63 continue
64 }
65 key, rest, ok := strings.Cut(line, "=")
66 key = strings.TrimSpace(key)
67 if !ok || key == "" || strings.ContainsAny(key, " \t[]\"'") {
68 return nil, fmt.Errorf("line %d: want key = \"value\"", i+1)
69 }
70 val, err := catalogValue(strings.TrimSpace(rest))
71 if err != nil {
72 return nil, fmt.Errorf("line %d: %w", i+1, err)
73 }
74 if _, dup := out[key]; dup {
75 return nil, fmt.Errorf("line %d: duplicate key %q", i+1, key)
76 }
77 out[key] = val
78 }
79 return out, nil
80}
81
82// catalogValue reads a quoted string, then allows only a # comment after it.
83func catalogValue(s string) (string, error) {
84 var end int
85 switch {
86 case strings.HasPrefix(s, `"`):
87 end = -1
88 for i := 1; i < len(s); i++ {
89 if s[i] == '\\' {
90 i++
91 } else if s[i] == '"' {
92 end = i
93 break
94 }
95 }
96 case strings.HasPrefix(s, "'"):
97 end = strings.Index(s[1:], "'") + 1
98 if end == 0 {
99 end = -1
100 }
101 default:
102 return "", fmt.Errorf("a value must be a quoted string")
103 }
104 if end < 0 {
105 return "", fmt.Errorf("unterminated string")
106 }
107 if tail := strings.TrimSpace(s[end+1:]); tail != "" && !strings.HasPrefix(tail, "#") {
108 return "", fmt.Errorf("unexpected %q after the string", tail)
109 }
110 if s[0] == '\'' {
111 return s[1:end], nil
112 }
113 // TOML's basic-string escapes are a subset of Go's.
114 return strconv.Unquote(s[:end+1])
115}
116