Template
1
0
Fork 0
mirror of https://codeberg.org/forgejo/forgejo synced 2024-11-27 20:26:09 +01:00

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

@ -289,6 +289,7 @@ package "code.gitea.io/gitea/modules/timeutil"
package "code.gitea.io/gitea/modules/translation" package "code.gitea.io/gitea/modules/translation"
func (MockLocale).Language func (MockLocale).Language
func (MockLocale).TrString
func (MockLocale).Tr func (MockLocale).Tr
func (MockLocale).TrN func (MockLocale).TrN
func (MockLocale).PrettyNumber func (MockLocale).PrettyNumber

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

@ -247,7 +247,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"
@ -110,7 +111,7 @@ func (r *Repository) AllUnitsEnabled(ctx context.Context) 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
@ -162,7 +162,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,
@ -183,13 +183,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
@ -88,20 +92,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]
@ -116,13 +106,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]
@ -144,8 +132,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

@ -105,44 +105,44 @@ 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:
if setting.Service.AllowDotsInUsernames { if setting.Service.AllowDotsInUsernames {
data["ErrorMsg"] = trName + l.Tr("form.username_error") data["ErrorMsg"] = trName + l.TrString("form.username_error")
} else { } else {
data["ErrorMsg"] = trName + l.Tr("form.username_error_no_dots") data["ErrorMsg"] = trName + l.TrString("form.username_error_no_dots")
} }
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 != "" {
@ -151,7 +151,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

@ -779,13 +779,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"
@ -80,119 +81,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)
} }
@ -234,7 +236,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 {
@ -243,7 +245,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
// It's a common practice for feed generators to use plain text titles. // It's a common practice for feed generators to use plain text titles.
// See https://codeberg.org/forgejo/forgejo/pulls/1595 // See https://codeberg.org/forgejo/forgejo/pulls/1595
plainTitle, err := html2text.FromString(title, html2text.Options{OmitLinks: true}) plainTitle, err := html2text.FromString(title+" "+string(titleExtra), html2text.Options{OmitLinks: true})
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -56,7 +56,7 @@ func showUserFeed(ctx *context.Context, formatType string) {
} }
feed := &feeds.Feed{ feed := &feeds.Feed{
Title: ctx.Tr("home.feed_of", ctx.ContextUser.DisplayName()), Title: ctx.Locale.TrString("home.feed_of", ctx.ContextUser.DisplayName()),
Link: &feeds.Link{Href: ctx.ContextUser.HTMLURL()}, Link: &feeds.Link{Href: ctx.ContextUser.HTMLURL()},
Description: ctxUserDescription, Description: ctxUserDescription,
Created: time.Now(), Created: time.Now(),

View file

@ -28,10 +28,10 @@ func ShowReleaseFeed(ctx *context.Context, repo *repo_model.Repository, isReleas
var link *feeds.Link var link *feeds.Link
if isReleasesOnly { if isReleasesOnly {
title = ctx.Tr("repo.release.releases_for", repo.FullName()) title = ctx.Locale.TrString("repo.release.releases_for", repo.FullName())
link = &feeds.Link{Href: repo.HTMLURL() + "/release"} link = &feeds.Link{Href: repo.HTMLURL() + "/release"}
} else { } else {
title = ctx.Tr("repo.release.tags_for", repo.FullName()) title = ctx.Locale.TrString("repo.release.tags_for", repo.FullName())
link = &feeds.Link{Href: repo.HTMLURL() + "/tags"} link = &feeds.Link{Href: repo.HTMLURL() + "/tags"}
} }

View file

@ -27,7 +27,7 @@ func ShowRepoFeed(ctx *context.Context, repo *repo_model.Repository, formatType
} }
feed := &feeds.Feed{ feed := &feeds.Feed{
Title: ctx.Tr("home.feed_of", repo.FullName()), Title: ctx.Locale.TrString("home.feed_of", repo.FullName()),
Link: &feeds.Link{Href: repo.HTMLURL()}, Link: &feeds.Link{Href: repo.HTMLURL()},
Description: repo.Description, Description: repo.Description,
Created: time.Now(), Created: time.Now(),

View file

@ -29,7 +29,7 @@ func Create(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("new_org") ctx.Data["Title"] = ctx.Tr("new_org")
ctx.Data["DefaultOrgVisibilityMode"] = setting.Service.DefaultOrgVisibilityMode ctx.Data["DefaultOrgVisibilityMode"] = setting.Service.DefaultOrgVisibilityMode
if !ctx.Doer.CanCreateOrganization() { if !ctx.Doer.CanCreateOrganization() {
ctx.ServerError("Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed"))) ctx.ServerError("Not allowed", errors.New(ctx.Locale.TrString("org.form.create_org_not_allowed")))
return return
} }
ctx.HTML(http.StatusOK, tplCreateOrg) ctx.HTML(http.StatusOK, tplCreateOrg)
@ -41,7 +41,7 @@ func CreatePost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("new_org") ctx.Data["Title"] = ctx.Tr("new_org")
if !ctx.Doer.CanCreateOrganization() { if !ctx.Doer.CanCreateOrganization() {
ctx.ServerError("Not allowed", errors.New(ctx.Tr("org.form.create_org_not_allowed"))) ctx.ServerError("Not allowed", errors.New(ctx.Locale.TrString("org.form.create_org_not_allowed")))
return return
} }

View file

@ -353,7 +353,7 @@ func ViewProject(ctx *context.Context) {
} }
if boards[0].ID == 0 { if boards[0].ID == 0 {
boards[0].Title = ctx.Tr("repo.projects.type.uncategorized") boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized")
} }
issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards) issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
@ -679,7 +679,7 @@ func MoveIssues(ctx *context.Context) {
board = &project_model.Board{ board = &project_model.Board{
ID: 0, ID: 0,
ProjectID: project.ID, ProjectID: project.ID,
Title: ctx.Tr("repo.projects.type.uncategorized"), Title: ctx.Locale.TrString("repo.projects.type.uncategorized"),
} }
} else { } else {
board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))

View file

@ -100,7 +100,7 @@ func List(ctx *context.Context) {
} }
wf, err := model.ReadWorkflow(bytes.NewReader(content)) wf, err := model.ReadWorkflow(bytes.NewReader(content))
if err != nil { if err != nil {
workflow.ErrMsg = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", err.Error()) workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
workflows = append(workflows, workflow) workflows = append(workflows, workflow)
continue continue
} }
@ -115,7 +115,7 @@ func List(ctx *context.Context) {
continue continue
} }
if !allRunnerLabels.Contains(ro) { if !allRunnerLabels.Contains(ro) {
workflow.ErrMsg = ctx.Locale.Tr("actions.runs.no_matching_online_runner_helper", ro) workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro)
break break
} }
} }

View file

