chore: migrate mail themes to shared framework and locked upstream inputs
This commit is contained in:
1 parent
5c0589f6f6
commit
fec3ace600
225 files changed
+4718
-15807
No files matched your search
+98
-112
@@ -2,129 +2,115 @@ package preview
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitea-mail-templates/tools/upstream"
|
||||
"html/template"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Locale provides English fallback translations, matching Gitea's locale.Tr interface.
|
||||
type Locale struct{}
|
||||
// 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
|
||||
}
|
||||
|
||||
// Tr translates a key and substitutes %s / %d / %[N]s / %[N]d arguments.
|
||||
// HTML arguments are preserved; plain strings are HTML-escaped.
|
||||
func (l Locale) Tr(key string, args ...any) template.HTML {
|
||||
s := LocaleEN[key]
|
||||
if s == "" {
|
||||
s = key
|
||||
}
|
||||
for i, arg := range args {
|
||||
var val string
|
||||
if h, ok := arg.(template.HTML); ok {
|
||||
val = string(h) // already safe HTML
|
||||
} else {
|
||||
val = template.HTMLEscapeString(fmt.Sprint(arg))
|
||||
}
|
||||
// Handle positional format specifiers: %[1]s, %[2]d, etc.
|
||||
pos := fmt.Sprintf("%%[%d]", i+1)
|
||||
s = strings.Replace(s, pos+"s", val, 1)
|
||||
s = strings.Replace(s, pos+"d", val, 1)
|
||||
}
|
||||
// Fallback for non-positional %s / %d (sequential replacement)
|
||||
for _, arg := range args {
|
||||
var val string
|
||||
if h, ok := arg.(template.HTML); ok {
|
||||
val = string(h)
|
||||
} else {
|
||||
val = template.HTMLEscapeString(fmt.Sprint(arg))
|
||||
}
|
||||
if !strings.Contains(s, "%s") && !strings.Contains(s, "%d") {
|
||||
break
|
||||
}
|
||||
s = strings.Replace(s, "%s", val, 1)
|
||||
s = strings.Replace(s, "%d", val, 1)
|
||||
}
|
||||
return template.HTML(s)
|
||||
return template.HTML(l.TrString(key, escapedArgs(args)...))
|
||||
}
|
||||
|
||||
// TrN picks singular/plural by count, then delegates to Tr.
|
||||
func (l Locale) TrN(count any, singular, plural string, args ...any) template.HTML {
|
||||
n := 0
|
||||
switch v := count.(type) {
|
||||
func (l Locale) TrN(count any, one, many string, args ...any) template.HTML {
|
||||
var n int64
|
||||
switch count := count.(type) {
|
||||
case int:
|
||||
n = v
|
||||
n = int64(count)
|
||||
case int16:
|
||||
n = int64(count)
|
||||
case int32:
|
||||
n = int64(count)
|
||||
case int64:
|
||||
n = int(v)
|
||||
case float64:
|
||||
n = int(v)
|
||||
n = count
|
||||
default:
|
||||
n = 1
|
||||
return l.Tr(many, args...)
|
||||
}
|
||||
key := plural
|
||||
if n == 1 {
|
||||
key = singular
|
||||
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
|
||||
}
|
||||
return l.Tr(key, args...)
|
||||
}
|
||||
|
||||
// LocaleEN holds all English translation strings used by Gitea mail templates.
|
||||
var LocaleEN = map[string]string{
|
||||
"mail.hi_user_x": "Hi <b>%s</b>,",
|
||||
"mail.activate_account": "Please activate your account",
|
||||
"mail.activate_account.title": "%s, please activate your account",
|
||||
"mail.activate_account.text_1": "Hi <b>%[1]s</b>, thanks for registering at %[2]s!",
|
||||
"mail.activate_account.text_2": "Please click the following link to activate your account within <b>%s</b>:",
|
||||
"mail.activate_email": "Verify your email address",
|
||||
"mail.activate_email.title": "%s, please verify your email address",
|
||||
"mail.activate_email.text": "Please click the following link to verify your email address within <b>%s</b>:",
|
||||
"mail.register_success": "Registration successful",
|
||||
"mail.register_notify": "Welcome to %s",
|
||||
"mail.register_notify.title": "%[1]s, welcome to %[2]s",
|
||||
"mail.register_notify.text_1": "This is your registration confirmation email for %s!",
|
||||
"mail.register_notify.text_2": "You can now log in via username: %s.",
|
||||
"mail.register_notify.text_3": "If this account has been created for you, please <a href=\"%s\">set your password</a> first.",
|
||||
"mail.reset_password": "Recover your account",
|
||||
"mail.reset_password.title": "%s, you have requested to recover your account",
|
||||
"mail.reset_password.text": "Please click the following link to recover your account within <b>%s</b>:",
|
||||
"mail.team_invite.subject": "%[1]s has invited you to join the %[2]s organization",
|
||||
"mail.team_invite.text_1": "%[1]s has invited you to join team %[2]s in organization %[3]s.",
|
||||
"mail.team_invite.text_2": "Please click the following link to join the team:",
|
||||
"mail.team_invite.text_3": "Note: This invitation was intended for %[1]s. If you were not expecting this invitation, you can ignore this email.",
|
||||
"mail.repo.collaborator.added.subject": "%s added you to %s",
|
||||
"mail.repo.collaborator.added.text": "You have been added as a collaborator of repository:",
|
||||
"mail.repo.transfer.subject_to": "%s would like to transfer \"%s\" to %s",
|
||||
"mail.repo.transfer.subject_to_you": "%s would like to transfer \"%s\" to you",
|
||||
"mail.repo.transfer.to_you": "you",
|
||||
"mail.repo.transfer.body": "To accept or reject it, visit %s or just ignore it.",
|
||||
"mail.release.new.subject": "%s in %s released",
|
||||
"mail.release.new.text": "<b>@%[1]s</b> released %[2]s in %[3]s",
|
||||
"mail.release.title": "Title: %s",
|
||||
"mail.release.note": "Note:",
|
||||
"mail.release.downloads": "Downloads:",
|
||||
"mail.release.download.zip": "Source Code (ZIP)",
|
||||
"mail.release.download.targz": "Source Code (TAR.GZ)",
|
||||
"mail.repo.actions.run.failed": "Run failed",
|
||||
"mail.repo.actions.run.succeeded": "Run succeeded",
|
||||
"mail.repo.actions.run.cancelled": "Run cancelled",
|
||||
"mail.repo.actions.jobs.all_succeeded": "All jobs have succeeded",
|
||||
"mail.repo.actions.jobs.all_failed": "All jobs have failed",
|
||||
"mail.repo.actions.jobs.some_not_successful": "Some jobs were not successful",
|
||||
"mail.repo.actions.jobs.all_cancelled": "All jobs have been cancelled",
|
||||
"mail.issue_assigned.pull": "@%[1]s assigned you to pull request %[2]s in repository %[3]s.",
|
||||
"mail.issue_assigned.issue": "@%[1]s assigned you to issue %[2]s in repository %[3]s.",
|
||||
"mail.issue.x_mentioned_you": "<b>@%s</b> mentioned you:",
|
||||
"mail.issue.action.close": "<b>@%[1]s</b> closed #%[2]d.",
|
||||
"mail.issue.action.reopen": "<b>@%[1]s</b> reopened #%[2]d.",
|
||||
"mail.issue.action.merge": "<b>@%[1]s</b> merged #%[2]d into %[3]s.",
|
||||
"mail.issue.action.approve": "<b>@%[1]s</b> approved this pull request.",
|
||||
"mail.issue.action.reject": "<b>@%[1]s</b> requested changes on this pull request.",
|
||||
"mail.issue.action.review": "<b>@%[1]s</b> commented on this pull request.",
|
||||
"mail.issue.action.review_dismissed": "<b>@%[1]s</b> dismissed last review from %[2]s for this pull request.",
|
||||
"mail.issue.action.ready_for_review": "<b>@%[1]s</b> marked this pull request ready for review.",
|
||||
"mail.issue.action.new": "<b>@%[1]s</b> created #%[2]d.",
|
||||
"mail.issue.action.force_push": "<b>%[1]s</b> force-pushed the <b>%[2]s</b> from %[3]s to %[4]s.",
|
||||
"mail.issue.action.push_1": "<b>@%[1]s</b> pushed %[3]d commit to %[2]s",
|
||||
"mail.issue.action.push_n": "<b>@%[1]s</b> pushed %[3]d commits to %[2]s",
|
||||
"mail.issue.in_tree_path": "In %s:",
|
||||
"mail.view_it_on": "View it on %s",
|
||||
"mail.reply": "or reply to this email directly",
|
||||
"mail.link_not_working_do_paste": "Not working? Try copying and pasting it to your browser.",
|
||||
if singular {
|
||||
return l.Tr(one, args...)
|
||||
}
|
||||
return l.Tr(many, args...)
|
||||
}
|
||||
Reference in new issue
Block a user