//go:generate ./gen.py package i18n import ( "errors" "fmt" "io/fs" "log" "maps" "slices" "strings" "time" "unicode/utf8" "git.thomasvoss.com/euro-cash.eu/pkg/atexit" "git.thomasvoss.com/euro-cash.eu/pkg/watch" "github.com/leonelquinteros/gotext" "git.thomasvoss.com/euro-cash.eu/src/wikipedia" ) type Printer struct { LocaleInfo inner *gotext.Locale } type number interface { int | float64 } type sprintfFunc func(LocaleInfo, *strings.Builder, any) error var ( handlers map[rune]sprintfFunc = map[rune]sprintfFunc{ -1: sprintfGeneric, 'E': sprintfE, 'L': sprintfL, 'e': sprintfe, 'l': sprintfl, 'm': sprintfm, 'p': sprintfp, 'r': sprintfr, } Printers = make(map[string]Printer, len(locales)) DefaultPrinter Printer ) func Init(dir fs.FS, debugp bool) { gotext.FallbackLocale = "en" i := slices.IndexFunc(locales[:], func(li LocaleInfo) bool { return li.Bcp == gotext.FallbackLocale }) if i == -1 { atexit.Exec() log.Fatalf("No translation file default locale ‘%s’\n", gotext.FallbackLocale) } if !locales[i].Enabledp { atexit.Exec() log.Fatalf("Default locale ‘%s’ is not enabled\n", locales[i].Name) } initLocale(dir, locales[i], locales[i].Name, debugp) DefaultPrinter = Printers[gotext.FallbackLocale] for j, li := range locales { if li.Enabledp && i != j { name := DefaultPrinter.GetC(li.Name, "Language Name") initLocale(dir, li, name, debugp) } } } func initLocale(dir fs.FS, li LocaleInfo, name string, debugp bool) { gl := gotext.NewLocaleFS(li.Bcp, dir) gl.AddDomain("messages") Printers[li.Bcp] = Printer{li, gl} if debugp { subdir, err := fs.Sub(dir, li.Bcp) if err != nil { log.Printf("No translations directory for ‘%s’\n", name) return } go watch.FileFS(subdir, "messages.po", func() { Printers[li.Bcp].inner.AddDomain("messages") log.Printf("Translations for ‘%s’ updated\n", name) }) } log.Printf("Initialized printer for ‘%s’\n", name) } func Locales() []LocaleInfo { return locales[:] } func (p Printer) Wikipedia(title string) string { return wikipedia.Url(title, p.Bcp) } func (p Printer) Get(fmt string, args ...map[string]any) string { return p.Sprintf(p.inner.Get(fmt), args...) } func (p Printer) GetC(fmt, ctx string, args ...map[string]any) string { return p.Sprintf(p.inner.GetC(fmt, ctx), args...) } func (p Printer) GetN(fmtS, fmtP string, n int, args ...map[string]any) string { return p.Sprintf(p.inner.GetN(fmtS, fmtP, n), args...) } /* Transform ‘en-US’ to ‘en’ */ func (l LocaleInfo) Language() string { return l.Bcp[:2] } func (p Printer) Itoa(n int) string { var bob strings.Builder writeInt(&bob, n, p.LocaleInfo) return bob.String() } func (p Printer) Ftoa(n float64) string { var bob strings.Builder writeFloat(&bob, n, p.LocaleInfo) return bob.String() } func (p Printer) Itop(n int) string { var bob strings.Builder sprintfp(p.LocaleInfo, &bob, n) return bob.String() } func (p Printer) Ftop(n float64) string { var bob strings.Builder sprintfp(p.LocaleInfo, &bob, n) return bob.String() } func (p Printer) Itom(n int) string { var bob strings.Builder sprintfm(p.LocaleInfo, &bob, n) return bob.String() } func (p Printer) Ftom(n float64) string { var bob strings.Builder sprintfm(p.LocaleInfo, &bob, n) return bob.String() } func (p Printer) Sprintf(format string, args ...map[string]any) string { var bob strings.Builder vars := map[string]any{ "-": "a", "Null": "", } for _, arg := range args { maps.Copy(vars, arg) } for { i := strings.IndexByte(format, '{') if i == -1 { htmlesc(&bob, format) break } htmlesc(&bob, format[:i]) format = format[i+1:] if len(format) == 0 { /* TODO: Handle error: trailing percent */ break } i = strings.IndexRune(format, '}') if i == -1 { /* TODO: Handle error: unterminated { */ return "unterminated {" } parts := strings.Split(format[:i], ":") format = format[i+1:] var flag rune switch len(parts) { case 1: flag = -1 case 2: f, n := utf8.DecodeRune([]byte(parts[1])) if n != len(parts[1]) { /* TODO: Handle error: flag too long or empty */ return "flag too long or empty" } flag = f default: /* TODO: Handle error: too many colons */ return "too many colons" } h, ok := handlers[flag] if !ok { /* TODO: Handle error: no such handler */ return "no such handler" } v, ok := vars[parts[0]] if !ok { /* TODO: Handle error: no such key */ return "no such key" } h(p.LocaleInfo, &bob, v) } return bob.String() } func sprintfGeneric(li LocaleInfo, bob *strings.Builder, v any) error { switch v.(type) { case time.Time: htmlesc(bob, v.(time.Time).Format(li.DateFormat)) case int: writeInt(bob, v.(int), li) case float64: writeFloat(bob, v.(float64), li) case string: htmlesc(bob, v.(string)) default: htmlesc(bob, fmt.Sprint(v)) } return nil } func sprintfe(li LocaleInfo, bob *strings.Builder, v any) error { s, ok := v.(string) if !ok { return errors.New("TODO") } bob.WriteString("") htmlesc(bob, s) bob.WriteString("") return nil } func sprintfE(li LocaleInfo, bob *strings.Builder, v any) error { s, ok := v.(string) if !ok { return errors.New("TODO") } for tag := range strings.SplitSeq(s, ",") { bob.WriteString("') } return nil } func sprintfl(li LocaleInfo, bob *strings.Builder, v any) error { s, ok := v.(string) if !ok { return errors.New("TODO") } bob.WriteString("") return nil } func sprintfL(li LocaleInfo, bob *strings.Builder, v any) error { s, ok := v.(string) if !ok { return errors.New("TODO") } bob.WriteString("") return nil } func sprintfm(li LocaleInfo, bob *strings.Builder, v any) error { var ( fmt string negp bool ) switch v.(type) { case int: negp = v.(int) < 0 case float64: negp = v.(float64) < 0 } fmt = li.MonetaryFormats[btoi(negp)] pre, suf, _ := strings.Cut(fmt, "123") htmlesc(bob, pre) switch v.(type) { case int: writeInt(bob, abs(v.(int)), li) case float64: writeFloat(bob, abs(v.(float64)), li) } htmlesc(bob, suf) return nil } func sprintfp(li LocaleInfo, bob *strings.Builder, v any) error { pre, suf, _ := strings.Cut(li.PercentFormat, "123") htmlesc(bob, pre) switch v.(type) { case int: writeInt(bob, v.(int), li) case float64: writeFloat(bob, v.(float64), li) } htmlesc(bob, suf) return nil } func sprintfr(li LocaleInfo, bob *strings.Builder, v any) error { s, ok := v.(string) if !ok { return errors.New("TODO") } bob.WriteString(s) return nil } func writeInt(bob *strings.Builder, num int, li LocaleInfo) { s := fmt.Sprintf("%d", num) if s[0] == '-' { bob.WriteByte('-') s = s[1:] } n := len(s) c := 3 - n%3 if c == 3 { c = 0 } for i := 0; i < n; i++ { c++ bob.WriteByte(s[i]) if c == 3 && i+1 < n { bob.WriteRune(li.GroupSeparator) c = 0 } } } func writeFloat(bob *strings.Builder, num float64, li LocaleInfo) { s := fmt.Sprintf("%.2f", num) if s[0] == '-' { bob.WriteByte('-') s = s[1:] } n := strings.IndexByte(s, '.') c := 3 - n%3 if c == 3 { c = 0 } for i := 0; i < n; i++ { c++ bob.WriteByte(s[i]) if c == 3 && i+1 < n { bob.WriteRune(li.GroupSeparator) c = 0 } } bob.WriteRune(li.DecimalSeparator) bob.WriteString(s[n+1:]) } func abs[T number](x T) T { if x < 0 { return -x } return x } func btoi(b bool) int { if b { return 1 } return 0 } func htmlesc(bob *strings.Builder, s string) { for _, r := range s { switch r { case '<': bob.WriteString("<") case '>': bob.WriteString(">") case '&': bob.WriteString("&") case '"': bob.WriteString(""") case '\'': bob.WriteString("'") default: bob.WriteRune(r) } } } func htmlescByte(bob *strings.Builder, b byte) { switch b { case '<': bob.WriteString("<") case '>': bob.WriteString(">") case '&': bob.WriteString("&") case '"': bob.WriteString(""") case '\'': bob.WriteString("'") default: bob.WriteByte(b) } }