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.
This commit is contained in:
2024-02-15 05:48:45 +08:00
committed by GitHub
parent 94d06be035
commit f3eb835886
77 changed files with 356 additions and 284 deletions

View File

@ -97,7 +97,7 @@ func (r *ActionRunner) StatusName() string {
} }
func (r *ActionRunner) StatusLocaleName(lang translation.Locale) string { func (r *ActionRunner) StatusLocaleName(lang translation.Locale) string {
return lang.Tr("actions.runners.status." + r.StatusName()) return lang.TrString("actions.runners.status." + r.StatusName())
} }
func (r *ActionRunner) IsOnline() bool { func (r *ActionRunner) IsOnline() bool {

View File

@ -41,7 +41,7 @@ func (s Status) String() string {
// LocaleString returns the locale string name of the Status // LocaleString returns the locale string name of the Status
func (s Status) LocaleString(lang translation.Locale) string { func (s Status) LocaleString(lang translation.Locale) string {
return lang.Tr("actions.status." + s.String()) return lang.TrString("actions.status." + s.String())
} }
// IsDone returns whether the Status is final // IsDone returns whether the Status is final

View File

@ -194,7 +194,7 @@ func (status *CommitStatus) APIURL(ctx context.Context) string {
// LocaleString returns the locale string name of the Status // LocaleString returns the locale string name of the Status
func (status *CommitStatus) LocaleString(lang translation.Locale) string { func (status *CommitStatus) LocaleString(lang translation.Locale) string {
return lang.Tr("repo.commitstatus." + status.State.String()) return lang.TrString("repo.commitstatus." + status.State.String())
} }
// CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc // CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc

View File

@ -210,12 +210,12 @@ const (
// LocaleString returns the locale string name of the role // LocaleString returns the locale string name of the role
func (r RoleInRepo) LocaleString(lang translation.Locale) string { func (r RoleInRepo) LocaleString(lang translation.Locale) string {
return lang.Tr("repo.issues.role." + string(r)) return lang.TrString("repo.issues.role." + string(r))
} }
// LocaleHelper returns the locale tooltip of the role // LocaleHelper returns the locale tooltip of the role
func (r RoleInRepo) LocaleHelper(lang translation.Locale) string { func (r RoleInRepo) LocaleHelper(lang translation.Locale) string {
return lang.Tr("repo.issues.role." + string(r) + "_helper") return lang.TrString("repo.issues.role." + string(r) + "_helper")
} }
// Comment represents a comment in commit and issue page. // Comment represents a comment in commit and issue page.

View File

@ -17,13 +17,13 @@ const (
func (o OwnerType) LocaleString(locale translation.Locale) string { func (o OwnerType) LocaleString(locale translation.Locale) string {
switch o { switch o {
case OwnerTypeSystemGlobal: case OwnerTypeSystemGlobal:
return locale.Tr("concept_system_global") return locale.TrString("concept_system_global")
case OwnerTypeIndividual: case OwnerTypeIndividual:
return locale.Tr("concept_user_individual") return locale.TrString("concept_user_individual")
case OwnerTypeRepository: case OwnerTypeRepository:
return locale.Tr("concept_code_repository") return locale.TrString("concept_code_repository")
case OwnerTypeOrganization: case OwnerTypeOrganization:
return locale.Tr("concept_user_organization") return locale.TrString("concept_user_organization")
} }
return locale.Tr("unknown") return locale.TrString("unknown")
} }

View File

@ -8,6 +8,7 @@ import (
"context" "context"
"crypto/rand" "crypto/rand"
"errors" "errors"
"html/template"
"math/big" "math/big"
"strings" "strings"
"sync" "sync"
@ -121,15 +122,15 @@ func Generate(n int) (string, error) {
} }
// BuildComplexityError builds the error message when password complexity checks fail // 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 var buffer bytes.Buffer
buffer.WriteString(locale.Tr("form.password_complexity")) buffer.WriteString(locale.TrString("form.password_complexity"))
buffer.WriteString("<ul>") buffer.WriteString("<ul>")
for _, c := range requiredList { for _, c := range requiredList {
buffer.WriteString("<li>") buffer.WriteString("<li>")
buffer.WriteString(locale.Tr(c.TrNameOne)) buffer.WriteString(locale.TrString(c.TrNameOne))
buffer.WriteString("</li>") buffer.WriteString("</li>")
} }
buffer.WriteString("</ul>") 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", Val: "ambiguous-code-point",
}, html.Attribute{ }, html.Attribute{
Key: "data-tooltip-content", Key: "data-tooltip-content",
Val: e.locale.Tr("repo.ambiguous_character", r, c), Val: e.locale.TrString("repo.ambiguous_character", r, c),
}); err != nil { }); err != nil {
return err return err
} }

View File

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

View File

@ -6,6 +6,7 @@ package context
import ( import (
"context" "context"
"fmt" "fmt"
"html/template"
"io" "io"
"net/http" "net/http"
"net/url" "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...) 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...) return b.Locale.TrN(cnt, key1, keyN, args...)
} }

View File

@ -6,7 +6,7 @@ package context
import ( import (
"context" "context"
"html" "fmt"
"html/template" "html/template"
"io" "io"
"net/http" "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{} type webContextKeyType struct{}
var WebContextKey = webContextKeyType{} 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 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) { func (ctx *Context) JSONError(msg any) {
ctx.JSON(http.StatusBadRequest, map[string]any{"errorMessage": msg}) 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. // 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 { if form != nil {
middleware.AssignForm(form, ctx.Data) middleware.AssignForm(form, ctx.Data)
} }
ctx.Flash.ErrorMsg = msg ctx.Flash.Error(msg, true)
ctx.Data["Flash"] = ctx.Flash
ctx.HTML(http.StatusOK, tpl) ctx.HTML(http.StatusOK, tpl)
} }

View File

@ -6,6 +6,7 @@ package context
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"html" "html"
"net/http" "net/http"
@ -85,7 +86,7 @@ func (r *Repository) CanCreateBranch() bool {
func RepoMustNotBeArchived() func(ctx *Context) { func RepoMustNotBeArchived() func(ctx *Context) {
return func(ctx *Context) { return func(ctx *Context) {
if ctx.Repo.Repository.IsArchived { 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) { func FormatError(err error, locale translation.Locale) (string, error) {
if perr, ok := err.(*stdcsv.ParseError); ok { if perr, ok := err.(*stdcsv.ParseError); ok {
if perr.Err == stdcsv.ErrFieldCount { 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 return "", err

View File

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

View File

@ -21,7 +21,7 @@ func createTOCNode(toc []markup.Header, lang string, detailsAttrs map[string]str
details.SetAttributeString(k, []byte(v)) 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) details.AppendChild(details, summary)
ul := ast.NewList('-') ul := ast.NewList('-')
details.AppendChild(details, ul) details.AppendChild(details, ul)

View File

@ -3,7 +3,7 @@
package migration 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) type Messenger func(key string, args ...any)
// NilMessenger represents an empty formatting function // 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. "dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
"Eval": Eval, "Eval": Eval,
"Safe": Safe, "Safe": Safe,
"Escape": html.EscapeString, "Escape": Escape,
"QueryEscape": url.QueryEscape, "QueryEscape": url.QueryEscape,
"JSEscape": template.JSEscapeString, "JSEscape": template.JSEscapeString,
"Str2html": Str2html, // TODO: rename it to SanitizeHTML "Str2html": Str2html, // TODO: rename it to SanitizeHTML
@ -159,7 +159,7 @@ func NewFuncMap() template.FuncMap {
"RenderCodeBlock": RenderCodeBlock, "RenderCodeBlock": RenderCodeBlock,
"RenderIssueTitle": RenderIssueTitle, "RenderIssueTitle": RenderIssueTitle,
"RenderEmoji": RenderEmoji, "RenderEmoji": RenderEmoji,
"RenderEmojiPlain": emoji.ReplaceAliases, "RenderEmojiPlain": RenderEmojiPlain,
"ReactionToEmoji": ReactionToEmoji, "ReactionToEmoji": ReactionToEmoji,
"RenderMarkdownToHtml": RenderMarkdownToHtml, "RenderMarkdownToHtml": RenderMarkdownToHtml,
@ -180,13 +180,45 @@ func NewFuncMap() template.FuncMap {
} }
// Safe render raw as HTML // Safe render raw as HTML
func Safe(raw string) template.HTML { func Safe(s any) template.HTML {
return template.HTML(raw) 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 // Str2html sanitizes the input by pre-defined markdown rules
func Str2html(raw string) template.HTML { func Str2html(s any) template.HTML {
return template.HTML(markup.Sanitize(raw)) 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 // 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 { switch {
case diff <= 0: case diff <= 0:
diff = 0 diff = 0
diffStr = lang.Tr("tool.now") diffStr = lang.TrString("tool.now")
case diff < 2: case diff < 2:
diff = 0 diff = 0
diffStr = lang.Tr("tool.1s") diffStr = lang.TrString("tool.1s")
case diff < 1*Minute: case diff < 1*Minute:
diffStr = lang.Tr("tool.seconds", diff) diffStr = lang.TrString("tool.seconds", diff)
diff = 0 diff = 0
case diff < 2*Minute: case diff < 2*Minute:
diff -= 1 * Minute diff -= 1 * Minute
diffStr = lang.Tr("tool.1m") diffStr = lang.TrString("tool.1m")
case diff < 1*Hour: case diff < 1*Hour:
diffStr = lang.Tr("tool.minutes", diff/Minute) diffStr = lang.TrString("tool.minutes", diff/Minute)
diff -= diff / Minute * Minute diff -= diff / Minute * Minute
case diff < 2*Hour: case diff < 2*Hour:
diff -= 1 * Hour diff -= 1 * Hour
diffStr = lang.Tr("tool.1h") diffStr = lang.TrString("tool.1h")
case diff < 1*Day: case diff < 1*Day:
diffStr = lang.Tr("tool.hours", diff/Hour) diffStr = lang.TrString("tool.hours", diff/Hour)
diff -= diff / Hour * Hour diff -= diff / Hour * Hour
case diff < 2*Day: case diff < 2*Day:
diff -= 1 * Day diff -= 1 * Day
diffStr = lang.Tr("tool.1d") diffStr = lang.TrString("tool.1d")
case diff < 1*Week: case diff < 1*Week:
diffStr = lang.Tr("tool.days", diff/Day) diffStr = lang.TrString("tool.days", diff/Day)
diff -= diff / Day * Day diff -= diff / Day * Day
case diff < 2*Week: case diff < 2*Week:
diff -= 1 * Week diff -= 1 * Week
diffStr = lang.Tr("tool.1w") diffStr = lang.TrString("tool.1w")
case diff < 1*Month: case diff < 1*Month:
diffStr = lang.Tr("tool.weeks", diff/Week) diffStr = lang.TrString("tool.weeks", diff/Week)
diff -= diff / Week * Week diff -= diff / Week * Week
case diff < 2*Month: case diff < 2*Month:
diff -= 1 * Month diff -= 1 * Month
diffStr = lang.Tr("tool.1mon") diffStr = lang.TrString("tool.1mon")
case diff < 1*Year: case diff < 1*Year:
diffStr = lang.Tr("tool.months", diff/Month) diffStr = lang.TrString("tool.months", diff/Month)
diff -= diff / Month * Month diff -= diff / Month * Month
case diff < 2*Year: case diff < 2*Year:
diff -= 1 * Year diff -= 1 * Year
diffStr = lang.Tr("tool.1y") diffStr = lang.TrString("tool.1y")
default: default:
diffStr = lang.Tr("tool.years", diff/Year) diffStr = lang.TrString("tool.years", diff/Year)
diff -= (diff / Year) * Year diff -= (diff / Year) * Year
} }
return diff, diffStr return diff, diffStr
@ -97,10 +97,10 @@ func timeSincePro(then, now time.Time, lang translation.Locale) string {
diff := now.Unix() - then.Unix() diff := now.Unix() - then.Unix()
if then.After(now) { if then.After(now) {
return lang.Tr("tool.future") return lang.TrString("tool.future")
} }
if diff == 0 { if diff == 0 {
return lang.Tr("tool.now") return lang.TrString("tool.now")
} }
var timeStr, diffStr string var timeStr, diffStr string
@ -115,7 +115,7 @@ func timeSincePro(then, now time.Time, lang translation.Locale) string {
return strings.TrimPrefix(timeStr, ", ") 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") friendlyText := then.Format("2006-01-02 15:04:05 -07:00")
// document: https://github.com/github/relative-time-element // document: https://github.com/github/relative-time-element

View File

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

View File

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

View File

@ -5,6 +5,8 @@ package i18n
import ( import (
"fmt" "fmt"
"html/template"
"slices"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "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 idxToMsgMap map[int]string // the map idx is generated by store's trKeyToIdxMap
} }
var _ Locale = (*locale)(nil)
type localeStore struct { type localeStore struct {
// After initializing has finished, these fields are read-only. // After initializing has finished, these fields are read-only.
langNames []string langNames []string
@ -85,20 +89,6 @@ func (store *localeStore) SetDefaultLang(lang string) {
store.defaultLang = lang 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 // Locale returns the locale for the lang or the default language
func (store *localeStore) Locale(lang string) (Locale, bool) { func (store *localeStore) Locale(lang string) (Locale, bool) {
l, found := store.localeMap[lang] l, found := store.localeMap[lang]
@ -113,13 +103,11 @@ func (store *localeStore) Locale(lang string) (Locale, bool) {
return l, found return l, found
} }
// Close implements io.Closer
func (store *localeStore) Close() error { func (store *localeStore) Close() error {
return nil return nil
} }
// Tr translates content to locale language. fall back to default language. func (l *locale) TrString(trKey string, trArgs ...any) string {
func (l *locale) Tr(trKey string, trArgs ...any) string {
format := trKey format := trKey
idx, ok := l.store.trKeyToIdxMap[trKey] idx, ok := l.store.trKeyToIdxMap[trKey]
@ -141,8 +129,23 @@ func (l *locale) Tr(trKey string, trArgs ...any) string {
return msg return msg
} }
// Has returns whether a key is present in this locale or not func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML {
func (l *locale) Has(trKey string) bool { 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] idx, ok := l.store.trKeyToIdxMap[trKey]
if !ok { if !ok {
return false return false

View File

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

View File

@ -5,6 +5,7 @@ package translation
import ( import (
"context" "context"
"html/template"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@ -27,8 +28,11 @@ var ContextKey any = &contextKey{}
// Locale represents an interface to translation // Locale represents an interface to translation
type Locale interface { type Locale interface {
Language() string Language() string
Tr(string, ...any) string TrString(string, ...any) string
TrN(cnt any, key1, keyN string, args ...any) string
Tr(key string, args ...any) template.HTML
TrN(cnt any, key1, keyN string, args ...any) template.HTML
PrettyNumber(v any) string PrettyNumber(v any) string
} }
@ -144,6 +148,8 @@ type locale struct {
msgPrinter *message.Printer msgPrinter *message.Printer
} }
var _ Locale = (*locale)(nil)
// NewLocale return a locale // NewLocale return a locale
func NewLocale(lang string) Locale { func NewLocale(lang string) Locale {
if lock != nil { 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 // 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 var c int64
if t, ok := cnt.(int); ok { if t, ok := cnt.(int); ok {
c = int64(t) c = int64(t)

View File

@ -104,40 +104,40 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
trName := field.Tag.Get("locale") trName := field.Tag.Get("locale")
if len(trName) == 0 { if len(trName) == 0 {
trName = l.Tr("form." + field.Name) trName = l.TrString("form." + field.Name)
} else { } else {
trName = l.Tr(trName) trName = l.TrString(trName)
} }
switch errs[0].Classification { switch errs[0].Classification {
case binding.ERR_REQUIRED: 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: 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: 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: 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: 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: 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: 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: case binding.ERR_EMAIL:
data["ErrorMsg"] = trName + l.Tr("form.email_error") data["ErrorMsg"] = trName + l.TrString("form.email_error")
case binding.ERR_URL: 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: 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: 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: 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: case validation.ErrUsername:
data["ErrorMsg"] = trName + l.Tr("form.username_error") data["ErrorMsg"] = trName + l.TrString("form.username_error")
case validation.ErrInvalidGroupTeamMap: 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: default:
msg := errs[0].Classification msg := errs[0].Classification
if msg != "" && errs[0].Message != "" { if msg != "" && errs[0].Message != "" {
@ -146,7 +146,7 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
msg += errs[0].Message msg += errs[0].Message
if msg == "" { if msg == "" {
msg = l.Tr("form.unknown_error") msg = l.TrString("form.unknown_error")
} }
data["ErrorMsg"] = trName + ": " + msg data["ErrorMsg"] = trName + ": " + msg
} }

View File

@ -3,7 +3,11 @@
package middleware package middleware
import "net/url" import (
"fmt"
"html/template"
"net/url"
)
// Flash represents a one time data transfer between two requests. // Flash represents a one time data transfer between two requests.
type Flash struct { 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 // Error sets error message
func (f *Flash) Error(msg string, current ...bool) { func (f *Flash) Error(msg any, current ...bool) {
f.ErrorMsg = msg f.ErrorMsg = flashMsgStringOrHTML(msg)
f.set("error", msg, current...) f.set("error", f.ErrorMsg, current...)
} }
// Warning sets warning message // Warning sets warning message
func (f *Flash) Warning(msg string, current ...bool) { func (f *Flash) Warning(msg any, current ...bool) {
f.WarningMsg = msg f.WarningMsg = flashMsgStringOrHTML(msg)
f.set("warning", msg, current...) f.set("warning", f.WarningMsg, current...)
} }
// Info sets info message // Info sets info message
func (f *Flash) Info(msg string, current ...bool) { func (f *Flash) Info(msg any, current ...bool) {
f.InfoMsg = msg f.InfoMsg = flashMsgStringOrHTML(msg)
f.set("info", msg, current...) f.set("info", f.InfoMsg, current...)
} }
// Success sets success message // Success sets success message
func (f *Flash) Success(msg string, current ...bool) { func (f *Flash) Success(msg any, current ...bool) {
f.SuccessMsg = msg f.SuccessMsg = flashMsgStringOrHTML(msg)
f.set("success", msg, current...) f.set("success", f.SuccessMsg, current...)
} }

View File

@ -762,13 +762,13 @@ func changeFilesCommitMessage(ctx *context.APIContext, files []*files_service.Ch
} }
message := "" message := ""
if len(createFiles) != 0 { if len(createFiles) != 0 {
message += ctx.Tr("repo.editor.add", strings.Join(createFiles, ", ")+"\n") message += ctx.Locale.TrString("repo.editor.add", strings.Join(createFiles, ", ")+"\n")
} }
if len(updateFiles) != 0 { if len(updateFiles) != 0 {
message += ctx.Tr("repo.editor.update", strings.Join(updateFiles, ", ")+"\n") message += ctx.Locale.TrString("repo.editor.update", strings.Join(updateFiles, ", ")+"\n")
} }
if len(deleteFiles) != 0 { if len(deleteFiles) != 0 {
message += ctx.Tr("repo.editor.delete", strings.Join(deleteFiles, ", ")) message += ctx.Locale.TrString("repo.editor.delete", strings.Join(deleteFiles, ", "))
} }
return strings.Trim(message, "\n") return strings.Trim(message, "\n")
} }

View File

@ -395,7 +395,7 @@ func CreateIssueComment(ctx *context.APIContext) {
} }
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin { if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Tr("repo.issues.comment_on_locked"))) ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Locale.TrString("repo.issues.comment_on_locked")))
return return
} }

View File

@ -210,16 +210,16 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi.Source, error) { func parseSSPIConfig(ctx *context.Context, form forms.AuthenticationForm) (*sspi.Source, error) {
if util.IsEmptyString(form.SSPISeparatorReplacement) { if util.IsEmptyString(form.SSPISeparatorReplacement) {
ctx.Data["Err_SSPISeparatorReplacement"] = true ctx.Data["Err_SSPISeparatorReplacement"] = true
return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.require_error")) return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.require_error"))
} }
if separatorAntiPattern.MatchString(form.SSPISeparatorReplacement) { if separatorAntiPattern.MatchString(form.SSPISeparatorReplacement) {
ctx.Data["Err_SSPISeparatorReplacement"] = true ctx.Data["Err_SSPISeparatorReplacement"] = true
return nil, errors.New(ctx.Tr("form.SSPISeparatorReplacement") + ctx.Tr("form.alpha_dash_dot_error")) return nil, errors.New(ctx.Locale.TrString("form.SSPISeparatorReplacement") + ctx.Locale.TrString("form.alpha_dash_dot_error"))
} }
if form.SSPIDefaultLanguage != "" && !langCodePattern.MatchString(form.SSPIDefaultLanguage) { if form.SSPIDefaultLanguage != "" && !langCodePattern.MatchString(form.SSPIDefaultLanguage) {
ctx.Data["Err_SSPIDefaultLanguage"] = true ctx.Data["Err_SSPIDefaultLanguage"] = true
return nil, errors.New(ctx.Tr("form.lang_select_error")) return nil, errors.New(ctx.Locale.TrString("form.lang_select_error"))
} }
return &sspi.Source{ return &sspi.Source{

View File

@ -37,7 +37,7 @@ func ForgotPasswd(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title") ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title")
if setting.MailService == nil { if setting.MailService == nil {
log.Warn(ctx.Tr("auth.disable_forgot_password_mail_admin")) log.Warn("no mail service configured")
ctx.Data["IsResetDisable"] = true ctx.Data["IsResetDisable"] = true
ctx.HTML(http.StatusOK, tplForgotPassword) ctx.HTML(http.StatusOK, tplForgotPassword)
return return

View File

@ -6,6 +6,7 @@ package feed
import ( import (
"fmt" "fmt"
"html" "html"
"html/template"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
@ -79,119 +80,120 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
// title // title
title = act.ActUser.DisplayName() + " " title = act.ActUser.DisplayName() + " "
var titleExtra template.HTML
switch act.OpType { switch act.OpType {
case activities_model.ActionCreateRepo: case activities_model.ActionCreateRepo:
title += ctx.TrHTMLEscapeArgs("action.create_repo", act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.create_repo", act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
link.Href = act.GetRepoAbsoluteLink(ctx) link.Href = act.GetRepoAbsoluteLink(ctx)
case activities_model.ActionRenameRepo: case activities_model.ActionRenameRepo:
title += ctx.TrHTMLEscapeArgs("action.rename_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.rename_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
link.Href = act.GetRepoAbsoluteLink(ctx) link.Href = act.GetRepoAbsoluteLink(ctx)
case activities_model.ActionCommitRepo: case activities_model.ActionCommitRepo:
link.Href = toBranchLink(ctx, act) link.Href = toBranchLink(ctx, act)
if len(act.Content) != 0 { if len(act.Content) != 0 {
title += ctx.TrHTMLEscapeArgs("action.commit_repo", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.commit_repo", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
} else { } else {
title += ctx.TrHTMLEscapeArgs("action.create_branch", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.create_branch", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetBranch(), act.ShortRepoPath(ctx))
} }
case activities_model.ActionCreateIssue: case activities_model.ActionCreateIssue:
link.Href = toIssueLink(ctx, act) link.Href = toIssueLink(ctx, act)
title += ctx.TrHTMLEscapeArgs("action.create_issue", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.create_issue", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionCreatePullRequest: case activities_model.ActionCreatePullRequest:
link.Href = toPullLink(ctx, act) link.Href = toPullLink(ctx, act)
title += ctx.TrHTMLEscapeArgs("action.create_pull_request", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.create_pull_request", link.Href, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionTransferRepo: case activities_model.ActionTransferRepo:
link.Href = act.GetRepoAbsoluteLink(ctx) link.Href = act.GetRepoAbsoluteLink(ctx)
title += ctx.TrHTMLEscapeArgs("action.transfer_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.transfer_repo", act.GetContent(), act.GetRepoAbsoluteLink(ctx), act.ShortRepoPath(ctx))
case activities_model.ActionPushTag: case activities_model.ActionPushTag:
link.Href = toTagLink(ctx, act) link.Href = toTagLink(ctx, act)
title += ctx.TrHTMLEscapeArgs("action.push_tag", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetTag(), act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.push_tag", act.GetRepoAbsoluteLink(ctx), link.Href, act.GetTag(), act.ShortRepoPath(ctx))
case activities_model.ActionCommentIssue: case activities_model.ActionCommentIssue:
issueLink := toIssueLink(ctx, act) issueLink := toIssueLink(ctx, act)
if link.Href == "#" { if link.Href == "#" {
link.Href = issueLink link.Href = issueLink
} }
title += ctx.TrHTMLEscapeArgs("action.comment_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.comment_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionMergePullRequest: case activities_model.ActionMergePullRequest:
pullLink := toPullLink(ctx, act) pullLink := toPullLink(ctx, act)
if link.Href == "#" { if link.Href == "#" {
link.Href = pullLink link.Href = pullLink
} }
title += ctx.TrHTMLEscapeArgs("action.merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionAutoMergePullRequest: case activities_model.ActionAutoMergePullRequest:
pullLink := toPullLink(ctx, act) pullLink := toPullLink(ctx, act)
if link.Href == "#" { if link.Href == "#" {
link.Href = pullLink link.Href = pullLink
} }
title += ctx.TrHTMLEscapeArgs("action.auto_merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.auto_merge_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionCloseIssue: case activities_model.ActionCloseIssue:
issueLink := toIssueLink(ctx, act) issueLink := toIssueLink(ctx, act)
if link.Href == "#" { if link.Href == "#" {
link.Href = issueLink link.Href = issueLink
} }
title += ctx.TrHTMLEscapeArgs("action.close_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.close_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionReopenIssue: case activities_model.ActionReopenIssue:
issueLink := toIssueLink(ctx, act) issueLink := toIssueLink(ctx, act)
if link.Href == "#" { if link.Href == "#" {
link.Href = issueLink link.Href = issueLink
} }
title += ctx.TrHTMLEscapeArgs("action.reopen_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.reopen_issue", issueLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionClosePullRequest: case activities_model.ActionClosePullRequest:
pullLink := toPullLink(ctx, act) pullLink := toPullLink(ctx, act)
if link.Href == "#" { if link.Href == "#" {
link.Href = pullLink link.Href = pullLink
} }
title += ctx.TrHTMLEscapeArgs("action.close_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.close_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionReopenPullRequest: case activities_model.ActionReopenPullRequest:
pullLink := toPullLink(ctx, act) pullLink := toPullLink(ctx, act)
if link.Href == "#" { if link.Href == "#" {
link.Href = pullLink link.Href = pullLink
} }
title += ctx.TrHTMLEscapeArgs("action.reopen_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.reopen_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionDeleteTag: case activities_model.ActionDeleteTag:
link.Href = act.GetRepoAbsoluteLink(ctx) link.Href = act.GetRepoAbsoluteLink(ctx)
title += ctx.TrHTMLEscapeArgs("action.delete_tag", act.GetRepoAbsoluteLink(ctx), act.GetTag(), act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.delete_tag", act.GetRepoAbsoluteLink(ctx), act.GetTag(), act.ShortRepoPath(ctx))
case activities_model.ActionDeleteBranch: case activities_model.ActionDeleteBranch:
link.Href = act.GetRepoAbsoluteLink(ctx) link.Href = act.GetRepoAbsoluteLink(ctx)
title += ctx.TrHTMLEscapeArgs("action.delete_branch", act.GetRepoAbsoluteLink(ctx), html.EscapeString(act.GetBranch()), act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.delete_branch", act.GetRepoAbsoluteLink(ctx), html.EscapeString(act.GetBranch()), act.ShortRepoPath(ctx))
case activities_model.ActionMirrorSyncPush: case activities_model.ActionMirrorSyncPush:
srcLink := toSrcLink(ctx, act) srcLink := toSrcLink(ctx, act)
if link.Href == "#" { if link.Href == "#" {
link.Href = srcLink link.Href = srcLink
} }
title += ctx.TrHTMLEscapeArgs("action.mirror_sync_push", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.mirror_sync_push", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
case activities_model.ActionMirrorSyncCreate: case activities_model.ActionMirrorSyncCreate:
srcLink := toSrcLink(ctx, act) srcLink := toSrcLink(ctx, act)
if link.Href == "#" { if link.Href == "#" {
link.Href = srcLink link.Href = srcLink
} }
title += ctx.TrHTMLEscapeArgs("action.mirror_sync_create", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.mirror_sync_create", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
case activities_model.ActionMirrorSyncDelete: case activities_model.ActionMirrorSyncDelete:
link.Href = act.GetRepoAbsoluteLink(ctx) link.Href = act.GetRepoAbsoluteLink(ctx)
title += ctx.TrHTMLEscapeArgs("action.mirror_sync_delete", act.GetRepoAbsoluteLink(ctx), act.GetBranch(), act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.mirror_sync_delete", act.GetRepoAbsoluteLink(ctx), act.GetBranch(), act.ShortRepoPath(ctx))
case activities_model.ActionApprovePullRequest: case activities_model.ActionApprovePullRequest:
pullLink := toPullLink(ctx, act) pullLink := toPullLink(ctx, act)
title += ctx.TrHTMLEscapeArgs("action.approve_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.approve_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionRejectPullRequest: case activities_model.ActionRejectPullRequest:
pullLink := toPullLink(ctx, act) pullLink := toPullLink(ctx, act)
title += ctx.TrHTMLEscapeArgs("action.reject_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.reject_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionCommentPull: case activities_model.ActionCommentPull:
pullLink := toPullLink(ctx, act) pullLink := toPullLink(ctx, act)
title += ctx.TrHTMLEscapeArgs("action.comment_pull", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.comment_pull", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
case activities_model.ActionPublishRelease: case activities_model.ActionPublishRelease:
releaseLink := toReleaseLink(ctx, act) releaseLink := toReleaseLink(ctx, act)
if link.Href == "#" { if link.Href == "#" {
link.Href = releaseLink link.Href = releaseLink
} }
title += ctx.TrHTMLEscapeArgs("action.publish_release", act.GetRepoAbsoluteLink(ctx), releaseLink, act.ShortRepoPath(ctx), act.Content) titleExtra = ctx.Locale.Tr("action.publish_release", act.GetRepoAbsoluteLink(ctx), releaseLink, act.ShortRepoPath(ctx), act.Content)
case activities_model.ActionPullReviewDismissed: case activities_model.ActionPullReviewDismissed:
pullLink := toPullLink(ctx, act) pullLink := toPullLink(ctx, act)
title += ctx.TrHTMLEscapeArgs("action.review_dismissed", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx), act.GetIssueInfos()[1]) titleExtra = ctx.Locale.Tr("action.review_dismissed", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx), act.GetIssueInfos()[1])
case activities_model.ActionStarRepo: case activities_model.ActionStarRepo:
link.Href = act.GetRepoAbsoluteLink(ctx) link.Href = act.GetRepoAbsoluteLink(ctx)
title += ctx.TrHTMLEscapeArgs("action.starred_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.starred_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
case activities_model.ActionWatchRepo: case activities_model.ActionWatchRepo:
link.Href = act.GetRepoAbsoluteLink(ctx) link.Href = act.GetRepoAbsoluteLink(ctx)
title += ctx.TrHTMLEscapeArgs("action.watched_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx)) titleExtra = ctx.Locale.Tr("action.watched_repo", act.GetRepoAbsoluteLink(ctx), act.GetRepoPath(ctx))
default: default:
return nil, fmt.Errorf("unknown action type: %v", act.OpType) return nil, fmt.Errorf("unknown action type: %v", act.OpType)
} }
@ -233,7 +235,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
case activities_model.ActionCloseIssue, activities_model.ActionReopenIssue, activities_model.ActionClosePullRequest, activities_model.ActionReopenPullRequest: case activities_model.ActionCloseIssue, activities_model.ActionReopenIssue, activities_model.ActionClosePullRequest, activities_model.ActionReopenPullRequest:
desc = act.GetIssueTitle(ctx) desc = act.GetIssueTitle(ctx)
case activities_model.ActionPullReviewDismissed: case activities_model.ActionPullReviewDismissed:
desc = ctx.Tr("action.review_dismissed_reason") + "\n\n" + act.GetIssueInfos()[2] desc = ctx.Locale.TrString("action.review_dismissed_reason") + "\n\n" + act.GetIssueInfos()[2]
} }
} }
if len(content) == 0 { if len(content) == 0 {
@ -241,7 +243,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
} }
items = append(items, &feeds.Item{ items = append(items, &feeds.Item{
Title: title, Title: template.HTMLEscapeString(title) + string(titleExtra),
Link: link, Link: link,
Description: desc, Description: desc,
IsPermaLink: "false", IsPermaLink: "false",

Some files were not shown because too many files have changed in this diff Show More