Refactor locale&string&template related code (#29165)

Clarify when "string" should be used (and be escaped), and when
"template.HTML" should be used (no need to escape)

And help PRs like  #29059 , to render the error messages correctly.

(cherry picked from commit f3eb835886031df7a562abc123c3f6011c81eca8)

Conflicts:
	modules/web/middleware/binding.go
	routers/web/feed/convert.go
	tests/integration/branches_test.go
	tests/integration/repo_branch_test.go
	trivial context conflicts
This commit is contained in:
wxiaoguang 2024-02-15 05:48:45 +08:00 committed by Earl Warren
parent d565d85160
commit 65248945c9
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
78 changed files with 359 additions and 286 deletions

View file

@ -8,6 +8,7 @@ import (
"context"
"crypto/rand"
"errors"
"html/template"
"math/big"
"strings"
"sync"
@ -121,15 +122,15 @@ func Generate(n int) (string, error) {
}
// BuildComplexityError builds the error message when password complexity checks fail
func BuildComplexityError(locale translation.Locale) string {
func BuildComplexityError(locale translation.Locale) template.HTML {
var buffer bytes.Buffer
buffer.WriteString(locale.Tr("form.password_complexity"))
buffer.WriteString(locale.TrString("form.password_complexity"))
buffer.WriteString("<ul>")
for _, c := range requiredList {
buffer.WriteString("<li>")
buffer.WriteString(locale.Tr(c.TrNameOne))
buffer.WriteString(locale.TrString(c.TrNameOne))
buffer.WriteString("</li>")
}
buffer.WriteString("</ul>")
return buffer.String()
return template.HTML(buffer.String())
}

View file

@ -173,7 +173,7 @@ func (e *escapeStreamer) ambiguousRune(r, c rune) error {
Val: "ambiguous-code-point",
}, html.Attribute{
Key: "data-tooltip-content",
Val: e.locale.Tr("repo.ambiguous_character", r, c),
Val: e.locale.TrString("repo.ambiguous_character", r, c),
}); err != nil {
return err
}

View file

@ -247,7 +247,7 @@ func APIContexter() func(http.Handler) http.Handler {
// NotFound handles 404s for APIContext
// String will replace message, errors will be added to a slice
func (ctx *APIContext) NotFound(objs ...any) {
message := ctx.Tr("error.not_found")
message := ctx.Locale.TrString("error.not_found")
var errors []string
for _, obj := range objs {
// Ignore nil

View file

@ -6,6 +6,7 @@ package context
import (
"context"
"fmt"
"html/template"
"io"
"net/http"
"net/url"
@ -286,11 +287,11 @@ func (b *Base) cleanUp() {
}
}
func (b *Base) Tr(msg string, args ...any) string {
func (b *Base) Tr(msg string, args ...any) template.HTML {
return b.Locale.Tr(msg, args...)
}
func (b *Base) TrN(cnt any, key1, keyN string, args ...any) string {
func (b *Base) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
return b.Locale.TrN(cnt, key1, keyN, args...)
}

View file

@ -6,7 +6,7 @@ package context
import (
"context"
"html"
"fmt"
"html/template"
"io"
"net/http"
@ -71,16 +71,6 @@ func init() {
})
}
// TrHTMLEscapeArgs runs ".Locale.Tr()" but pre-escapes all arguments with html.EscapeString.
// This is useful if the locale message is intended to only produce HTML content.
func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string {
trArgs := make([]any, len(args))
for i, arg := range args {
trArgs[i] = html.EscapeString(arg)
}
return ctx.Locale.Tr(msg, trArgs...)
}
type webContextKeyType struct{}
var WebContextKey = webContextKeyType{}
@ -253,6 +243,13 @@ func (ctx *Context) JSONOK() {
ctx.JSON(http.StatusOK, map[string]any{"ok": true}) // this is only a dummy response, frontend seldom uses it
}
func (ctx *Context) JSONError(msg string) {
ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": msg})
func (ctx *Context) JSONError(msg any) {
switch v := msg.(type) {
case string:
ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "text"})
case template.HTML:
ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": v, "renderFormat": "html"})
default:
panic(fmt.Sprintf("unsupported type: %T", msg))
}
}

View file

@ -98,12 +98,11 @@ func (ctx *Context) RenderToString(name base.TplName, data map[string]any) (stri
}
// RenderWithErr used for page has form validation but need to prompt error to users.
func (ctx *Context) RenderWithErr(msg string, tpl base.TplName, form any) {
func (ctx *Context) RenderWithErr(msg any, tpl base.TplName, form any) {
if form != nil {
middleware.AssignForm(form, ctx.Data)
}
ctx.Flash.ErrorMsg = msg
ctx.Data["Flash"] = ctx.Flash
ctx.Flash.Error(msg, true)
ctx.HTML(http.StatusOK, tpl)
}

View file

@ -6,6 +6,7 @@ package context
import (
"context"
"errors"
"fmt"
"html"
"net/http"
@ -110,7 +111,7 @@ func (r *Repository) AllUnitsEnabled(ctx context.Context) bool {
func RepoMustNotBeArchived() func(ctx *Context) {
return func(ctx *Context) {
if ctx.Repo.Repository.IsArchived {
ctx.NotFound("IsArchived", fmt.Errorf(ctx.Tr("repo.archive.title")))
ctx.NotFound("IsArchived", errors.New(ctx.Locale.TrString("repo.archive.title")))
}
}
}

View file

@ -123,9 +123,9 @@ func guessDelimiter(data []byte) rune {
func FormatError(err error, locale translation.Locale) (string, error) {
if perr, ok := err.(*stdcsv.ParseError); ok {
if perr.Err == stdcsv.ErrFieldCount {
return locale.Tr("repo.error.csv.invalid_field_count", perr.Line), nil
return locale.TrString("repo.error.csv.invalid_field_count", perr.Line), nil
}
return locale.Tr("repo.error.csv.unexpected", perr.Line, perr.Column), nil
return locale.TrString("repo.error.csv.unexpected", perr.Line, perr.Column), nil
}
return "", err

View file

@ -804,7 +804,7 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
// indicate that in the text by appending (comment)
if m[4] != -1 && m[5] != -1 {
if locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale); ok {
text += " " + locale.Tr("repo.from_comment")
text += " " + locale.TrString("repo.from_comment")
} else {
text += " (comment)"
}

View file

@ -21,7 +21,7 @@ func createTOCNode(toc []markup.Header, lang string, detailsAttrs map[string]str
details.SetAttributeString(k, []byte(v))
}
summary.AppendChild(summary, ast.NewString([]byte(translation.NewLocale(lang).Tr("toc"))))
summary.AppendChild(summary, ast.NewString([]byte(translation.NewLocale(lang).TrString("toc"))))
details.AppendChild(details, summary)
ul := ast.NewList('-')
details.AppendChild(details, ul)

View file

@ -3,7 +3,7 @@
package migration
// Messenger is a formatting function similar to i18n.Tr
// Messenger is a formatting function similar to i18n.TrString
type Messenger func(key string, args ...any)
// NilMessenger represents an empty formatting function

View file

@ -36,7 +36,7 @@ func NewFuncMap() template.FuncMap {
"dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
"Eval": Eval,
"Safe": Safe,
"Escape": html.EscapeString,
"Escape": Escape,
"QueryEscape": url.QueryEscape,
"JSEscape": template.JSEscapeString,
"Str2html": Str2html, // TODO: rename it to SanitizeHTML
@ -162,7 +162,7 @@ func NewFuncMap() template.FuncMap {
"RenderCodeBlock": RenderCodeBlock,
"RenderIssueTitle": RenderIssueTitle,
"RenderEmoji": RenderEmoji,
"RenderEmojiPlain": emoji.ReplaceAliases,
"RenderEmojiPlain": RenderEmojiPlain,
"ReactionToEmoji": ReactionToEmoji,
"RenderMarkdownToHtml": RenderMarkdownToHtml,
@ -183,13 +183,45 @@ func NewFuncMap() template.FuncMap {
}
// Safe render raw as HTML
func Safe(raw string) template.HTML {
return template.HTML(raw)
func Safe(s any) template.HTML {
switch v := s.(type) {
case string:
return template.HTML(v)
case template.HTML:
return v
}
panic(fmt.Sprintf("unexpected type %T", s))
}
// Str2html render Markdown text to HTML
func Str2html(raw string) template.HTML {
return template.HTML(markup.Sanitize(raw))
// Str2html sanitizes the input by pre-defined markdown rules
func Str2html(s any) template.HTML {
switch v := s.(type) {
case string:
return template.HTML(markup.Sanitize(v))
case template.HTML:
return template.HTML(markup.Sanitize(string(v)))
}
panic(fmt.Sprintf("unexpected type %T", s))
}
func Escape(s any) template.HTML {
switch v := s.(type) {
case string:
return template.HTML(html.EscapeString(v))
case template.HTML:
return v
}
panic(fmt.Sprintf("unexpected type %T", s))
}
func RenderEmojiPlain(s any) any {
switch v := s.(type) {
case string:
return emoji.ReplaceAliases(v)
case template.HTML:
return template.HTML(emoji.ReplaceAliases(string(v)))
}
panic(fmt.Sprintf("unexpected type %T", s))
}
// DotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent autolinkers from detecting these as urls

View file

@ -28,54 +28,54 @@ func computeTimeDiffFloor(diff int64, lang translation.Locale) (int64, string) {
switch {
case diff <= 0:
diff = 0
diffStr = lang.Tr("tool.now")
diffStr = lang.TrString("tool.now")
case diff < 2:
diff = 0
diffStr = lang.Tr("tool.1s")
diffStr = lang.TrString("tool.1s")
case diff < 1*Minute:
diffStr = lang.Tr("tool.seconds", diff)
diffStr = lang.TrString("tool.seconds", diff)
diff = 0
case diff < 2*Minute:
diff -= 1 * Minute
diffStr = lang.Tr("tool.1m")
diffStr = lang.TrString("tool.1m")
case diff < 1*Hour:
diffStr = lang.Tr("tool.minutes", diff/Minute)
diffStr = lang.TrString("tool.minutes", diff/Minute)
diff -= diff / Minute * Minute
case diff < 2*Hour:
diff -= 1 * Hour
diffStr = lang.Tr("tool.1h")
diffStr = lang.TrString("tool.1h")
case diff < 1*Day:
diffStr = lang.Tr("tool.hours", diff/Hour)
diffStr = lang.TrString("tool.hours", diff/Hour)
diff -= diff / Hour * Hour
case diff < 2*Day:
diff -= 1 * Day
diffStr = lang.Tr("tool.1d")
diffStr = lang.TrString("tool.1d")
case diff < 1*Week:
diffStr = lang.Tr("tool.days", diff/Day)
diffStr = lang.TrString("tool.days", diff/Day)
diff -= diff / Day * Day
case diff < 2*Week:
diff -= 1 * Week
diffStr = lang.Tr("tool.1w")
diffStr = lang.TrString("tool.1w")
case diff < 1*Month:
diffStr = lang.Tr("tool.weeks", diff/Week)
diffStr = lang.TrString("tool.weeks", diff/Week)
diff -= diff / Week * Week
case diff < 2*Month:
diff -= 1 * Month
diffStr = lang.Tr("tool.1mon")
diffStr = lang.TrString("tool.1mon")
case diff < 1*Year:
diffStr = lang.Tr("tool.months", diff/Month)
diffStr = lang.TrString("tool.months", diff/Month)
diff -= diff / Month * Month
case diff < 2*Year:
diff -= 1 * Year
diffStr = lang.Tr("tool.1y")
diffStr = lang.TrString("tool.1y")
default:
diffStr = lang.Tr("tool.years", diff/Year)
diffStr = lang.TrString("tool.years", diff/Year)
diff -= (diff / Year) * Year
}
return diff, diffStr
@ -97,10 +97,10 @@ func timeSincePro(then, now time.Time, lang translation.Locale) string {
diff := now.Unix() - then.Unix()
if then.After(now) {
return lang.Tr("tool.future")
return lang.TrString("tool.future")
}
if diff == 0 {
return lang.Tr("tool.now")
return lang.TrString("tool.now")
}
var timeStr, diffStr string
@ -115,7 +115,7 @@ func timeSincePro(then, now time.Time, lang translation.Locale) string {
return strings.TrimPrefix(timeStr, ", ")
}
func timeSinceUnix(then, now time.Time, lang translation.Locale) template.HTML {
func timeSinceUnix(then, now time.Time, _ translation.Locale) template.HTML {
friendlyText := then.Format("2006-01-02 15:04:05 -07:00")
// document: https://github.com/github/relative-time-element

View file

@ -4,26 +4,25 @@
package i18n
import (
"html/template"
"io"
)
var DefaultLocales = NewLocaleStore()
type Locale interface {
// Tr translates a given key and arguments for a language
Tr(trKey string, trArgs ...any) string
// Has reports if a locale has a translation for a given key
Has(trKey string) bool
// TrString translates a given key and arguments for a language
TrString(trKey string, trArgs ...any) string
// TrHTML translates a given key and arguments for a language, string arguments are escaped to HTML
TrHTML(trKey string, trArgs ...any) template.HTML
// HasKey reports if a locale has a translation for a given key
HasKey(trKey string) bool
}
// LocaleStore provides the functions common to all locale stores
type LocaleStore interface {
io.Closer
// Tr translates a given key and arguments for a language
Tr(lang, trKey string, trArgs ...any) string
// Has reports if a locale has a translation for a given key
Has(lang, trKey string) bool
// SetDefaultLang sets the default language to fall back to
SetDefaultLang(lang string)
// ListLangNameDesc provides paired slices of language names to descriptors
@ -45,7 +44,7 @@ func ResetDefaultLocales() {
DefaultLocales = NewLocaleStore()
}
// GetLocales returns the locale from the default locales
// GetLocale returns the locale from the default locales
func GetLocale(lang string) (Locale, bool) {
return DefaultLocales.Locale(lang)
}

View file

@ -17,7 +17,7 @@ fmt = %[1]s %[2]s
[section]
sub = Sub String
mixed = test value; <span style="color: red\; background: none;">more text</span>
mixed = test value; <span style="color: red\; background: none;">%s</span>
`)
testData2 := []byte(`
@ -32,29 +32,33 @@ sub = Changed Sub String
assert.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", testData2, nil))
ls.SetDefaultLang("lang1")
result := ls.Tr("lang1", "fmt", "a", "b")
lang1, _ := ls.Locale("lang1")
lang2, _ := ls.Locale("lang2")
result := lang1.TrString("fmt", "a", "b")
assert.Equal(t, "a b", result)
result = ls.Tr("lang2", "fmt", "a", "b")
result = lang2.TrString("fmt", "a", "b")
assert.Equal(t, "b a", result)
result = ls.Tr("lang1", "section.sub")
result = lang1.TrString("section.sub")
assert.Equal(t, "Sub String", result)
result = ls.Tr("lang2", "section.sub")
result = lang2.TrString("section.sub")
assert.Equal(t, "Changed Sub String", result)
result = ls.Tr("", ".dot.name")
langNone, _ := ls.Locale("none")
result = langNone.TrString(".dot.name")
assert.Equal(t, "Dot Name", result)
result = ls.Tr("lang2", "section.mixed")
assert.Equal(t, `test value; <span style="color: red; background: none;">more text</span>`, result)
result2 := lang2.TrHTML("section.mixed", "a&b")
assert.EqualValues(t, `test value; <span style="color: red; background: none;">a&amp;b</span>`, result2)
langs, descs := ls.ListLangNameDesc()
assert.ElementsMatch(t, []string{"lang1", "lang2"}, langs)
assert.ElementsMatch(t, []string{"Lang1", "Lang2"}, descs)
found := ls.Has("lang1", "no-such")
found := lang1.HasKey("no-such")
assert.False(t, found)
assert.NoError(t, ls.Close())
}
@ -72,9 +76,10 @@ c=22
ls := NewLocaleStore()
assert.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, testData2))
assert.Equal(t, "11", ls.Tr("lang1", "a"))
assert.Equal(t, "21", ls.Tr("lang1", "b"))
assert.Equal(t, "22", ls.Tr("lang1", "c"))
lang1, _ := ls.Locale("lang1")
assert.Equal(t, "11", lang1.TrString("a"))
assert.Equal(t, "21", lang1.TrString("b"))
assert.Equal(t, "22", lang1.TrString("c"))
}
func TestLocaleStoreQuirks(t *testing.T) {
@ -110,8 +115,9 @@ func TestLocaleStoreQuirks(t *testing.T) {
for _, testData := range testDataList {
ls := NewLocaleStore()
err := ls.AddLocaleByIni("lang1", "Lang1", []byte("a="+testData.in), nil)
lang1, _ := ls.Locale("lang1")
assert.NoError(t, err, testData.hint)
assert.Equal(t, testData.out, ls.Tr("lang1", "a"), testData.hint)
assert.Equal(t, testData.out, lang1.TrString("a"), testData.hint)
assert.NoError(t, ls.Close())
}

View file

@ -5,6 +5,8 @@ package i18n
import (
"fmt"
"html/template"
"slices"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@ -18,6 +20,8 @@ type locale struct {
idxToMsgMap map[int]string // the map idx is generated by store's trKeyToIdxMap
}
var _ Locale = (*locale)(nil)
type localeStore struct {
// After initializing has finished, these fields are read-only.
langNames []string
@ -88,20 +92,6 @@ func (store *localeStore) SetDefaultLang(lang string) {
store.defaultLang = lang
}
// Tr translates content to target language. fall back to default language.
func (store *localeStore) Tr(lang, trKey string, trArgs ...any) string {
l, _ := store.Locale(lang)
return l.Tr(trKey, trArgs...)
}
// Has returns whether the given language has a translation for the provided key
func (store *localeStore) Has(lang, trKey string) bool {
l, _ := store.Locale(lang)
return l.Has(trKey)
}
// Locale returns the locale for the lang or the default language
func (store *localeStore) Locale(lang string) (Locale, bool) {
l, found := store.localeMap[lang]
@ -116,13 +106,11 @@ func (store *localeStore) Locale(lang string) (Locale, bool) {
return l, found
}
// Close implements io.Closer
func (store *localeStore) Close() error {
return nil
}
// Tr translates content to locale language. fall back to default language.
func (l *locale) Tr(trKey string, trArgs ...any) string {
func (l *locale) TrString(trKey string, trArgs ...any) string {
format := trKey
idx, ok := l.store.trKeyToIdxMap[trKey]
@ -144,8 +132,23 @@ func (l *locale) Tr(trKey string, trArgs ...any) string {
return msg
}
// Has returns whether a key is present in this locale or not
func (l *locale) Has(trKey string) bool {
func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML {
args := slices.Clone(trArgs)
for i, v := range args {
switch v := v.(type) {
case string:
args[i] = template.HTML(template.HTMLEscapeString(v))
case fmt.Stringer:
args[i] = template.HTMLEscapeString(v.String())
default: // int, float, include template.HTML
// do nothing, just use it
}
}
return template.HTML(l.TrString(trKey, args...))
}
// HasKey returns whether a key is present in this locale or not
func (l *locale) HasKey(trKey string) bool {
idx, ok := l.store.trKeyToIdxMap[trKey]
if !ok {
return false

View file

@ -3,7 +3,10 @@
package translation
import "fmt"
import (
"fmt"
"html/template"
)
// MockLocale provides a mocked locale without any translations
type MockLocale struct{}
@ -14,12 +17,16 @@ func (l MockLocale) Language() string {
return "en"
}
func (l MockLocale) Tr(s string, _ ...any) string {
func (l MockLocale) TrString(s string, _ ...any) string {
return s
}
func (l MockLocale) TrN(_cnt any, key1, _keyN string, _args ...any) string {
return key1
func (l MockLocale) Tr(s string, a ...any) template.HTML {
return template.HTML(s)
}
func (l MockLocale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
return template.HTML(key1)
}
func (l MockLocale) PrettyNumber(v any) string {

View file

@ -5,6 +5,7 @@ package translation
import (
"context"
"html/template"
"sort"
"strings"
"sync"
@ -27,8 +28,11 @@ var ContextKey any = &contextKey{}
// Locale represents an interface to translation
type Locale interface {
Language() string
Tr(string, ...any) string
TrN(cnt any, key1, keyN string, args ...any) string
TrString(string, ...any) string
Tr(key string, args ...any) template.HTML
TrN(cnt any, key1, keyN string, args ...any) template.HTML
PrettyNumber(v any) string
}
@ -144,6 +148,8 @@ type locale struct {
msgPrinter *message.Printer
}
var _ Locale = (*locale)(nil)
// NewLocale return a locale
func NewLocale(lang string) Locale {
if lock != nil {
@ -216,8 +222,12 @@ var trNLangRules = map[string]func(int64) int{
},
}
func (l *locale) Tr(s string, args ...any) template.HTML {
return l.TrHTML(s, args...)
}
// TrN returns translated message for plural text translation
func (l *locale) TrN(cnt any, key1, keyN string, args ...any) string {
func (l *locale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
var c int64
if t, ok := cnt.(int); ok {
c = int64(t)

View file

@ -105,44 +105,44 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
trName := field.Tag.Get("locale")
if len(trName) == 0 {
trName = l.Tr("form." + field.Name)
trName = l.TrString("form." + field.Name)
} else {
trName = l.Tr(trName)
trName = l.TrString(trName)
}
switch errs[0].Classification {
case binding.ERR_REQUIRED:
data["ErrorMsg"] = trName + l.Tr("form.require_error")
data["ErrorMsg"] = trName + l.TrString("form.require_error")
case binding.ERR_ALPHA_DASH:
data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_error")
data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_error")
case binding.ERR_ALPHA_DASH_DOT:
data["ErrorMsg"] = trName + l.Tr("form.alpha_dash_dot_error")
data["ErrorMsg"] = trName + l.TrString("form.alpha_dash_dot_error")
case validation.ErrGitRefName:
data["ErrorMsg"] = trName + l.Tr("form.git_ref_name_error")
data["ErrorMsg"] = trName + l.TrString("form.git_ref_name_error")
case binding.ERR_SIZE:
data["ErrorMsg"] = trName + l.Tr("form.size_error", GetSize(field))
data["ErrorMsg"] = trName + l.TrString("form.size_error", GetSize(field))
case binding.ERR_MIN_SIZE:
data["ErrorMsg"] = trName + l.Tr("form.min_size_error", GetMinSize(field))
data["ErrorMsg"] = trName + l.TrString("form.min_size_error", GetMinSize(field))
case binding.ERR_MAX_SIZE:
data["ErrorMsg"] = trName + l.Tr("form.max_size_error", GetMaxSize(field))
data["ErrorMsg"] = trName + l.TrString("form.max_size_error", GetMaxSize(field))
case binding.ERR_EMAIL:
data["ErrorMsg"] = trName + l.Tr("form.email_error")
data["ErrorMsg"] = trName + l.TrString("form.email_error")
case binding.ERR_URL:
data["ErrorMsg"] = trName + l.Tr("form.url_error", errs[0].Message)
data["ErrorMsg"] = trName + l.TrString("form.url_error", errs[0].Message)
case binding.ERR_INCLUDE:
data["ErrorMsg"] = trName + l.Tr("form.include_error", GetInclude(field))
data["ErrorMsg"] = trName + l.TrString("form.include_error", GetInclude(field))
case validation.ErrGlobPattern:
data["ErrorMsg"] = trName + l.Tr("form.glob_pattern_error", errs[0].Message)
data["ErrorMsg"] = trName + l.TrString("form.glob_pattern_error", errs[0].Message)
case validation.ErrRegexPattern:
data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message)
data["ErrorMsg"] = trName + l.TrString("form.regex_pattern_error", errs[0].Message)
case validation.ErrUsername:
if setting.Service.AllowDotsInUsernames {
data["ErrorMsg"] = trName + l.Tr("form.username_error")
data["ErrorMsg"] = trName + l.TrString("form.username_error")
} else {
data["ErrorMsg"] = trName + l.Tr("form.username_error_no_dots")
data["ErrorMsg"] = trName + l.TrString("form.username_error_no_dots")
}
case validation.ErrInvalidGroupTeamMap:
data["ErrorMsg"] = trName + l.Tr("form.invalid_group_team_map_error", errs[0].Message)
data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message)
default:
msg := errs[0].Classification
if msg != "" && errs[0].Message != "" {
@ -151,7 +151,7 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
msg += errs[0].Message
if msg == "" {
msg = l.Tr("form.unknown_error")
msg = l.TrString("form.unknown_error")
}
data["ErrorMsg"] = trName + ": " + msg
}

View file

@ -3,7 +3,11 @@
package middleware
import "net/url"
import (
"fmt"
"html/template"
"net/url"
)
// Flash represents a one time data transfer between two requests.
type Flash struct {
@ -26,26 +30,36 @@ func (f *Flash) set(name, msg string, current ...bool) {
}
}
func flashMsgStringOrHTML(msg any) string {
switch v := msg.(type) {
case string:
return v
case template.HTML:
return string(v)
}
panic(fmt.Sprintf("unknown type: %T", msg))
}
// Error sets error message
func (f *Flash) Error(msg string, current ...bool) {
f.ErrorMsg = msg
f.set("error", msg, current...)
func (f *Flash) Error(msg any, current ...bool) {
f.ErrorMsg = flashMsgStringOrHTML(msg)
f.set("error", f.ErrorMsg, current...)
}
// Warning sets warning message
func (f *Flash) Warning(msg string, current ...bool) {
f.WarningMsg = msg
f.set("warning", msg, current...)
func (f *Flash) Warning(msg any, current ...bool) {
f.WarningMsg = flashMsgStringOrHTML(msg)
f.set("warning", f.WarningMsg, current...)
}
// Info sets info message
func (f *Flash) Info(msg string, current ...bool) {
f.InfoMsg = msg
f.set("info", msg, current...)
func (f *Flash) Info(msg any, current ...bool) {
f.InfoMsg = flashMsgStringOrHTML(msg)
f.set("info", f.InfoMsg, current...)
}
// Success sets success message
func (f *Flash) Success(msg string, current ...bool) {
f.SuccessMsg = msg
f.set("success", msg, current...)
func (f *Flash) Success(msg any, current ...bool) {
f.SuccessMsg = flashMsgStringOrHTML(msg)
f.set("success", f.SuccessMsg, current...)
}