package abono import ( "embed" "fmt" "io/fs" "strconv" "strings" "amadan.net/rastrillo/rastrillo" ) //go:embed locales/*.toml var localesFS embed.FS // english is locales/en.toml, decoded once. A malformed file is a build // bug, so it panics at start-up, as rastrillo's own base catalogs do. var english = func() rastrillo.Catalog { data, err := fs.ReadFile(localesFS, "locales/en.toml") if err != nil { panic(fmt.Sprintf("abono: catalog en: %v", err)) } m, err := decodeCatalog(data) if err != nil { panic(fmt.Sprintf("abono: catalog en: %v", err)) } return m }() // Catalog is abono's English, the "abono.*" message keys the partials and // the starter pages use. It is a copy: change it freely. // // Merge it into the base catalog you give rastrillo.NewLocales, which sits // under every locale's own catalog and above nothing but the key itself: // // base := rastrillo.BaseCatalog() // for k, v := range abono.Catalog() { // base[k] = v // } // locs, err := rastrillo.NewLocales(codes, "en", base, appFS) // // An app overrides any key, or translates it, in its own // locales/.toml, where an "abono.*" key wins over this one. A // locale the app has not translated reads the English. func Catalog() rastrillo.Catalog { out := make(rastrillo.Catalog, len(english)) for k, v := range english { out[k] = v } return out } // decodeCatalog reads rastrillo's flat catalog format: one key = "value" // per line, # comments and blank lines. rastrillo's own decoder is // internal to it, so this is the same subset: no tables, a dot in a key // is just a character, a value is a basic string (with escapes) or a // literal one. func decodeCatalog(data []byte) (rastrillo.Catalog, error) { out := rastrillo.Catalog{} for i, raw := range strings.Split(string(data), "\n") { line := strings.TrimSpace(raw) if line == "" || strings.HasPrefix(line, "#") { continue } key, rest, ok := strings.Cut(line, "=") key = strings.TrimSpace(key) if !ok || key == "" || strings.ContainsAny(key, " \t[]\"'") { return nil, fmt.Errorf("line %d: want key = \"value\"", i+1) } val, err := catalogValue(strings.TrimSpace(rest)) if err != nil { return nil, fmt.Errorf("line %d: %w", i+1, err) } if _, dup := out[key]; dup { return nil, fmt.Errorf("line %d: duplicate key %q", i+1, key) } out[key] = val } return out, nil } // catalogValue reads a quoted string, then allows only a # comment after it. func catalogValue(s string) (string, error) { var end int switch { case strings.HasPrefix(s, `"`): end = -1 for i := 1; i < len(s); i++ { if s[i] == '\\' { i++ } else if s[i] == '"' { end = i break } } case strings.HasPrefix(s, "'"): end = strings.Index(s[1:], "'") + 1 if end == 0 { end = -1 } default: return "", fmt.Errorf("a value must be a quoted string") } if end < 0 { return "", fmt.Errorf("unterminated string") } if tail := strings.TrimSpace(s[end+1:]); tail != "" && !strings.HasPrefix(tail, "#") { return "", fmt.Errorf("unexpected %q after the string", tail) } if s[0] == '\'' { return s[1:end], nil } // TOML's basic-string escapes are a subset of Go's. return strconv.Unquote(s[:end+1]) }