diff --git a/modules/translation/i18n/i18n_test.go b/modules/translation/i18n/i18n_test.go
index ffe69a74df..b364992dfe 100644
--- a/modules/translation/i18n/i18n_test.go
+++ b/modules/translation/i18n/i18n_test.go
@@ -4,6 +4,7 @@
 package i18n
 
 import (
+	"html/template"
 	"strings"
 	"testing"
 
@@ -82,6 +83,71 @@ c=22
 	assert.Equal(t, "22", lang1.TrString("c"))
 }
 
+type stringerPointerReceiver struct {
+	s string
+}
+
+func (s *stringerPointerReceiver) String() string {
+	return s.s
+}
+
+type stringerStructReceiver struct {
+	s string
+}
+
+func (s stringerStructReceiver) String() string {
+	return s.s
+}
+
+type errorStructReceiver struct {
+	s string
+}
+
+func (e errorStructReceiver) Error() string {
+	return e.s
+}
+
+type errorPointerReceiver struct {
+	s string
+}
+
+func (e *errorPointerReceiver) Error() string {
+	return e.s
+}
+
+func TestLocaleWithTemplate(t *testing.T) {
+	ls := NewLocaleStore()
+	assert.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", []byte(`key=<a>%s</a>`), nil))
+	lang1, _ := ls.Locale("lang1")
+
+	tmpl := template.New("test").Funcs(template.FuncMap{"tr": lang1.TrHTML})
+	tmpl = template.Must(tmpl.Parse(`{{tr "key" .var}}`))
+
+	cases := []struct {
+		in   any
+		want string
+	}{
+		{"<str>", "<a>&lt;str&gt;</a>"},
+		{[]byte("<bytes>"), "<a>[60 98 121 116 101 115 62]</a>"},
+		{template.HTML("<html>"), "<a><html></a>"},
+		{stringerPointerReceiver{"<stringerPointerReceiver>"}, "<a>{&lt;stringerPointerReceiver&gt;}</a>"},
+		{&stringerPointerReceiver{"<stringerPointerReceiver ptr>"}, "<a>&lt;stringerPointerReceiver ptr&gt;</a>"},
+		{stringerStructReceiver{"<stringerStructReceiver>"}, "<a>&lt;stringerStructReceiver&gt;</a>"},
+		{&stringerStructReceiver{"<stringerStructReceiver ptr>"}, "<a>&lt;stringerStructReceiver ptr&gt;</a>"},
+		{errorStructReceiver{"<errorStructReceiver>"}, "<a>&lt;errorStructReceiver&gt;</a>"},
+		{&errorStructReceiver{"<errorStructReceiver ptr>"}, "<a>&lt;errorStructReceiver ptr&gt;</a>"},
+		{errorPointerReceiver{"<errorPointerReceiver>"}, "<a>{&lt;errorPointerReceiver&gt;}</a>"},
+		{&errorPointerReceiver{"<errorPointerReceiver ptr>"}, "<a>&lt;errorPointerReceiver ptr&gt;</a>"},
+	}
+
+	buf := &strings.Builder{}
+	for _, c := range cases {
+		buf.Reset()
+		assert.NoError(t, tmpl.Execute(buf, map[string]any{"var": c.in}))
+		assert.Equal(t, c.want, buf.String())
+	}
+}
+
 func TestLocaleStoreQuirks(t *testing.T) {
 	const nl = "\n"
 	q := func(q1, s string, q2 ...string) string {
diff --git a/modules/translation/i18n/localestore.go b/modules/translation/i18n/localestore.go
index 69cc9fd91d..b422996984 100644
--- a/modules/translation/i18n/localestore.go
+++ b/modules/translation/i18n/localestore.go
@@ -133,12 +133,14 @@ func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML {
 	args := slices.Clone(trArgs)
 	for i, v := range args {
 		switch v := v.(type) {
+		case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML:
+			// for most basic types (including template.HTML which is safe), just do nothing and use it
 		case string:
-			args[i] = template.HTML(template.HTMLEscapeString(v))
+			args[i] = template.HTMLEscapeString(v)
 		case fmt.Stringer:
 			args[i] = template.HTMLEscapeString(v.String())
-		default: // int, float, include template.HTML
-			// do nothing, just use it
+		default:
+			args[i] = template.HTMLEscapeString(fmt.Sprint(v))
 		}
 	}
 	return template.HTML(l.TrString(trKey, args...))