@ -209,8 +209,8 @@ func ViewPost(ctx *context_module.Context) {
Link: run.RefLink(), Link: run.RefLink(),
} }
resp.State.Run.Commit = ViewCommit{ resp.State.Run.Commit = ViewCommit{
LocaleCommit: ctx.Tr("actions.runs.commit"), LocaleCommit: ctx.Locale.TrString("actions.runs.commit"),
LocalePushedBy: ctx.Tr("actions.runs.pushed_by"), LocalePushedBy: ctx.Locale.TrString("actions.runs.pushed_by"),
ShortSha: base.ShortSha(run.CommitSHA), ShortSha: base.ShortSha(run.CommitSHA),
Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA), Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA),
Pusher: pusher, Pusher: pusher,
@ -235,7 +235,7 @@ func ViewPost(ctx *context_module.Context) {
resp.State.CurrentJob.Title = current.Name resp.State.CurrentJob.Title = current.Name
resp.State.CurrentJob.Detail = current.Status.LocaleString(ctx.Locale) resp.State.CurrentJob.Detail = current.Status.LocaleString(ctx.Locale)
if run.NeedApproval { if run.NeedApproval {
resp.State.CurrentJob.Detail = ctx.Locale.Tr("actions.need_approval_desc") resp.State.CurrentJob.Detail = ctx.Locale.TrString("actions.need_approval_desc")
} }
resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json

View file

@ -104,9 +104,9 @@ func CherryPickPost(ctx *context.Context) {
message := strings.TrimSpace(form.CommitSummary) message := strings.TrimSpace(form.CommitSummary)
if message == "" { if message == "" {
if form.Revert { if form.Revert {
message = ctx.Tr("repo.commit.revert-header", sha) message = ctx.Locale.TrString("repo.commit.revert-header", sha)
} else { } else {
message = ctx.Tr("repo.commit.cherry-pick-header", sha) message = ctx.Locale.TrString("repo.commit.cherry-pick-header", sha)
} }
} }

View file

@ -126,7 +126,7 @@ func setCsvCompareContext(ctx *context.Context) {
return CsvDiffResult{nil, ""} return CsvDiffResult{nil, ""}
} }
errTooLarge := errors.New(ctx.Locale.Tr("repo.error.csv.too_large")) errTooLarge := errors.New(ctx.Locale.TrString("repo.error.csv.too_large"))
csvReaderFromCommit := func(ctx *markup.RenderContext, blob *git.Blob) (*csv.Reader, io.Closer, error) { csvReaderFromCommit := func(ctx *markup.RenderContext, blob *git.Blob) (*csv.Reader, io.Closer, error) {
if blob == nil { if blob == nil {

View file

@ -300,9 +300,9 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
message := strings.TrimSpace(form.CommitSummary) message := strings.TrimSpace(form.CommitSummary)
if len(message) == 0 { if len(message) == 0 {
if isNewFile { if isNewFile {
message = ctx.Tr("repo.editor.add", form.TreePath) message = ctx.Locale.TrString("repo.editor.add", form.TreePath)
} else { } else {
message = ctx.Tr("repo.editor.update", form.TreePath) message = ctx.Locale.TrString("repo.editor.update", form.TreePath)
} }
} }
form.CommitMessage = strings.TrimSpace(form.CommitMessage) form.CommitMessage = strings.TrimSpace(form.CommitMessage)
@ -479,7 +479,7 @@ func DiffPreviewPost(ctx *context.Context) {
} }
if diff.NumFiles == 0 { if diff.NumFiles == 0 {
ctx.PlainText(http.StatusOK, ctx.Tr("repo.editor.no_changes_to_show")) ctx.PlainText(http.StatusOK, ctx.Locale.TrString("repo.editor.no_changes_to_show"))
return return
} }
ctx.Data["File"] = diff.Files[0] ctx.Data["File"] = diff.Files[0]
@ -546,7 +546,7 @@ func DeleteFilePost(ctx *context.Context) {
message := strings.TrimSpace(form.CommitSummary) message := strings.TrimSpace(form.CommitSummary)
if len(message) == 0 { if len(message) == 0 {
message = ctx.Tr("repo.editor.delete", ctx.Repo.TreePath) message = ctx.Locale.TrString("repo.editor.delete", ctx.Repo.TreePath)
} }
form.CommitMessage = strings.TrimSpace(form.CommitMessage) form.CommitMessage = strings.TrimSpace(form.CommitMessage)
if len(form.CommitMessage) > 0 { if len(form.CommitMessage) > 0 {
@ -755,7 +755,7 @@ func UploadFilePost(ctx *context.Context) {
if dir == "" { if dir == "" {
dir = "/" dir = "/"
} }
message = ctx.Tr("repo.editor.upload_files_to_dir", dir) message = ctx.Locale.TrString("repo.editor.upload_files_to_dir", dir)
} }
form.CommitMessage = strings.TrimSpace(form.CommitMessage) form.CommitMessage = strings.TrimSpace(form.CommitMessage)

View file

@ -1042,7 +1042,7 @@ func renderErrorOfTemplates(ctx *context.Context, errs map[string]error) string
}) })
if err != nil { if err != nil {
log.Debug("render flash error: %v", err) log.Debug("render flash error: %v", err)
flashError = ctx.Tr("repo.issues.choose.ignore_invalid_templates") flashError = ctx.Locale.TrString("repo.issues.choose.ignore_invalid_templates")
} }
return flashError return flashError
} }
@ -1664,7 +1664,7 @@ func ViewIssue(ctx *context.Context) {
} }
ghostMilestone := &issues_model.Milestone{ ghostMilestone := &issues_model.Milestone{
ID: -1, ID: -1,
Name: ctx.Tr("repo.issues.deleted_milestone"), Name: ctx.Locale.TrString("repo.issues.deleted_milestone"),
} }
if comment.OldMilestoneID > 0 && comment.OldMilestone == nil { if comment.OldMilestoneID > 0 && comment.OldMilestone == nil {
comment.OldMilestone = ghostMilestone comment.OldMilestone = ghostMilestone
@ -1681,7 +1681,7 @@ func ViewIssue(ctx *context.Context) {
ghostProject := &project_model.Project{ ghostProject := &project_model.Project{
ID: -1, ID: -1,
Title: ctx.Tr("repo.issues.deleted_project"), Title: ctx.Locale.TrString("repo.issues.deleted_project"),
} }
if comment.OldProjectID > 0 && comment.OldProject == nil { if comment.OldProjectID > 0 && comment.OldProject == nil {

View file

@ -56,12 +56,12 @@ func GetContentHistoryList(ctx *context.Context) {
for _, item := range items { for _, item := range items {
var actionText string var actionText string
if item.IsDeleted { if item.IsDeleted {
actionTextDeleted := ctx.Locale.Tr("repo.issues.content_history.deleted") actionTextDeleted := ctx.Locale.TrString("repo.issues.content_history.deleted")
actionText = "<i data-history-is-deleted='1'>" + actionTextDeleted + "</i>" actionText = "<i data-history-is-deleted='1'>" + actionTextDeleted + "</i>"
} else if item.IsFirstCreated { } else if item.IsFirstCreated {
actionText = ctx.Locale.Tr("repo.issues.content_history.created") actionText = ctx.Locale.TrString("repo.issues.content_history.created")
} else { } else {
actionText = ctx.Locale.Tr("repo.issues.content_history.edited") actionText = ctx.Locale.TrString("repo.issues.content_history.edited")
} }
username := item.UserName username := item.UserName

View file

@ -123,7 +123,7 @@ func TestDeleteLabel(t *testing.T) {
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
unittest.AssertNotExistsBean(t, &issues_model.Label{ID: 2}) unittest.AssertNotExistsBean(t, &issues_model.Label{ID: 2})
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{LabelID: 2}) unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{LabelID: 2})
assert.Equal(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg) assert.EqualValues(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
} }
func TestUpdateIssueLabel_Clear(t *testing.T) { func TestUpdateIssueLabel_Clear(t *testing.T) {

View file

@ -79,7 +79,7 @@ func NewDiffPatchPost(ctx *context.Context) {
// `message` will be both the summary and message combined // `message` will be both the summary and message combined
message := strings.TrimSpace(form.CommitSummary) message := strings.TrimSpace(form.CommitSummary)
if len(message) == 0 { if len(message) == 0 {
message = ctx.Tr("repo.editor.patch") message = ctx.Locale.TrString("repo.editor.patch")
} }
form.CommitMessage = strings.TrimSpace(form.CommitMessage) form.CommitMessage = strings.TrimSpace(form.CommitMessage)

View file

@ -315,7 +315,7 @@ func ViewProject(ctx *context.Context) {
} }
if boards[0].ID == 0 { if boards[0].ID == 0 {
boards[0].Title = ctx.Tr("repo.projects.type.uncategorized") boards[0].Title = ctx.Locale.TrString("repo.projects.type.uncategorized")
} }
issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards) issuesMap, err := issues_model.LoadIssuesFromBoardList(ctx, boards)
@ -633,7 +633,7 @@ func MoveIssues(ctx *context.Context) {
board = &project_model.Board{ board = &project_model.Board{
ID: 0, ID: 0,
ProjectID: project.ID, ProjectID: project.ID,
Title: ctx.Tr("repo.projects.type.uncategorized"), Title: ctx.Locale.TrString("repo.projects.type.uncategorized"),
} }
} else { } else {
board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID")) board, err = project_model.GetBoard(ctx, ctx.ParamsInt64(":boardID"))

View file

@ -733,7 +733,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
type pullCommitList struct { type pullCommitList struct {
Commits []pull_service.CommitInfo `json:"commits"` Commits []pull_service.CommitInfo `json:"commits"`
LastReviewCommitSha string `json:"last_review_commit_sha"` LastReviewCommitSha string `json:"last_review_commit_sha"`
Locale map[string]string `json:"locale"` Locale map[string]any `json:"locale"`
} }
// GetPullCommits get all commits for given pull request // GetPullCommits get all commits for given pull request
@ -751,7 +751,7 @@ func GetPullCommits(ctx *context.Context) {
} }
// Get the needed locale // Get the needed locale
resp.Locale = map[string]string{ resp.Locale = map[string]any{
"lang": ctx.Locale.Language(), "lang": ctx.Locale.Language(),
"show_all_commits": ctx.Tr("repo.pulls.show_all_commits"), "show_all_commits": ctx.Tr("repo.pulls.show_all_commits"),
"stats_num_commits": ctx.TrN(len(commits), "repo.activity.git_stats_commit_1", "repo.activity.git_stats_commit_n", len(commits)), "stats_num_commits": ctx.TrN(len(commits), "repo.activity.git_stats_commit_1", "repo.activity.git_stats_commit_n", len(commits)),

View file

@ -209,9 +209,9 @@ func SubmitReview(ctx *context.Context) {
if issue.IsPoster(ctx.Doer.ID) { if issue.IsPoster(ctx.Doer.ID) {
var translated string var translated string
if reviewType == issues_model.ReviewTypeApprove { if reviewType == issues_model.ReviewTypeApprove {
translated = ctx.Tr("repo.issues.review.self.approval") translated = ctx.Locale.TrString("repo.issues.review.self.approval")
} else { } else {
translated = ctx.Tr("repo.issues.review.self.rejection") translated = ctx.Locale.TrString("repo.issues.review.self.rejection")
} }
ctx.Flash.Error(translated) ctx.Flash.Error(translated)

View file

@ -38,7 +38,7 @@ func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error {
defer r.Close() defer r.Close()
if form.Avatar.Size > setting.Avatar.MaxFileSize { if form.Avatar.Size > setting.Avatar.MaxFileSize {
return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024)) return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024))
} }
data, err := io.ReadAll(r) data, err := io.ReadAll(r)
@ -47,7 +47,7 @@ func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error {
} }
st := typesniffer.DetectContentType(data) st := typesniffer.DetectContentType(data)
if !(st.IsImage() && !st.IsSvgImage()) { if !(st.IsImage() && !st.IsSvgImage()) {
return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image")) return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_not_a_image"))
} }
if err = repo_service.UploadAvatar(ctx, ctxRepo, data); err != nil { if err = repo_service.UploadAvatar(ctx, ctxRepo, data); err != nil {
return fmt.Errorf("UploadAvatar: %w", err) return fmt.Errorf("UploadAvatar: %w", err)

View file

@ -68,7 +68,7 @@ func SettingsProtectedBranch(c *context.Context) {
} }
c.Data["PageIsSettingsBranches"] = true c.Data["PageIsSettingsBranches"] = true
c.Data["Title"] = c.Tr("repo.settings.protected_branch") + " - " + rule.RuleName c.Data["Title"] = c.Locale.TrString("repo.settings.protected_branch") + " - " + rule.RuleName
users, err := access_model.GetRepoReaders(c, c.Repo.Repository) users, err := access_model.GetRepoReaders(c, c.Repo.Repository)
if err != nil { if err != nil {

View file

@ -743,7 +743,7 @@ func checkHomeCodeViewable(ctx *context.Context) {
} }
} }
ctx.NotFound("Home", fmt.Errorf(ctx.Tr("units.error.no_unit_allowed_repo"))) ctx.NotFound("Home", fmt.Errorf(ctx.Locale.TrString("units.error.no_unit_allowed_repo")))
} }
func checkCitationFile(ctx *context.Context, entry *git.TreeEntry) { func checkCitationFile(ctx *context.Context, entry *git.TreeEntry) {

View file

@ -714,7 +714,7 @@ func NewWikiPost(ctx *context.Context) {
wikiName := wiki_service.UserTitleToWebPath("", form.Title) wikiName := wiki_service.UserTitleToWebPath("", form.Title)
if len(form.Message) == 0 { if len(form.Message) == 0 {
form.Message = ctx.Tr("repo.editor.add", form.Title) form.Message = ctx.Locale.TrString("repo.editor.add", form.Title)
} }
if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.Content, form.Message); err != nil { if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.Content, form.Message); err != nil {
@ -766,7 +766,7 @@ func EditWikiPost(ctx *context.Context) {
newWikiName := wiki_service.UserTitleToWebPath("", form.Title) newWikiName := wiki_service.UserTitleToWebPath("", form.Title)
if len(form.Message) == 0 { if len(form.Message) == 0 {
form.Message = ctx.Tr("repo.editor.update", form.Title) form.Message = ctx.Locale.TrString("repo.editor.update", form.Title)
} }
if err := wiki_service.EditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, oldWikiName, newWikiName, form.Content, form.Message); err != nil { if err := wiki_service.EditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, oldWikiName, newWikiName, form.Content, form.Message); err != nil {

View file

@ -85,7 +85,7 @@ func Dashboard(ctx *context.Context) {
page = 1 page = 1
} }
ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Tr("dashboard") ctx.Data["Title"] = ctxUser.DisplayName() + " - " + ctx.Locale.TrString("dashboard")
ctx.Data["PageIsDashboard"] = true ctx.Data["PageIsDashboard"] = true
ctx.Data["PageIsNews"] = true ctx.Data["PageIsNews"] = true
cnt, _ := organization.GetOrganizationCount(ctx, ctxUser) cnt, _ := organization.GetOrganizationCount(ctx, ctxUser)

View file

@ -126,7 +126,7 @@ func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser *
defer fr.Close() defer fr.Close()
if form.Avatar.Size > setting.Avatar.MaxFileSize { if form.Avatar.Size > setting.Avatar.MaxFileSize {
return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024)) return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024))
} }
data, err := io.ReadAll(fr) data, err := io.ReadAll(fr)
@ -136,7 +136,7 @@ func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser *
st := typesniffer.DetectContentType(data) st := typesniffer.DetectContentType(data)
if !(st.IsImage() && !st.IsSvgImage()) { if !(st.IsImage() && !st.IsSvgImage()) {
return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image")) return errors.New(ctx.Locale.TrString("settings.uploaded_avatar_not_a_image"))
} }
if err = user_service.UploadAvatar(ctx, ctxUser, data); err != nil { if err = user_service.UploadAvatar(ctx, ctxUser, data); err != nil {
return fmt.Errorf("UploadAvatar: %w", err) return fmt.Errorf("UploadAvatar: %w", err)
@ -389,7 +389,7 @@ func UpdateUserLang(ctx *context.Context) {
middleware.SetLocaleCookie(ctx.Resp, ctx.Doer.Language, 0) middleware.SetLocaleCookie(ctx.Resp, ctx.Doer.Language, 0)
log.Trace("User settings updated: %s", ctx.Doer.Name) log.Trace("User settings updated: %s", ctx.Doer.Name)
ctx.Flash.Success(translation.NewLocale(ctx.Doer.Language).Tr("settings.update_language_success")) ctx.Flash.Success(translation.NewLocale(ctx.Doer.Language).TrString("settings.update_language_success"))
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance") ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
} }

View file

@ -39,7 +39,7 @@ func TaskStatus(ctx *context.Context) {
Args: []any{task.Message}, Args: []any{task.Message},
} }
} }
message = ctx.Tr(translatableMessage.Format, translatableMessage.Args...) message = ctx.Locale.TrString(translatableMessage.Format, translatableMessage.Args...)
} }
ctx.JSON(http.StatusOK, map[string]any{ ctx.JSON(http.StatusOK, map[string]any{

View file

@ -152,7 +152,7 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Cont
if ctx.Doer.MustChangePassword { if ctx.Doer.MustChangePassword {
if ctx.Req.URL.Path != "/user/settings/change_password" { if ctx.Req.URL.Path != "/user/settings/change_password" {
if strings.HasPrefix(ctx.Req.UserAgent(), "git") { if strings.HasPrefix(ctx.Req.UserAgent(), "git") {
ctx.Error(http.StatusUnauthorized, ctx.Tr("auth.must_change_password")) ctx.Error(http.StatusUnauthorized, ctx.Locale.TrString("auth.must_change_password"))
return return
} }
ctx.Data["Title"] = ctx.Tr("auth.must_change_password") ctx.Data["Title"] = ctx.Tr("auth.must_change_password")

View file

@ -70,7 +70,7 @@ func (b *BaseConfig) DoNoticeOnSuccess() bool {
// Please note the `status` string will be concatenated with `admin.dashboard.cron.` and `admin.dashboard.task.` to provide locale messages. Similarly `name` will be composed with `admin.dashboard.` to provide the locale name for the task. // Please note the `status` string will be concatenated with `admin.dashboard.cron.` and `admin.dashboard.task.` to provide locale messages. Similarly `name` will be composed with `admin.dashboard.` to provide the locale name for the task.
func (b *BaseConfig) FormatMessage(locale translation.Locale, name, status, doer string, args ...any) string { func (b *BaseConfig) FormatMessage(locale translation.Locale, name, status, doer string, args ...any) string {
realArgs := make([]any, 0, len(args)+2) realArgs := make([]any, 0, len(args)+2)
realArgs = append(realArgs, locale.Tr("admin.dashboard."+name)) realArgs = append(realArgs, locale.TrString("admin.dashboard."+name))
if doer == "" { if doer == "" {
realArgs = append(realArgs, "(Cron)") realArgs = append(realArgs, "(Cron)")
} else { } else {
@ -80,7 +80,7 @@ func (b *BaseConfig) FormatMessage(locale translation.Locale, name, status, doer
realArgs = append(realArgs, args...) realArgs = append(realArgs, args...)
} }
if doer == "" { if doer == "" {
return locale.Tr("admin.dashboard.cron."+status, realArgs...) return locale.TrString("admin.dashboard.cron."+status, realArgs...)
} }
return locale.Tr("admin.dashboard.task."+status, realArgs...) return locale.TrString("admin.dashboard.task."+status, realArgs...)
} }

View file

@ -159,7 +159,7 @@ func RegisterTask(name string, config Config, fun func(context.Context, *user_mo
log.Debug("Registering task: %s", name) log.Debug("Registering task: %s", name)
i18nKey := "admin.dashboard." + name i18nKey := "admin.dashboard." + name
if value := translation.NewLocale("en-US").Tr(i18nKey); value == i18nKey { if value := translation.NewLocale("en-US").TrString(i18nKey); value == i18nKey {
return fmt.Errorf("translation is missing for task %q, please add translation for %q", name, i18nKey) return fmt.Errorf("translation is missing for task %q, please add translation for %q", name, i18nKey)
} }

View file

@ -325,7 +325,7 @@ func (f *NewSlackHookForm) Validate(req *http.Request, errs binding.Errors) bind
errs = append(errs, binding.Error{ errs = append(errs, binding.Error{
FieldNames: []string{"Channel"}, FieldNames: []string{"Channel"},
Classification: "", Classification: "",
Message: ctx.Tr("repo.settings.add_webhook.invalid_channel_name"), Message: ctx.Locale.TrString("repo.settings.add_webhook.invalid_channel_name"),
}) })
} }
return middleware.Validate(errs, ctx.Data, f, ctx.Locale) return middleware.Validate(errs, ctx.Data, f, ctx.Locale)

View file

@ -94,7 +94,7 @@ func SendActivateAccountMail(locale translation.Locale, u *user_model.User) {
// No mail service configured // No mail service configured
return return
} }
sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.Tr("mail.activate_account"), "activate account") sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.activate_account"), "activate account")
} }
// SendResetPasswordMail sends a password reset mail to the user // SendResetPasswordMail sends a password reset mail to the user
@ -104,7 +104,7 @@ func SendResetPasswordMail(u *user_model.User) {
return return
} }
locale := translation.NewLocale(u.Language) locale := translation.NewLocale(u.Language)
sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.Tr("mail.reset_password"), "recover account") sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.reset_password"), "recover account")
} }
// SendActivateEmailMail sends confirmation email to confirm new email address // SendActivateEmailMail sends confirmation email to confirm new email address
@ -130,7 +130,7 @@ func SendActivateEmailMail(u *user_model.User, email string) {
return return
} }
msg := NewMessage(email, locale.Tr("mail.activate_email"), content.String()) msg := NewMessage(email, locale.TrString("mail.activate_email"), content.String())
msg.Info = fmt.Sprintf("UID: %d, activate email", u.ID) msg.Info = fmt.Sprintf("UID: %d, activate email", u.ID)
SendAsync(msg) SendAsync(msg)
@ -158,7 +158,7 @@ func SendRegisterNotifyMail(u *user_model.User) {
return return
} }
msg := NewMessage(u.Email, locale.Tr("mail.register_notify"), content.String()) msg := NewMessage(u.Email, locale.TrString("mail.register_notify"), content.String())
msg.Info = fmt.Sprintf("UID: %d, registration notify", u.ID) msg.Info = fmt.Sprintf("UID: %d, registration notify", u.ID)
SendAsync(msg) SendAsync(msg)
@ -173,7 +173,7 @@ func SendCollaboratorMail(u, doer *user_model.User, repo *repo_model.Repository)
locale := translation.NewLocale(u.Language) locale := translation.NewLocale(u.Language)
repoName := repo.FullName() repoName := repo.FullName()
subject := locale.Tr("mail.repo.collaborator.added.subject", doer.DisplayName(), repoName) subject := locale.TrString("mail.repo.collaborator.added.subject", doer.DisplayName(), repoName)
data := map[string]any{ data := map[string]any{
"locale": locale, "locale": locale,
"Subject": subject, "Subject": subject,

View file

@ -52,8 +52,8 @@ func mailNewUser(ctx context.Context, u *user_model.User, lang string, tos []str
locale := translation.NewLocale(lang) locale := translation.NewLocale(lang)
manageUserURL := setting.AppURL + "admin/users/" + strconv.FormatInt(u.ID, 10) manageUserURL := setting.AppURL + "admin/users/" + strconv.FormatInt(u.ID, 10)
subject := locale.Tr("mail.admin.new_user.subject", u.Name) subject := locale.TrString("mail.admin.new_user.subject", u.Name)
body := locale.Tr("mail.admin.new_user.text", manageUserURL) body := locale.TrString("mail.admin.new_user.text", manageUserURL)
mailMeta := map[string]any{ mailMeta := map[string]any{
"NewUser": u, "NewUser": u,
"NewUserUrl": u.HTMLURL(), "NewUserUrl": u.HTMLURL(),

View file

@ -68,7 +68,7 @@ func mailNewRelease(ctx context.Context, lang string, tos []string, rel *repo_mo
return return
} }
subject := locale.Tr("mail.release.new.subject", rel.TagName, rel.Repo.FullName()) subject := locale.TrString("mail.release.new.subject", rel.TagName, rel.Repo.FullName())
mailMeta := map[string]any{ mailMeta := map[string]any{
"locale": locale, "locale": locale,
"Release": rel, "Release": rel,

View file

@ -56,11 +56,11 @@ func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.U
content bytes.Buffer content bytes.Buffer
) )
destination := locale.Tr("mail.repo.transfer.to_you") destination := locale.TrString("mail.repo.transfer.to_you")
subject := locale.Tr("mail.repo.transfer.subject_to_you", doer.DisplayName(), repo.FullName()) subject := locale.TrString("mail.repo.transfer.subject_to_you", doer.DisplayName(), repo.FullName())
if newOwner.IsOrganization() { if newOwner.IsOrganization() {
destination = newOwner.DisplayName() destination = newOwner.DisplayName()
subject = locale.Tr("mail.repo.transfer.subject_to", doer.DisplayName(), repo.FullName(), destination) subject = locale.TrString("mail.repo.transfer.subject_to", doer.DisplayName(), repo.FullName(), destination)
} }
data := map[string]any{ data := map[string]any{

View file

@ -50,7 +50,7 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod
inviteURL = fmt.Sprintf("%suser/login?redirect_to=%s", setting.AppURL, inviteRedirect) inviteURL = fmt.Sprintf("%suser/login?redirect_to=%s", setting.AppURL, inviteRedirect)
} }
subject := locale.Tr("mail.team_invite.subject", inviter.DisplayName(), org.DisplayName()) subject := locale.TrString("mail.team_invite.subject", inviter.DisplayName(), org.DisplayName())
mailMeta := map[string]any{ mailMeta := map[string]any{
"locale": locale, "locale": locale,
"Inviter": inviter, "Inviter": inviter,

View file

@ -13,9 +13,9 @@
<body> <body>
<p> <p>
{{if .IsPull}} {{if .IsPull}}
{{.locale.Tr "mail.issue_assigned.pull" .Doer.Name $link $repo_url | Str2html}} {{.locale.Tr "mail.issue_assigned.pull" .Doer.Name ($link|Safe) ($repo_url|Safe)}}
{{else}} {{else}}
{{.locale.Tr "mail.issue_assigned.issue" .Doer.Name $link $repo_url | Str2html}} {{.locale.Tr "mail.issue_assigned.issue" .Doer.Name ($link|Safe) ($repo_url|Safe)}}
{{end}} {{end}}
</p> </p>
<div class="footer"> <div class="footer">

View file

@ -28,7 +28,7 @@
{{$newShortSha := ShortSha .Comment.NewCommit}} {{$newShortSha := ShortSha .Comment.NewCommit}}
{{$newCommitLink := printf "<a href='%[1]s'><b>%[2]s</b></a>" (Escape $newCommitUrl) (Escape $newShortSha)}} {{$newCommitLink := printf "<a href='%[1]s'><b>%[2]s</b></a>" (Escape $newCommitUrl) (Escape $newShortSha)}}
{{.locale.Tr "mail.issue.action.force_push" .Doer.Name .Comment.Issue.PullRequest.HeadBranch $oldCommitLink $newCommitLink | Str2html}} {{.locale.Tr "mail.issue.action.force_push" .Doer.Name .Comment.Issue.PullRequest.HeadBranch ($oldCommitLink|Safe) ($newCommitLink|Safe)}}
{{else}} {{else}}
{{.locale.TrN (len .Comment.Commits) "mail.issue.action.push_1" "mail.issue.action.push_n" .Doer.Name .Comment.Issue.PullRequest.HeadBranch (len .Comment.Commits) | Str2html}} {{.locale.TrN (len .Comment.Commits) "mail.issue.action.push_1" "mail.issue.action.push_n" .Doer.Name .Comment.Issue.PullRequest.HeadBranch (len .Comment.Commits) | Str2html}}
{{end}} {{end}}

View file

@ -8,7 +8,7 @@
{{$url := printf "<a href='%[1]s'>%[2]s</a>" (Escape .Link) (Escape .Repo)}} {{$url := printf "<a href='%[1]s'>%[2]s</a>" (Escape .Link) (Escape .Repo)}}
<body> <body>
<p>{{.Subject}}. <p>{{.Subject}}.
{{.locale.Tr "mail.repo.transfer.body" $url | Str2html}} {{.locale.Tr "mail.repo.transfer.body" ($url|Safe)}}
</p> </p>
<p> <p>
--- ---

View file

@ -15,7 +15,7 @@
{{$repo_url := printf "<a href='%s'>%s</a>" (.Release.Repo.HTMLURL | Escape) (.Release.Repo.FullName | Escape)}} {{$repo_url := printf "<a href='%s'>%s</a>" (.Release.Repo.HTMLURL | Escape) (.Release.Repo.FullName | Escape)}}
<body> <body>
<p> <p>
{{.locale.Tr "mail.release.new.text" .Release.Publisher.Name $release_url $repo_url | Str2html}} {{.locale.Tr "mail.release.new.text" .Release.Publisher.Name ($release_url|Safe) ($repo_url|Safe)}}
</p> </p>
<h4>{{.locale.Tr "mail.release.title" .Release.Title}}</h4> <h4>{{.locale.Tr "mail.release.title" .Release.Title}}</h4>
<p> <p>

View file

@ -13,9 +13,9 @@
{{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .SHA)}} {{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .SHA)}}
{{$shalink := printf `<a class="ui primary sha label" href="%s">%s</a>` (Escape $shaurl) (ShortSha .SHA)}} {{$shalink := printf `<a class="ui primary sha label" href="%s">%s</a>` (Escape $shaurl) (ShortSha .SHA)}}
{{if eq .CherryPickType "revert"}} {{if eq .CherryPickType "revert"}}
{{ctx.Locale.Tr "repo.editor.revert" $shalink | Str2html}} {{ctx.Locale.Tr "repo.editor.revert" ($shalink|Safe)}}
{{else}} {{else}}
{{ctx.Locale.Tr "repo.editor.cherry_pick" $shalink | Str2html}} {{ctx.Locale.Tr "repo.editor.cherry_pick" ($shalink|Safe)}}
{{end}} {{end}}
<a class="section" href="{{$.RepoLink}}">{{.Repository.FullName}}</a> <a class="section" href="{{$.RepoLink}}">{{.Repository.FullName}}</a>
<div class="breadcrumb-divider">:</div> <div class="breadcrumb-divider">:</div>

View file

@ -595,11 +595,11 @@
{{$newProjectDisplayHtml = printf `<span data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr $trKey | Escape) (.Project.Title | Escape)}} {{$newProjectDisplayHtml = printf `<span data-tooltip-content="%s">%s</span>` (ctx.Locale.Tr $trKey | Escape) (.Project.Title | Escape)}}
{{end}} {{end}}
{{if and (gt .OldProjectID 0) (gt .ProjectID 0)}} {{if and (gt .OldProjectID 0) (gt .ProjectID 0)}}
{{ctx.Locale.Tr "repo.issues.change_project_at" $oldProjectDisplayHtml $newProjectDisplayHtml $createdStr | Safe}} {{ctx.Locale.Tr "repo.issues.change_project_at" ($oldProjectDisplayHtml|Safe) ($newProjectDisplayHtml|Safe) $createdStr}}
{{else if gt .OldProjectID 0}} {{else if gt .OldProjectID 0}}
{{ctx.Locale.Tr "repo.issues.remove_project_at" $oldProjectDisplayHtml $createdStr | Safe}} {{ctx.Locale.Tr "repo.issues.remove_project_at" ($oldProjectDisplayHtml|Safe) $createdStr}}
{{else if gt .ProjectID 0}} {{else if gt .ProjectID 0}}
{{ctx.Locale.Tr "repo.issues.add_project_at" $newProjectDisplayHtml $createdStr | Safe}} {{ctx.Locale.Tr "repo.issues.add_project_at" ($newProjectDisplayHtml|Safe) $createdStr}}
{{end}} {{end}}
</span> </span>
</div> </div>

View file

@ -56,18 +56,18 @@
{{$mergedStr:= TimeSinceUnix .Issue.PullRequest.MergedUnix ctx.Locale}} {{$mergedStr:= TimeSinceUnix .Issue.PullRequest.MergedUnix ctx.Locale}}
{{if .Issue.OriginalAuthor}} {{if .Issue.OriginalAuthor}}
{{.Issue.OriginalAuthor}} {{.Issue.OriginalAuthor}}
<span class="pull-desc">{{ctx.Locale.Tr "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr | Safe}}</span> <span class="pull-desc">{{ctx.Locale.Tr "repo.pulls.merged_title_desc" .NumCommits ($headHref|Safe) ($baseHref|Safe) $mergedStr}}</span>
{{else}} {{else}}
<a {{if gt .Issue.PullRequest.Merger.ID 0}}href="{{.Issue.PullRequest.Merger.HomeLink}}"{{end}}>{{.Issue.PullRequest.Merger.GetDisplayName}}</a> <a {{if gt .Issue.PullRequest.Merger.ID 0}}href="{{.Issue.PullRequest.Merger.HomeLink}}"{{end}}>{{.Issue.PullRequest.Merger.GetDisplayName}}</a>
<span class="pull-desc">{{ctx.Locale.Tr "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr | Safe}}</span> <span class="pull-desc">{{ctx.Locale.Tr "repo.pulls.merged_title_desc" .NumCommits ($headHref|Safe) ($baseHref|Safe) $mergedStr}}</span>
{{end}} {{end}}
{{else}} {{else}}
{{if .Issue.OriginalAuthor}} {{if .Issue.OriginalAuthor}}
<span id="pull-desc" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref | Safe}}</span> <span id="pull-desc" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits ($headHref|Safe) ($baseHref|Safe)}}</span>
{{else}} {{else}}
<span id="pull-desc" class="pull-desc"> <span id="pull-desc" class="pull-desc">
<a {{if gt .Issue.Poster.ID 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.GetDisplayName}}</a> <a {{if gt .Issue.Poster.ID 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.GetDisplayName}}</a>
{{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref | Safe}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits ($headHref|Safe) ($baseHref|Safe)}}
</span> </span>
{{end}} {{end}}
<span id="pull-desc-edit" class="gt-hidden flex-text-block"> <span id="pull-desc-edit" class="gt-hidden flex-text-block">

View file

@ -309,7 +309,7 @@ func TestLDAPUserSyncWithGroupFilter(t *testing.T) {
// all groups the user is a member of, the user filter is modified accordingly inside // all groups the user is a member of, the user filter is modified accordingly inside
// the addAuthSourceLDAP based on the value of the groupFilter // the addAuthSourceLDAP based on the value of the groupFilter
u := otherLDAPUsers[0] u := otherLDAPUsers[0]
testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").Tr("form.username_password_incorrect")) testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").TrString("form.username_password_incorrect"))
auth.SyncExternalUsers(context.Background(), true) auth.SyncExternalUsers(context.Background(), true)
@ -362,7 +362,7 @@ func TestLDAPUserSigninFailed(t *testing.T) {
addAuthSourceLDAP(t, "", "") addAuthSourceLDAP(t, "", "")
u := otherLDAPUsers[0] u := otherLDAPUsers[0]
testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").Tr("form.username_password_incorrect")) testLoginFailed(t, u.UserName, u.Password, translation.NewLocale("en-US").TrString("form.username_password_incorrect"))
} }
func TestLDAPUserSSHKeySync(t *testing.T) { func TestLDAPUserSSHKeySync(t *testing.T) {

View file

@ -218,7 +218,7 @@ func TestCantMergeWorkInProgress(t *testing.T) {
text := strings.TrimSpace(htmlDoc.doc.Find(".merge-section > .item").Last().Text()) text := strings.TrimSpace(htmlDoc.doc.Find(".merge-section > .item").Last().Text())
assert.NotEmpty(t, text, "Can't find WIP text") assert.NotEmpty(t, text, "Can't find WIP text")
assert.Contains(t, text, translation.NewLocale("en-US").Tr("repo.pulls.cannot_merge_work_in_progress"), "Unable to find WIP text") assert.Contains(t, text, translation.NewLocale("en-US").TrString("repo.pulls.cannot_merge_work_in_progress"), "Unable to find WIP text")
assert.Contains(t, text, "[wip]", "Unable to find WIP text") assert.Contains(t, text, "[wip]", "Unable to find WIP text")
}) })
} }

View file

@ -90,7 +90,7 @@ func TestCreateRelease(t *testing.T) {
session := loginUser(t, "user2") session := loginUser(t, "user2")
createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", false, false) createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", false, false)
checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.stable"), 4) checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").TrString("repo.release.stable"), 4)
} }
func TestDeleteRelease(t *testing.T) { func TestDeleteRelease(t *testing.T) {
@ -137,7 +137,7 @@ func TestCreateReleasePreRelease(t *testing.T) {
session := loginUser(t, "user2") session := loginUser(t, "user2")
createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", true, false) createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", true, false)
checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.prerelease"), 4) checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").TrString("repo.release.prerelease"), 4)
} }
func TestCreateReleaseDraft(t *testing.T) { func TestCreateReleaseDraft(t *testing.T) {
@ -146,7 +146,7 @@ func TestCreateReleaseDraft(t *testing.T) {
session := loginUser(t, "user2") session := loginUser(t, "user2")
createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", false, true) createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", false, true)
checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.draft"), 4) checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").TrString("repo.release.draft"), 4)
} }
func TestCreateReleasePaging(t *testing.T) { func TestCreateReleasePaging(t *testing.T) {
@ -166,11 +166,11 @@ func TestCreateReleasePaging(t *testing.T) {
} }
createNewRelease(t, session, "/user2/repo1", "v0.0.12", "v0.0.12", false, true) createNewRelease(t, session, "/user2/repo1", "v0.0.12", "v0.0.12", false, true)
checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.12", translation.NewLocale("en-US").Tr("repo.release.draft"), 10) checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.12", translation.NewLocale("en-US").TrString("repo.release.draft"), 10)
// Check that user4 does not see draft and still see 10 latest releases // Check that user4 does not see draft and still see 10 latest releases
session2 := loginUser(t, "user4") session2 := loginUser(t, "user4")
checkLatestReleaseAndCount(t, session2, "/user2/repo1", "v0.0.11", translation.NewLocale("en-US").Tr("repo.release.stable"), 10) checkLatestReleaseAndCount(t, session2, "/user2/repo1", "v0.0.11", translation.NewLocale("en-US").TrString("repo.release.stable"), 10)
} }
func TestViewReleaseListNoLogin(t *testing.T) { func TestViewReleaseListNoLogin(t *testing.T) {
@ -265,7 +265,7 @@ func TestReleaseOnCommit(t *testing.T) {
session := loginUser(t, "user2") session := loginUser(t, "user2")
createNewReleaseTarget(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", "65f1bf27bc3bf70f64657658635e66094edbcb4d", false, false) createNewReleaseTarget(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", "65f1bf27bc3bf70f64657658635e66094edbcb4d", false, false)
checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").Tr("repo.release.stable"), 4) checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", translation.NewLocale("en-US").TrString("repo.release.stable"), 4)
} }
func TestViewTagsList(t *testing.T) { func TestViewTagsList(t *testing.T) {

View file

@ -63,39 +63,39 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) {
OldRefSubURL: "branch/master", OldRefSubURL: "branch/master",
NewBranch: "feature/test1", NewBranch: "feature/test1",
ExpectedStatus: http.StatusSeeOther, ExpectedStatus: http.StatusSeeOther,
FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test1"), FlashMessage: translation.NewLocale("en-US").TrString("repo.branch.create_success", "feature/test1"),
CheckBranch: true, CheckBranch: true,
}, },
{ {
OldRefSubURL: "branch/master", OldRefSubURL: "branch/master",
NewBranch: "", NewBranch: "",
ExpectedStatus: http.StatusSeeOther, ExpectedStatus: http.StatusSeeOther,
FlashMessage: translation.NewLocale("en-US").Tr("form.NewBranchName") + translation.NewLocale("en-US").Tr("form.require_error"), FlashMessage: translation.NewLocale("en-US").TrString("form.NewBranchName") + translation.NewLocale("en-US").TrString("form.require_error"),
}, },
{ {
OldRefSubURL: "branch/master", OldRefSubURL: "branch/master",
NewBranch: "feature=test1", NewBranch: "feature=test1",
ExpectedStatus: http.StatusSeeOther, ExpectedStatus: http.StatusSeeOther,
FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature=test1"), FlashMessage: translation.NewLocale("en-US").TrString("repo.branch.create_success", "feature=test1"),
CheckBranch: true, CheckBranch: true,
}, },
{ {
OldRefSubURL: "branch/master", OldRefSubURL: "branch/master",
NewBranch: strings.Repeat("b", 101), NewBranch: strings.Repeat("b", 101),
ExpectedStatus: http.StatusSeeOther, ExpectedStatus: http.StatusSeeOther,
FlashMessage: translation.NewLocale("en-US").Tr("form.NewBranchName") + translation.NewLocale("en-US").Tr("form.max_size_error", "100"), FlashMessage: translation.NewLocale("en-US").TrString("form.NewBranchName") + translation.NewLocale("en-US").TrString("form.max_size_error", "100"),
}, },
{ {
OldRefSubURL: "branch/master", OldRefSubURL: "branch/master",
NewBranch: "master", NewBranch: "master",
ExpectedStatus: http.StatusSeeOther, ExpectedStatus: http.StatusSeeOther,
FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.branch_already_exists", "master"), FlashMessage: translation.NewLocale("en-US").TrString("repo.branch.branch_already_exists", "master"),
}, },
{ {
OldRefSubURL: "branch/master", OldRefSubURL: "branch/master",
NewBranch: "master/test", NewBranch: "master/test",
ExpectedStatus: http.StatusSeeOther, ExpectedStatus: http.StatusSeeOther,
FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.branch_name_conflict", "master/test", "master"), FlashMessage: translation.NewLocale("en-US").TrString("repo.branch.branch_name_conflict", "master/test", "master"),
}, },
{ {
OldRefSubURL: "commit/acd1d892867872cb47f3993468605b8aa59aa2e0", OldRefSubURL: "commit/acd1d892867872cb47f3993468605b8aa59aa2e0",
@ -106,7 +106,7 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) {
OldRefSubURL: "commit/65f1bf27bc3bf70f64657658635e66094edbcb4d", OldRefSubURL: "commit/65f1bf27bc3bf70f64657658635e66094edbcb4d",
NewBranch: "feature/test3", NewBranch: "feature/test3",
ExpectedStatus: http.StatusSeeOther, ExpectedStatus: http.StatusSeeOther,
FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test3"), FlashMessage: translation.NewLocale("en-US").TrString("repo.branch.create_success", "feature/test3"),
CheckBranch: true, CheckBranch: true,
}, },
{ {
@ -114,14 +114,14 @@ func testCreateBranches(t *testing.T, giteaURL *url.URL) {
NewBranch: "v1.0.0", NewBranch: "v1.0.0",
CreateRelease: "v1.0.0", CreateRelease: "v1.0.0",
ExpectedStatus: http.StatusSeeOther, ExpectedStatus: http.StatusSeeOther,
FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.tag_collision", "v1.0.0"), FlashMessage: translation.NewLocale("en-US").TrString("repo.branch.tag_collision", "v1.0.0"),
}, },
{ {
OldRefSubURL: "tag/v1.0.0", OldRefSubURL: "tag/v1.0.0",
NewBranch: "feature/test4", NewBranch: "feature/test4",
CreateRelease: "v1.0.1", CreateRelease: "v1.0.1",
ExpectedStatus: http.StatusSeeOther, ExpectedStatus: http.StatusSeeOther,
FlashMessage: translation.NewLocale("en-US").Tr("repo.branch.create_success", "feature/test4"), FlashMessage: translation.NewLocale("en-US").TrString("repo.branch.create_success", "feature/test4"),
CheckBranch: true, CheckBranch: true,
}, },
} }

View file

@ -49,10 +49,10 @@ func TestSignin(t *testing.T) {
password string password string
message string message string
}{ }{
{username: "wrongUsername", password: "wrongPassword", message: translation.NewLocale("en-US").Tr("form.username_password_incorrect")}, {username: "wrongUsername", password: "wrongPassword", message: translation.NewLocale("en-US").TrString("form.username_password_incorrect")},
{username: "wrongUsername", password: "password", message: translation.NewLocale("en-US").Tr("form.username_password_incorrect")}, {username: "wrongUsername", password: "password", message: translation.NewLocale("en-US").TrString("form.username_password_incorrect")},
{username: "user15", password: "wrongPassword", message: translation.NewLocale("en-US").Tr("form.username_password_incorrect")}, {username: "user15", password: "wrongPassword", message: translation.NewLocale("en-US").TrString("form.username_password_incorrect")},
{username: "user1@example.com", password: "wrongPassword", message: translation.NewLocale("en-US").Tr("form.username_password_incorrect")}, {username: "user1@example.com", password: "wrongPassword", message: translation.NewLocale("en-US").TrString("form.username_password_incorrect")},
} }
for _, s := range samples { for _, s := range samples {

View file

@ -69,9 +69,9 @@ func TestSignupEmail(t *testing.T) {
wantStatus int wantStatus int
wantMsg string wantMsg string
}{ }{
{"exampleUser@example.com\r\n", http.StatusOK, translation.NewLocale("en-US").Tr("form.email_invalid")}, {"exampleUser@example.com\r\n", http.StatusOK, translation.NewLocale("en-US").TrString("form.email_invalid")},
{"exampleUser@example.com\r", http.StatusOK, translation.NewLocale("en-US").Tr("form.email_invalid")}, {"exampleUser@example.com\r", http.StatusOK, translation.NewLocale("en-US").TrString("form.email_invalid")},
{"exampleUser@example.com\n", http.StatusOK, translation.NewLocale("en-US").Tr("form.email_invalid")}, {"exampleUser@example.com\n", http.StatusOK, translation.NewLocale("en-US").TrString("form.email_invalid")},
{"exampleUser@example.com", http.StatusSeeOther, ""}, {"exampleUser@example.com", http.StatusSeeOther, ""},
} }

View file

@ -85,7 +85,7 @@ func TestRenameInvalidUsername(t *testing.T) {
htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc := NewHTMLParser(t, resp.Body)
assert.Contains(t, assert.Contains(t,
htmlDoc.doc.Find(".ui.negative.message").Text(), htmlDoc.doc.Find(".ui.negative.message").Text(),
translation.NewLocale("en-US").Tr("form.username_error"), translation.NewLocale("en-US").TrString("form.username_error"),
) )
unittest.AssertNotExistsBean(t, &user_model.User{Name: invalidUsername}) unittest.AssertNotExistsBean(t, &user_model.User{Name: invalidUsername})
@ -147,7 +147,7 @@ func TestRenameReservedUsername(t *testing.T) {
htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc := NewHTMLParser(t, resp.Body)
assert.Contains(t, assert.Contains(t,
htmlDoc.doc.Find(".ui.negative.message").Text(), htmlDoc.doc.Find(".ui.negative.message").Text(),
translation.NewLocale("en-US").Tr("user.form.name_reserved", reservedUsername), translation.NewLocale("en-US").TrString("user.form.name_reserved", reservedUsername),
) )
unittest.AssertNotExistsBean(t, &user_model.User{Name: reservedUsername}) unittest.AssertNotExistsBean(t, &user_model.User{Name: reservedUsername})