Files
KenanZhu fec3ace600
Release / Validate Templates (push) Canceled after 0s
Release / Package & Release (push) Canceled after 0s
Release / Update Latest Release Documentation (push) Canceled after 0s
chore: migrate mail themes to shared framework and locked upstream inputs
2026-10-09 18:34:36 +08:00

117 lines
2.9 KiB
Go

package preview
import (
"fmt"
"gitea-mail-templates/tools/upstream"
"html/template"
"reflect"
"strings"
)
// Locale is a lightweight adapter for the translation implementation pinned in
// upstream/modules/translation. Catalogs are always official snapshot data.
type Locale struct {
Lang string
Catalog map[string]string
English map[string]string
Problems *[]error
Warnings *[]string
}
func (l Locale) Language() string { return l.Lang }
func (l Locale) HasKey(key string) bool { _, ok := l.Catalog[key]; return ok }
func (l Locale) problem(err error) {
if l.Problems != nil {
*l.Problems = append(*l.Problems, err)
}
}
func (l Locale) TrString(key string, args ...any) string {
format := l.Catalog[key]
if format == "" {
format = l.English[key]
}
if format == "" {
l.problem(fmt.Errorf("missing translation key %s", key))
format = template.HTMLEscapeString(key)
}
if len(args) == 0 {
return format
}
values := make([]any, 0, len(args))
for _, a := range args {
v := reflect.ValueOf(a)
if v.IsValid() && v.Kind() == reflect.Slice {
if len(args) != 1 {
l.problem(fmt.Errorf("translation %s has uncertain slice arguments", key))
break
}
for i := 0; i < v.Len(); i++ {
values = append(values, v.Index(i).Interface())
}
} else {
values = append(values, a)
}
}
msg := fmt.Sprintf(format, values...)
// Upstream logs formatting failures; a fixture preview must surface them.
if strings.Contains(msg, "%!") && upstream.ReviewedFormattingIssue(l.Lang, key, format) {
if l.Warnings != nil {
*l.Warnings = append(*l.Warnings, "[UPSTREAM-WARN] "+l.Lang+" "+key+": official placeholder %[1]s is malformed; output matches Gitea")
}
} else if strings.Contains(msg, "%!") {
l.problem(fmt.Errorf("translation %s formatting failed: %s", key, msg))
}
return msg
}
func escapedArgs(args []any) []any {
out := append([]any(nil), args...)
for i, a := range out {
switch a := a.(type) {
case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML:
case string:
out[i] = template.HTMLEscapeString(a)
case fmt.Stringer:
out[i] = template.HTMLEscapeString(a.String())
default:
out[i] = template.HTMLEscapeString(fmt.Sprint(a))
}
}
return out
}
func (l Locale) Tr(key string, args ...any) template.HTML {
return template.HTML(l.TrString(key, escapedArgs(args)...))
}
func (l Locale) TrN(count any, one, many string, args ...any) template.HTML {
var n int64
switch count := count.(type) {
case int:
n = int64(count)
case int16:
n = int64(count)
case int32:
n = int64(count)
case int64:
n = count
default:
return l.Tr(many, args...)
}
singular := n == 1
switch l.Lang {
case "zh-CN", "zh-HK", "zh-TW":
singular = true
case "lv-LV", "ru-RU":
singular = n%10 == 1 && n%100 != 11
case "fr-FR":
singular = n > -2 && n < 2
}
if singular {
return l.Tr(one, args...)
}
return l.Tr(many, args...)
}