diff options
Diffstat (limited to 'src/i18n/i18n.go')
-rw-r--r-- | src/i18n/i18n.go | 428 |
1 files changed, 428 insertions, 0 deletions
diff --git a/src/i18n/i18n.go b/src/i18n/i18n.go new file mode 100644 index 0000000..93b8d63 --- /dev/null +++ b/src/i18n/i18n.go @@ -0,0 +1,428 @@ +//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:] + 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("<a href=\"mailto:") + htmlesc(bob, s) + bob.WriteString("\">") + htmlesc(bob, s) + bob.WriteString("</a>") + 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("</") + bob.WriteString(tag) + bob.WriteByte('>') + } + return nil +} + +func sprintfl(li LocaleInfo, bob *strings.Builder, v any) error { + s, ok := v.(string) + if !ok { + return errors.New("TODO") + } + bob.WriteString("<a href=\"") + htmlesc(bob, 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("<a href=\"") + htmlesc(bob, s) + bob.WriteString("\" target=\"_blank\">") + 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) + } +} |