diff options
Diffstat (limited to 'modules/translation')
-rw-r--r-- | modules/translation/i18n/i18n.go | 17 | ||||
-rw-r--r-- | modules/translation/i18n/i18n_test.go | 32 | ||||
-rw-r--r-- | modules/translation/i18n/localestore.go | 41 | ||||
-rw-r--r-- | modules/translation/mock.go | 15 | ||||
-rw-r--r-- | modules/translation/translation.go | 16 |
5 files changed, 73 insertions, 48 deletions
diff --git a/modules/translation/i18n/i18n.go b/modules/translation/i18n/i18n.go index 42475545b3..1555cd961e 100644 --- a/modules/translation/i18n/i18n.go +++ b/modules/translation/i18n/i18n.go @@ -4,26 +4,25 @@ package i18n import ( + "html/template" "io" ) var DefaultLocales = NewLocaleStore() type Locale interface { - // Tr translates a given key and arguments for a language - Tr(trKey string, trArgs ...any) string - // Has reports if a locale has a translation for a given key - Has(trKey string) bool + // TrString translates a given key and arguments for a language + TrString(trKey string, trArgs ...any) string + // TrHTML translates a given key and arguments for a language, string arguments are escaped to HTML + TrHTML(trKey string, trArgs ...any) template.HTML + // HasKey reports if a locale has a translation for a given key + HasKey(trKey string) bool } // LocaleStore provides the functions common to all locale stores type LocaleStore interface { io.Closer - // Tr translates a given key and arguments for a language - Tr(lang, trKey string, trArgs ...any) string - // Has reports if a locale has a translation for a given key - Has(lang, trKey string) bool // SetDefaultLang sets the default language to fall back to SetDefaultLang(lang string) // ListLangNameDesc provides paired slices of language names to descriptors @@ -45,7 +44,7 @@ func ResetDefaultLocales() { DefaultLocales = NewLocaleStore() } -// GetLocales returns the locale from the default locales +// GetLocale returns the locale from the default locales func GetLocale(lang string) (Locale, bool) { return DefaultLocales.Locale(lang) } diff --git a/modules/translation/i18n/i18n_test.go b/modules/translation/i18n/i18n_test.go index 1d1be43318..ffe69a74df 100644 --- a/modules/translation/i18n/i18n_test.go +++ b/modules/translation/i18n/i18n_test.go @@ -17,7 +17,7 @@ fmt = %[1]s %[2]s [section] sub = Sub String -mixed = test value; <span style="color: red\; background: none;">more text</span> +mixed = test value; <span style="color: red\; background: none;">%s</span> `) testData2 := []byte(` @@ -32,29 +32,33 @@ sub = Changed Sub String assert.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", testData2, nil)) ls.SetDefaultLang("lang1") - result := ls.Tr("lang1", "fmt", "a", "b") + lang1, _ := ls.Locale("lang1") + lang2, _ := ls.Locale("lang2") + + result := lang1.TrString("fmt", "a", "b") assert.Equal(t, "a b", result) - result = ls.Tr("lang2", "fmt", "a", "b") + result = lang2.TrString("fmt", "a", "b") assert.Equal(t, "b a", result) - result = ls.Tr("lang1", "section.sub") + result = lang1.TrString("section.sub") assert.Equal(t, "Sub String", result) - result = ls.Tr("lang2", "section.sub") + result = lang2.TrString("section.sub") assert.Equal(t, "Changed Sub String", result) - result = ls.Tr("", ".dot.name") + langNone, _ := ls.Locale("none") + result = langNone.TrString(".dot.name") assert.Equal(t, "Dot Name", result) - result = ls.Tr("lang2", "section.mixed") - assert.Equal(t, `test value; <span style="color: red; background: none;">more text</span>`, result) + result2 := lang2.TrHTML("section.mixed", "a&b") + assert.EqualValues(t, `test value; <span style="color: red; background: none;">a&b</span>`, result2) langs, descs := ls.ListLangNameDesc() assert.ElementsMatch(t, []string{"lang1", "lang2"}, langs) assert.ElementsMatch(t, []string{"Lang1", "Lang2"}, descs) - found := ls.Has("lang1", "no-such") + found := lang1.HasKey("no-such") assert.False(t, found) assert.NoError(t, ls.Close()) } @@ -72,9 +76,10 @@ c=22 ls := NewLocaleStore() assert.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, testData2)) - assert.Equal(t, "11", ls.Tr("lang1", "a")) - assert.Equal(t, "21", ls.Tr("lang1", "b")) - assert.Equal(t, "22", ls.Tr("lang1", "c")) + lang1, _ := ls.Locale("lang1") + assert.Equal(t, "11", lang1.TrString("a")) + assert.Equal(t, "21", lang1.TrString("b")) + assert.Equal(t, "22", lang1.TrString("c")) } func TestLocaleStoreQuirks(t *testing.T) { @@ -110,8 +115,9 @@ func TestLocaleStoreQuirks(t *testing.T) { for _, testData := range testDataList { ls := NewLocaleStore() err := ls.AddLocaleByIni("lang1", "Lang1", []byte("a="+testData.in), nil) + lang1, _ := ls.Locale("lang1") assert.NoError(t, err, testData.hint) - assert.Equal(t, testData.out, ls.Tr("lang1", "a"), testData.hint) + assert.Equal(t, testData.out, lang1.TrString("a"), testData.hint) assert.NoError(t, ls.Close()) } diff --git a/modules/translation/i18n/localestore.go b/modules/translation/i18n/localestore.go index f5a951a79f..69cc9fd91d 100644 --- a/modules/translation/i18n/localestore.go +++ b/modules/translation/i18n/localestore.go @@ -5,6 +5,8 @@ package i18n import ( "fmt" + "html/template" + "slices" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -18,6 +20,8 @@ type locale struct { idxToMsgMap map[int]string // the map idx is generated by store's trKeyToIdxMap } +var _ Locale = (*locale)(nil) + type localeStore struct { // After initializing has finished, these fields are read-only. langNames []string @@ -85,20 +89,6 @@ func (store *localeStore) SetDefaultLang(lang string) { store.defaultLang = lang } -// Tr translates content to target language. fall back to default language. -func (store *localeStore) Tr(lang, trKey string, trArgs ...any) string { - l, _ := store.Locale(lang) - - return l.Tr(trKey, trArgs...) -} - -// Has returns whether the given language has a translation for the provided key -func (store *localeStore) Has(lang, trKey string) bool { - l, _ := store.Locale(lang) - - return l.Has(trKey) -} - // Locale returns the locale for the lang or the default language func (store *localeStore) Locale(lang string) (Locale, bool) { l, found := store.localeMap[lang] @@ -113,13 +103,11 @@ func (store *localeStore) Locale(lang string) (Locale, bool) { return l, found } -// Close implements io.Closer func (store *localeStore) Close() error { return nil } -// Tr translates content to locale language. fall back to default language. -func (l *locale) Tr(trKey string, trArgs ...any) string { +func (l *locale) TrString(trKey string, trArgs ...any) string { format := trKey idx, ok := l.store.trKeyToIdxMap[trKey] @@ -141,8 +129,23 @@ func (l *locale) Tr(trKey string, trArgs ...any) string { return msg } -// Has returns whether a key is present in this locale or not -func (l *locale) Has(trKey string) bool { +func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML { + args := slices.Clone(trArgs) + for i, v := range args { + switch v := v.(type) { + case string: + args[i] = template.HTML(template.HTMLEscapeString(v)) + case fmt.Stringer: + args[i] = template.HTMLEscapeString(v.String()) + default: // int, float, include template.HTML + // do nothing, just use it + } + } + return template.HTML(l.TrString(trKey, args...)) +} + +// HasKey returns whether a key is present in this locale or not +func (l *locale) HasKey(trKey string) bool { idx, ok := l.store.trKeyToIdxMap[trKey] if !ok { return false diff --git a/modules/translation/mock.go b/modules/translation/mock.go index 2d0cb17324..1f0559f38d 100644 --- a/modules/translation/mock.go +++ b/modules/translation/mock.go @@ -3,7 +3,10 @@ package translation -import "fmt" +import ( + "fmt" + "html/template" +) // MockLocale provides a mocked locale without any translations type MockLocale struct{} @@ -14,12 +17,16 @@ func (l MockLocale) Language() string { return "en" } -func (l MockLocale) Tr(s string, _ ...any) string { +func (l MockLocale) TrString(s string, _ ...any) string { return s } -func (l MockLocale) TrN(_cnt any, key1, _keyN string, _args ...any) string { - return key1 +func (l MockLocale) Tr(s string, a ...any) template.HTML { + return template.HTML(s) +} + +func (l MockLocale) TrN(cnt any, key1, keyN string, args ...any) template.HTML { + return template.HTML(key1) } func (l MockLocale) PrettyNumber(v any) string { diff --git a/modules/translation/translation.go b/modules/translation/translation.go index dba4de6607..b7c18f610a 100644 --- a/modules/translation/translation.go +++ b/modules/translation/translation.go @@ -5,6 +5,7 @@ package translation import ( "context" + "html/template" "sort" "strings" "sync" @@ -27,8 +28,11 @@ var ContextKey any = &contextKey{} // Locale represents an interface to translation type Locale interface { Language() string - Tr(string, ...any) string - TrN(cnt any, key1, keyN string, args ...any) string + TrString(string, ...any) string + + Tr(key string, args ...any) template.HTML + TrN(cnt any, key1, keyN string, args ...any) template.HTML + PrettyNumber(v any) string } @@ -144,6 +148,8 @@ type locale struct { msgPrinter *message.Printer } +var _ Locale = (*locale)(nil) + // NewLocale return a locale func NewLocale(lang string) Locale { if lock != nil { @@ -216,8 +222,12 @@ var trNLangRules = map[string]func(int64) int{ }, } +func (l *locale) Tr(s string, args ...any) template.HTML { + return l.TrHTML(s, args...) +} + // TrN returns translated message for plural text translation -func (l *locale) TrN(cnt any, key1, keyN string, args ...any) string { +func (l *locale) TrN(cnt any, key1, keyN string, args ...any) template.HTML { var c int64 if t, ok := cnt.(int); ok { c = int64(t) |